This article mainly refers to the official documentation, and then saves the search history as an example to operate a wave.

The preparatory work

Room provides an abstraction layer on top of SQLite for smooth database access while taking full advantage of SQLite’s power.

Rely on

To use Room in your application, add the following dependencies to your application’s build.gradle file.

dependencies {
  def room_version = "2.2.5"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}
Copy the code

The major components

  • The database: contains the database holder and serves as the primary access point to the underlying connection to apply retained persistent relational data. use@DatabaseAnnotated classes should meet the following criteria:
    • Is to extendRoomDatabaseAbstract class of.
    • Add a list of entities associated with the database to the comment.
    • Contains zero parameters and returns use@DaoAbstract methods of annotated classes.

    At run time, you can callRoom.databaseBuilder()Room.inMemoryDatabaseBuilder() To obtainDatabase The instance.

  • Entity: indicates a table in the database.
  • DAO: contains methods used to access the database.

The application uses the Room database to get the DATA access object (DAO) associated with the database. The application then uses each DAO to retrieve entities from the database, and then saves all changes to those entities back into the database. Finally, the application uses entities to get and set values corresponding to table columns in the database.

The relationship is shown as follows:

Ok, once you understand the basic concept, let’s see how it works.

Entity

@Entity(tableName = "t_history")
data class History(

    / * * *@PrimaryKeyPrimary key, autoGenerate = true increment *@ColumnInfoColumn, typeAffinity field type *@IgnoreIgnore the * /

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    val id: Int? = null.@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    valname: String? .@ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)
    valinsertTime: String? .@ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)
    val type: Int = 1
)
Copy the code
  • The Entity object corresponds to a table and is used@EntityAnnotate and declare your table name
  • @PrimaryKeyThe primary key,autoGenerate = trueSince the increase
  • @ColumnInfoColumn and declare the column name,typeAffinityThe field type
  • @IgnoreDeclare objects to be ignored

A very simple table, mainly name and insertTime fields.

DAO

@Dao
interface HistoryDao {

    // Query all search histories by type
    @Query("SELECT * FROM t_history WHERE type=:type")
    fun getAll(type: Int = 1): Flow<List<History>>

    @ExperimentalCoroutinesApi
    fun getAllDistinctUntilChanged(a) = getAll().distinctUntilChanged()

    // Add a search history
    @Insert
    fun insert(history: History)

    // Delete a search history
    @Delete
    fun delete(history: History)

    // Update a search history
    @Update
    fun update(history: History)

    // Delete a search history by id
    @Query("DELETE FROM t_history WHERE id = :id")
    fun deleteByID(id: Int)

    // Delete all search history
    @Query("DELETE FROM t_history")
    fun deleteAll(a)
}
Copy the code
  • @ Insert: increase
  • @ Delete: Delete
  • @ Update: change
  • @ Query: to check

One thing to note here is that the set returned by querying all the search histories I’ve decorated with Flow.

Whenever any data in the database is updated, no matter which row of data is changed, the query operation is re-executed and the Flow is distributed again.

Similarly, if an unrelated data update occurs, the Flow will also be distributed and receive the same data as before.

This is because the SQLite database’s content update notification function is in units of Table data, not Row data, so it triggers content update notifications whenever data in the Table is updated. Room does not know which data in the table is updated, so it refires the Query operation defined in the DAO. You can use operators of Flow, such as distinctUntilChanged, to ensure that you are notified only when there is an update to the data you are interested in.

    // Query all search histories by type
    @Query("SELECT * FROM t_history WHERE type=:type")
    fun getAll(type: Int = 1): Flow<List<History>>

    @ExperimentalCoroutinesApi
    fun getAllDistinctUntilChanged(a) = getAll().distinctUntilChanged()
Copy the code

The database

