头图

At Google I/O 2019, we shared an Room 2.2 . Although many functions were already supported at that time, such as supporting Flow API , supporting pre-populated database , supporting one-to-one and many-to-many database relationships , but developers also have higher expectations for Room, we As such, a lot of new features that developers have been waiting for have been released in versions 2.2.0 - 2.4.0! Includes automated migrations, relational query methods, and support for Kotlin Symbol Processing (KSP) and more. Let's take a look at these new features one by one!

If you prefer to see this through video, check it out here:

https://www.bilibili.com/video/BV1LR4y1M7R9/?aid=338171545&cid=486220837&page=1

△ An in-depth look at the latest developments in Room 2.4.0

Automated Migration

Before talking about automated migrations, let's take a look at what database migrations are. If you change the database schema, you need to migrate according to the database version to prevent the loss of existing data in the built-in database of the user device.

If you use Room, the updated schema will be checked and verified during the migration of , and you can also set exportSchema in @Database to export schema information.

For database migrations prior to Room version 2.4.0, you need to implement the Migration class and write a lot of complex and lengthy SQL statements in it to handle migrations between versions. This form of manual migration is very prone to various errors.

Now that Room supports automatic migrations, let's compare manual and automatic migrations with two examples:

Modify table name

Suppose there is a database with two tables, the table names are Artist and Track, and now you want to change the table name Track to Song.

If using manual migration, SQL statements must be written and executed to make changes, which require the following:

val MIGRATION_1_2: Migration = Migration(1, 2) {
    fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE `Track` RENAME TO `Song`")
    }
}

If you use automatic migration, you only need to add the @AutoMigration configuration when defining the database, and provide the schema exported by the two versions of the database. The Auto Migration API will generate and implement the migrate function for you, writing and executing the SQL statements required for the migration. code show as below:

@Database(
    version = MusicDatabase.LATEST_VERSION
    entities = {Song.class, Artist.class}
    autoMigrations = {
        @AutoMigration (from = 1,to = 2)
    }
    exprotSchema = true
)

Modify field name

Now, to demonstrate a more complex scenario, suppose we want to modify the singerName field in the Artist table to artistName.

Although this seems simple, since SQLite does not provide an API for this operation, we need to implement the following steps according to ALERT TABLE:

  1. Get the table that needs to be changed
  2. Create a new table, satisfying the changed table structure
  3. Insert data from old table into new table
  4. delete old table
  5. Rename the new table to the original table name
  6. Do foreign key checks

The migration code is as follows:

