At Google I/O 2019, we shared an update on Room 2.2. Although there was already support for many features, such as support for Flow apis, support for pre-populate databases, and support for one-to-one and many-to-many database relationships, developers had higher expectations for Room, and we worked on that, releasing a lot of new features that developers were looking forward to in versions 2.2.0 to 2.4.0! This includes automated migration, relational query methods, and support for Kotlin Symbol Processing (KSP). Let’s take a look at each of these new features!

If you prefer to see this in video, check it out here.

Automated migration

Before we talk about automated migration, let’s look at what database migration is. If you change the database schema, you need to migrate based on the database version to prevent loss of existing data in the built-in database on the user device.

If you use Room, the updated schema is checked and validated during Database migration, and you can also set exportSchema in @DATABASE to export the schema information.

For database migrations prior to Room 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 releases. This form of manual migration is very error-prone.

Now that Room supports automatic migration, let’s use two examples to compare manual and automatic migration:

Modify the name of the table

Suppose you have a database with two tables named Artist and Track, and now you want to change the name Track to Song.

If you use manual migration, you must write and execute SQL statements to make changes, as follows:

val MIGRATION_1_2: Migration = Migration(1.2) {
    fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE `Track` RENAME TO `Song`")}}Copy the code

If automatic migration is used, you only need to add @automigration configuration when defining the database and provide the schemas exported from both versions of the database. The Auto Migration API generates and implements the Migrate function for you, writing and executing the SQL statements required for the Migration. The code is as follows:

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

Changing the field name

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

This may seem simple, but since SQLite does not provide an API for this operation, we need to follow the following steps based on the ALERT TABLE implementation:

  1. Gets the table to which changes need to be made
  2. Create a new table that satisfies the changed table structure
  3. Insert data from the old table into the new table
  4. Remove old table
  5. Rename the new table to the old table name
  6. Perform 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`)")}}Copy the code

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

Let’s look at how automatic migration can be used. In the example above, automatic migration cannot directly handle a column in a renamed table, because Room iterates through two versions of the database Schema to detect changes between them by comparing them. When handling a column or table rename, Room does not know what has changed. Or was it renamed? The same problem occurs when dealing with column or table deletions.

So we need to add some configuration to Room to illustrate these uncertain scenarios — define AutoMigrationSpec. AutoMigrationSpec is the interface that defines the automatic migration specification. We need to implement this class and add and modify corresponding annotations on the implementation class. In this case, 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 you need to perform other tasks after the migration, you can do so in AutoMigrationSpec’s onPostMigrate function:

@RenameColumn(
    tableName = "Artist",
    fromColumnName = "singerName",
    toColumnName = "artistName"
)
static class MySpec : AutoMigrationSpec {
    override fun onPostMigrate(db: SupportSQLiteDatabase) {
        // Handle the callback of the task after the migration is complete}}Copy the code

After AutoMigrationSpec is implemented, it needs to be added to @Automigation, which provides two versions of database schema. The AutoMigration API generates and implements migrate functions. The configuration code is as follows:

@Database( version = MusicDatabase.LATEST_VERSION entities = {Song.class, Names, Rations = {@automatic (from = 1,to = 2, spec = myspec.class)
    }
    exprotSchema = true
)
Copy the code

The above example mentions @renamecolumn, and the relevant change handling annotations are as follows:

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

Assuming multiple changes need to be configured in the same migration, we can also simplify processing with these reusable annotations.

Testing 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(a) {
    val helper = MigrationTestHelper(
        InstrumentationRegisty.getInstrumentation(),
            AutoMigrationDbKotlin::class.java
    )
    val db: SupportSQLiteDatabase = helper.runMigrationsAndValidate(
        name = TEST_DB,
        version = 2,
        validateDroppedTables = true)}Copy the code

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

It is important to note that migrations provided by the developer have a higher priority, that is, if manual migration is already defined between two versions of automatic migration that you define, manual migration takes precedence over automatic migration.

Relational query method

Relational queries are also an important new feature, which we’ll illustrate with an example.

Suppose we use the same database and table as before, now named Artist and Song respectively. If we want to get a set of musician-to-song mappings, we need to establish a relationship between artistName and songName. Purple Lloyd, pictured below, matches his hits “Another Tile in the Ceiling” and “The Great Pig in the Sky,” AB/CD will match its hits Back in White and Highway to Heaven.

Using the @ function

If the mapping Relation between @relation and @embedded is used, the code is as follows:

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

In this scenario, we created a new data class that associates musicians with song lists. However, this additional way of creating data classes tends to cause code problems. However, @Relation does not support filtering, sorting, grouping or combination of keys. It is also designed to be used for some simple relationships in the database. Although limited by the relationship results, it is a convenient way to quickly complete simple tasks.

So instead of extending @Relation to support the processing of complex relationships, we want you to use SQL to its full potential because it is so powerful.

Let’s take a look at how Room solves this problem with new features.

Use the new relationship query function

To represent the relationship between the musician and his song shown above, we can now write a simple DAO method that returns type Map, and all we need to do is provide @Query and return tags, and Room will take care of the rest for you! The relevant codes are as follows:

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

Inside the Room, all you really need to do is find the musician, 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 musician maps to a collection of songs. Of course, we can also use one-to-one mapping, as shown below:

// One-to-one mapping
@Query("SELECT * FROM Song JOIN Artist ON Song.songArtistName = Artist.artistName")
fun getSongAndArtist(a): Map<Song, Artist>
Copy the code

Using the @ MapInfo

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

MapInfo is a helper API for explaining developer configuration, similar to the automatic migration change annotations discussed earlier. You can use MapInfo to specify how you want to process the information contained in the Cursor that you query. Using the MapInfo annotation you can specify the columns mapped to the keys and values used for the query in the output data structure. Note that the type used for Key must implement the equals and hashCode functions because this is important to the mapping process.

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

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

In this example, the artistName is used as the Key, the musician is mapped to his list of song names, and finally the artistName is mapped to his list of song names.

MapInfo annotations give you the flexibility to use specific columns rather than the entire Data class for more custom mapping.

Other advantages

Another benefit of the relational query approach is that it supports more data manipulation, enabling grouping, filtering, and so on. Example 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(a): Map<Artist, Integer>
Copy the code

Finally, note that multiple mapping is a core return type that can be encapsulated using the various observable types that Room already supports (including LiveData, Flowable, Flow). Therefore, the relational query approach lets you easily define any number of relationships in the database.

More new features

Built-in Enum type converter

Now, if the system does not provide any type converters, Room will default to an “enumeration-string” bidirectional type converter. If a type converter exists for enumeration, Room will use that converter in preference to the default converter.

Query callback is supported

Now, Room provides a general callback API RoomDatabase. QueryCallback, the API will be invoked when the query is performed, it will be very help us to log in the Debug mode. This callback can be set by roomDatabase.builder #setQueryCallback().

If you want to record a query to see what is happening in the database, this feature can help you to do so, with the following example code:

fun setUp(a) {
    database = databaseBuilder.setQueryCallback(
        RoomDatabase.QueryCallback{ sqlQuery, bindArgs ->
            // Log all queries triggered
            Log.d(TAG, "SQL Query $sqlQuery")
        },
        myBackgroundExecutor
    ).build()
}
Copy the code

Native Paging 3.0 apis are supported

Support for the return value type for androidx Room now. The paging. PagingSource and @ Query annotation methods generate implementation.

Support RxJava3

Room now supports the RxJava3 type. By relying on Androidx. room: room-rxJava3, you can declare DAO methods that return values of type Flowable, Single, Maybe, and Completable.

Support Kotlin Symbol Processing (KSP)

KSP is used as a replacement for KAPT and has the ability to run the annotation processor natively on the Kotlin compiler, significantly reducing build times.

For Room, using KSP has the following benefits:

  • Increased build speed by 2 times;
  • Handle Kotlin code directly for better null security.

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

Migrating from KAPT to KSP is as simple as replacing the KAPT plug-in with the KSP plug-in and configuring the Room annotation processor with KSP, as shown in the following code:

Plugins ("kotlin-kapt") id("com.google.devtools. KSP ")} dependencies // kapt "androidx.room:room-compiler:$version" ksp "androidx.room:room-compiler:$version" }Copy the code

conclusion

Automated migrations, relational query methods, and KSP — Room bring a lot of new features. I hope you’re as excited as we are about all of these Room updates. Check out and start using these new features in your applications!

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!