@Database(entities = [History::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {

    abstract fun historyDao(a): HistoryDao

    companion object {
        private const val DATABASE_NAME = "history.db"
        private lateinit var mPersonDatabase: HistoryDatabase

        // Note: If your application is running in a single process, follow the singleton design pattern when instantiating AppDatabase objects.
        // The cost per RoomDatabase instance is quite high, and you rarely need to access multiple instances in a single process
        fun getInstance(context: Context): HistoryDatabase {
            if (!this::mPersonDatabase.isInitialized) {
                // Create an instance of the database
                mPersonDatabase = Room.databaseBuilder(
                    context.applicationContext,
                    HistoryDatabase::class.java,
                    DATABASE_NAME
                ).build()
            }
            return mPersonDatabase
        }
    }

}
Copy the code
  • use@DatabaseAnnotation statement
  • entitiesArray that corresponds to all tables in this database
  • versionDatabase Version number

Note:

If your application is running in a single process, follow the singleton design pattern when instantiating AppDatabase objects. The cost of each RoomDatabase instance is quite high, and you rarely need to access multiple instances in a single process.

use

Get the database where you need it

mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
Copy the code

Get search history

    private fun getSearchHistory(a) {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.getAll().collect {
                withContext(Dispatchers.Main){
                    / / update the UI}}}}Copy the code

Collect is a way for Flow to obtain data, not the only way to view documents.

Why put it in a coroutine? Because database operations are time consuming, and coroutines can easily specify threads so that they don’t block the UI thread.

Check Flow source also found that Flow is under the coroutine package

package kotlinx.coroutines.flow
Copy the code

Collect, for example, is also modified by suspend. Since suspend is supported, it is not beautiful to cooperate with coroutines.

    @InternalCoroutinesApi
    public suspend fun collect(collector: FlowCollector<T>)
Copy the code

Save search records

    private fun saveSearchHistory(text: String) {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))
        }
    }
Copy the code

Clearing local History

    private fun cleanHistory(a) {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.deleteAll()
        }
    }
Copy the code

Author: yechaoa

Database Upgrade

Database upgrade is an important operation, after all, may cause data loss, is also a very serious problem.

Room uses the Migration class to perform the upgrade, and we just have to tell the Migration class what has changed, such as adding fields or tables.

Define Migration class

    /** * new updateTime column */
    private val MIGRATION_1_2: Migration = object : Migration(1.2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/** * database version 2->3 new label table */
    private val MIGRATION_2_3: Migration = object : Migration(2.3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}Copy the code

Migration takes two arguments:

  • StartVersion old version
  • EndVersion new version

Notifying the database of updates

    mPersonDatabase = Room.databaseBuilder(
        context.applicationContext,
        HistoryDatabase::class.java,
        DATABASE_NAME
    ).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
        .build()
Copy the code

The complete code

@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {

    abstract fun historyDao(a): HistoryDao

    companion object {
        private const val DATABASE_NAME = "history.db"
        private lateinit var mPersonDatabase: HistoryDatabase

        fun getInstance(context: Context): HistoryDatabase {
            if (!this::mPersonDatabase.isInitialized) {
                // Create an instance of the database
                mPersonDatabase = Room.databaseBuilder(
                    context.applicationContext,
                    HistoryDatabase::class.java,
                    DATABASE_NAME
                ).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return mPersonDatabase
        }

        /** * new updateTime column */
        private val MIGRATION_1_2: Migration = object : Migration(1.2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/**
         * 数据库版本 2->3 新增label表
         */
        private val MIGRATION_2_3: Migration = object : Migration(2.3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}}}Copy the code

Note: a version number change in the @database annotation should also be added to the entities parameter if it is a new table.

Recommended upgrade sequence

Change the version number -> Add Migration -> Add to databaseBuilder

Configure compiler options

Room has the following annotation processor options:

  • room.schemaLocation: Configures and enables the ability to export the database schema to a JSON file in a given directory. For more details, see Room Migration.
  • room.incremental: Enables the Gradle incremental comment processor.
  • room.expandProjection: Configure Room to rewrite the query so that its top star projection expands to contain only the columns defined in the DAO method return type.
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += [
                    "room.schemaLocation":"$projectDir/schemas".toString(),
                    "room.incremental":"true"."room.expandProjection":"true"]}}}}Copy the code

After configuration, compile and run, the Schemas folder will be generated under the Module folder, where there is a JSON file that contains the basic information of the database.

{
  "formatVersion": 1."database": {
    "version": 1."identityHash": "xxx"."entities": [{"tableName": "t_history"."createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `insert_time` TEXT, `type` INTEGER NOT NULL)"."fields": [{"fieldPath": "id"."columnName": "id"."affinity": "INTEGER"."notNull": false
          },
          {
            "fieldPath": "name"."columnName": "name"."affinity": "TEXT"."notNull": false
          },
          {
            "fieldPath": "insertTime"."columnName": "insert_time"."affinity": "TEXT"."notNull": false
          },
          {
            "fieldPath": "type"."columnName": "type"."affinity": "INTEGER"."notNull": true}]."primaryKey": {
          "columnNames": [
            "id"]."autoGenerate": true
        },
        "indices": []."foreignKeys": []}],"views": []."setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)"."INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'xxx')"]}}Copy the code

Ok, the basic use of the explanation is over, if it is useful to you, click a thumbs up ^ _ ^

reference

  • Room Official Document
  • Room Update Log
  • Flow official documentation
  • Actual combat | using Flow in the Room
  • Coroutines Flow best practice | application based on the Android developer summit