val MIGRATION_1_2: Migration = Mirgation(1, 2) {
    fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("CREATE TABLE IF NOT EXISTS `_new_Artist`(`id` INTEGER NOT 
            NULL, artistName` TEXT, PRIMARY KEY(`id`)"
        )
        db.execSQL("INSERT INTO `_new_Artist` (id,artistName) 
            SELECT id, singerName FROM `Artist`"
        )
        db.execSQL("DROP TABLE `Artist`")
        db.execSQL("ALTER TABLE `_new_Artist` RENAME TO `Artist`")
        db.execSQL("PRAGMA foreign_key_check(`Artist`)")
    }
}

As you can see from the code above, using manual migrations, even if there is only one change between the two versions, can be tedious and error-prone.

Then let's see how to use automatic migration. In the example above, the automatic migration cannot directly handle renaming a column in the table, because when Room does the automatic migration, it traverses the two versions of the database schema and compares to detect changes between the two. When processing the renaming of a column or table, Room cannot know what has changed. At this time, there may be two cases, is it newly added after deletion? Or was it renamed? The same problem occurs when dealing with delete operations on columns or tables.

So we need to add some configuration to Room to account for these uncertain scenarios - define AutoMigrationSpec. AutoMigrationSpec is an interface that defines an automatic migration specification. We need to implement this class and add and modify the corresponding annotations on the implementation class. In this example, we use the @RenameColumn annotation, and in the annotation parameters, provide the table name, the original name of the column, and the updated name. If other tasks need to be performed after the migration is completed, they can be processed in the onPostMigrate function of AutoMigrationSpec. The relevant code is as follows:

@RenameColumn(
    tableName = "Artist",
    fromColumnName = "singerName",
    toColumnName = "artistName"
)
static class MySpec : AutoMigrationSpec {
    override fun onPostMigrate(db: SupportSQLiteDatabase) {
        // 迁移工作完成后处理任务的回调
    }
}

After completing the implementation of AutoMigrationSpec, you also need to add it to @AutoMigation configured when the database is defined, and provide two versions of the database schema. The Auto Migration API will generate and implement the migrate function. The configuration code is as follows:

@Database(
    version = MusicDatabase.LATEST_VERSION
    entities = {Song.class, Artist.class}
    autoMigrations = {
        @AutoMigration (from = 1,to = 2,spec = MySpec.class)
    }
    exprotSchema = true
)

The above case mentioned @RenameColumn, and the related change processing annotations are as follows:

  • @DeleteColumn
  • @DeleteTable
  • @RenameColumn
  • @RenameTable

We can also simplify processing with these reusable annotations, assuming there are multiple changes that need to be configured in the same migration.

test automatic migration

Assuming you started with automatic migration and now want to test that it works, you can use the existing MigrationTestHelper API without any changes. Such as the following code:

@Test
fun v1ToV2() {
    val helper = MigrationTestHelper(
        InstrumentationRegisty.getInstrumentation(),
            AutoMigrationDbKotlin::class.java
    )
    val db: SupportSQLiteDatabase = helper.runMigrationsAndValidate(
        name = TEST_DB,
        version = 2,
        validateDroppedTables = true
    )
}

MigrationTestHelper will automatically run and verify all automatic migrations without additional configuration. Inside Room, if there are automatic migrations, they are automatically added to the list of migrations that need to be run and verified.

It should be noted that the migration provided by the developer has a higher priority, that is, if you define a manual migration between the two versions of the automatic migration, the manual migration will take precedence over the automatic migration.

Relational query method

Relational query is also an important new function, we still use an example to illustrate.

Suppose we use the same database and table as before, now named Artist and Song. If we want to get a set of artist-to-song mappings, we need to establish a relationship between artistName and songName. Purple Lloyd is matched with its hits "Another Tile in the Ceiling" and "The Great Pig in the Sky" in the image below, and the AB/CD will be matched with its hits "Back in White" and "Highway to Heaven".

using @Relation

If you use @Relation and @Embedded to reflect the mapping relationship, you have the following code:

data class ArtistAndSongs(
    @Embedded
    val artist: Artist,
    @Relation(...)
    val songs: List<Song>
)
 
@Query("SELECT * FROM Artist")
fun getArtistsAndSongs(): List<ArtistAndSongs>

In this scenario, we create a brand new data class to relate artists to song lists. However, this way of additionally creating data classes can easily cause the problem of cumbersome code. However, @Relation does not support filtering, sorting, grouping or key combination. Its original intention is to use only some simple relationships in the database. Although it is limited by the relationship results, it is a convenient way to quickly complete simpler tasks. .

So in order to support the processing of complex relations, we did not extend @Relation, but hope that you can use the full potential of SQL, because it is very powerful.

Let's take a look at how Room solves this problem with a brand new feature.

uses the new relational query function

To represent the relationship between artists and their songs as shown earlier, we can now write a simple DAO method with a return type of Map, and all we need to do is provide the @Query and the return tag, and Room will handle it for you Everything else! The relevant code is as follows:

@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getAllArtistAndTheirSongsList(): Map<Artist, List<Song>>

Inside the Room, what is actually done is to find the Artist, Song and Cursor and put them into the Key and Value in the Map.

In this example, a one-to-many mapping is involved, where a single artist maps to a collection of songs. Of course we can also use a one-to-one mapping, as shown below:

// 一对一映射关系
@Query("SELECT * FROM Song JOIN Artist ON Song.songArtistName = Artist.artistName")
fun getSongAndArtist(): Map<Song, Artist>

using @MapInfo

In fact, you can be more flexible in the use of maps with @MapInfo.

MapInfo is a helper API for specifying developer configurations, similar to the automatic migration change annotations discussed earlier. You can use MapInfo to specify what you want to do with the information contained in the queried Cursor. Using MapInfo annotations you can specify the columns in the output data structure to which the Key and Value are mapped for the query. Note that the type used for Key must implement the equals and hashCode functions as this is very important to the mapping process.

Suppose we want to use artistName as Key and get the list of songs as Value, the code is implemented as follows:

@MapInfo(keyColumn = "artistName")
@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getArtistNameToSongs(): Map<String, List<Song>>

In this example, artistName is used as the Key, artists are mapped to their list of song names, and finally artistName is mapped to their list of song names.

MapInfo annotations give you the flexibility to use specific columns instead of the entire data class for more custom mapping.

Other advantages

Another benefit of the relational query method is that it supports more data operations, which can be used to support grouping, filtering and other functions through this new function. The sample code is as follows:

@MapInfo(valueColumn = "songCount")
@Query("
    SELECT *, COUNT(songId) as songCount FROM Artist JOIN Song ON
    Artist.artistName = Song.songArtistName
    GROUP BY artistName WHERE songCount = 2
")
fun getArtistAndSongCountMap(): Map<Artist, Integer>

Finally, note that multimap is a core return type that can be encapsulated with various observable types already supported by Room (including LiveData, Flowable, Flow). Therefore, the relational query method allows you to easily define any number of associations in the database.

More new features

Built-in Enum type converter

Room will now default to the "enum - string" bidirectional type converter if no type converter is provided by the system. If a type converter for the enum already exists, Room will use that converter over the default converter.

supports query callback

Room now provides a generic callback API, RoomDatabase.QueryCallback, which is called when a query is executed, which will be very helpful for us to log in Debug mode. This callback can be set via RoomDatabase.Builder#setQueryCallback().

If you wish to log queries to understand what is happening in the database, this function can help you to do so, the sample code is as follows:

fun setUp() {
    database = databaseBuilder.setQueryCallback(
        RoomDatabase.QueryCallback{ sqlQuery, bindArgs ->
            // 记录所有触发的查询
            Log.d(TAG, "SQL Query $sqlQuery")
        },
        myBackgroundExecutor
    ).build()
}

supports native Paging 3.0 API

Room now supports generating implementations for methods annotated with @Query that return a value of type androidx.paging.PagingSource.

supports RxJava3

Room now supports RxJava3 types. By relying on androidx.room:room-rxjava3, you can declare DAO methods with return value types of Flowable, Single, Maybe, and Completable.

supports Kotlin Symbol Processing (KSP)

KSP is used as a replacement for KAPT, which can run annotation processors natively on the Kotlin compiler, resulting in significantly faster build times.

For Room, using KSP has the following benefits:

  • 2x faster builds;
  • Direct processing of Kotlin code, better support for null safety.

As KSP stabilizes, Room will use its functionality to implement value classes, generate Kotlin code, and more.

Migrating from KAPT to KSP is very simple, just replace the KAPT plugin with the KSP plugin and configure the Room annotation processor with KSP, the sample code is as follows:

plugins{
    // 使用 KSP 插件替换 KATP 插件
    // id("kotlin-kapt") 
    id("com.google.devtools.ksp")
}
 
dependencies{
    // 使用 KSP 配置替代 KAPT
    // kapt "androidx.room:room-compiler:$version"
    ksp "androidx.room:room-compiler:$version"
}

summary

Automated Migrations, Relational Query Methods, KSP - Room brings a lot of new features, hope you're as excited about all these Room updates as we are, check it out and start using these new features in your apps!

You are welcome here to submit feedback to us, or share your favorite content and found problems. Your feedback is very important to us, thank you for your support!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。