深入探讨 Room 2.4.0 的最新进展

深入探讨 Room 2.4.0 的最新进展

在 Google I/O 2019,我们分享了 Room 2.2 的最新进展。尽管当时已经支持了很多功能,如 支持 Flow API支持预填充数据库支持一对一及多对多数据库关系,但是开发者们对 Room 有着更高的期望,我们也致力于此,在 2.2.0 - 2.4.0 版本中发布了很多开发者们期待的新功能!包括自动化迁移,关系查询方法以及支持 Kotlin Symbol Processing (KSP) 等等。下面我们就来逐一介绍这些新功能!

如果您更喜欢通过视频了解此内容,请在此处查看:

△ 深入探讨 Room 2.4.0 的最新进展

自动化迁移

在谈自动化迁移之前,先看看什么是数据库迁移。假如您更改了数据库 schema,就需要根据数据库版本进行迁移,以防用户设备内置数据库中现有数据丢失。

如果您使用 Room,那么在 数据库迁移 过程中会进行检查并验证更新后的 schema,另外您也可以在 @Database 中设置 exportSchema,来导出 schema 信息。

对于 Room 2.4.0 版本之前的数据库迁移,您需要实现 Migration 类,并在其中编写大量复杂冗长的 SQL 语句,来处理不同版本之间的迁移。这种手动迁移的形式,非常容易引发各种错误。

现在 Room 支持了自动迁移,让我们通过两个示例来对比手动迁移和自动迁移:

修改表名

假设有一个包含两个表的数据库,表名分别是 Artist 和 Track,现在想要将表名 Track 改为 Song。

如果使用手动迁移,必须编写和执行 SQL 语句才能更改,需要如下操作:

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

如果使用自动迁移,您只需要在定义数据库时添加 @AutoMigration 配置,同时提供两个版本数据库导出的 schema。Auto Migration API 将为您生成并实现 migrate 函数,编写并执行迁移所需的 SQL 语句。代码如下:

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

修改字段名

现在,演示一个更复杂的场景,假设我们要将 Artist 表中的 singerName 字段修改为 artistName。

虽然这看起来很简单,但是由于 SQLite 并没有提供用于此操作的 API,因此我们需要根据 ALERT TABLE 实现,有如下几步操作:

  1. 获取需要执行更改的表
  2. 创建一个新表,满足更改后的表结构
  3. 将旧表的数据插入到新表中
  4. 删除旧表
  5. 把新表重命名为原表名称
  6. 进行外键检查

迁移代码如下:

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`)")
    }
}

从上面的代码就可以看出,如果使用手动迁移,即使两个版本之间仅有一处更改,也可能需要繁琐的操作,并且这些操作极易出错。

那我们来看看自动迁移该如何使用。在上面的示例中,自动迁移无法直接处理重命名表中的某一列,因为 Room 在进行自动迁移时,会遍历两个版本的数据库 schema,通过比较来检测两者之间的更改。在处理列或者表的重命名时,Room 无法明确发生了什么更改,此时可能有两种情况,是删除后新添加的?还是进行了重命名?处理列或者表的删除操作时也会有同样问题。

所以我们需要给 Room 添加一些配置来说明这些不确定的场景——定义 AutoMigrationSpec。AutoMigrationSpec 是定义自动迁移规范的接口,我们需要实现该类,并在实现类上添加和修改相对应的注解。本例中,我们使用 @RenameColumn 注解,并在注解参数中,提供表名、列的原始名称以及更新后的名称。如果在迁移完成之后,还需要执行其他任务,可以在 AutoMigrationSpec 的 onPostMigrate 函数中进行处理,相关代码如下:

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

完成 AutoMigrationSpec 的实现后,还需要将其添加到数据库定义时配置的 @AutoMigation 中,同时提供两个版本的数据库 schema,Auto Migration API 将生成和实现 migrate 函数,配置代码如下:

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

上面的案例提到了 @RenameColumn,相关的变更处理注解有如下几种:

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

假设在同一迁移中有多个更改需要配置,我们还可以通过这些可复用的注解简化处理。

测试自动迁移

假设您在一开始就使用了自动迁移,现在希望测试其是否正常工作,可以使用现有的 MigrationTestHelper API 无需任何更改。如以下代码:

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

在无需额外配置的情况下,MigrationTestHelper 将自动运行并验证所有自动迁移。在 Room 内部,如果存在自动迁移,它们将自动添加到需要运行和验证的迁移列表中。

需要注意的是,开发者提供的迁移具有更高的优先级,也就是说,如果您定义自动迁移的两个版本之间,已经定义了手动迁移,那么手动迁移将优先于自动迁移。

关系查询方法

关系查询也是新增的一个重要功能,我们还是用一个示例说明。

假设我们使用与之前相同的数据库和表,现在表名分别为 Artist 和 Song。如果我们希望获得音乐人到歌曲的映射集合,就要在 artistName 和 songName 之间建立关系。如下图中 Purple Lloyd 与其热门歌曲《Another Tile in the Ceiling》和《The Great Pig in the Sky》匹配,AB/CD 将与其热门歌曲《Back in White》和《Highway to Heaven》匹配。

使用 @Relation

如果使用 @Relation 和 @Embedded 反应该映射关系,则有如下代码:

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

在此方案中,我们创建了全新的 数据类,将音乐人和歌曲列表相关系。但是这种额外创建 data 类的方式,容易造成代码繁冗的问题。而 @Relation 中并不支持过滤、排序、分组或组合键,其设计初衷也是用于数据库中只有一些简单的关系,虽然受限于关系结果,但这是一种快速完成较简单任务的便捷方法。

所以为了支持复杂关系的处理,我们并没有扩展 @Relation,而是希望您充分发挥 SQL 的潜能,因为它的功能非常强大。

接下来让我们来看看 Room 如何利用全新的功能来解决这一问题。

使用全新关系查询功能

为了表示前面所示的音乐人与其歌曲之间的关系,我们现在可以编写一个简单的 DAO 方法,其返回类型为 Map,而我们需要做的仅仅是提供 @Query 和返回标记,Room 将为您处理其余的一切!相关代码如下:

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

在 Room 内部,实际上要做的是找到音乐人、歌曲和 Cursor 并将它们放入 Map 中的 Key 和 Value 中。

在本例中,涉及到一对多的映射关系,其中单个音乐人映射到一个歌曲集合。当然我们也可以使用一对一映射,如下文所示:

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

使用 @MapInfo

实际上,您可以通过 @MapInfo 在映射的使用中更加灵活。

MapInfo 是用于说明开发者配置的辅助程序 API,类似于前面谈到的自动迁移更改注解。您可以使用 MapInfo 明确说明您希望如何处理查询到的 Cursor 所包含的信息。使用 MapInfo 注解您可以指定输出的数据结构中用于查询的 Key 和 Value 所映射的列。需要注意,用于 Key 的类型必须实现 equals 和 hashCode 函数因为这对映射过程非常重要。

假设我们希望以 artistName 作为 Key,获得歌曲列表作为 Value,则代码实现如下:

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

在该示例中,artistName 用作 Key,音乐人被映射到其歌曲名称列表,最后 artistName 被映射到其歌曲名称列表。

MapInfo 注解使您可以灵活地使用特定列,而不是整个 data 类从而进行更加自定义的映射。

其他优势

关系查询方法的另一个好处是支持更多的数据操作,可以通过这个新功能来支持分组、筛选等功能。示例代码如下:

@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>

最后需要注意多重映射是一个核心返回类型,可以使用 Room 已经支持的各种可观察类型封装 (包括 LiveData、Flowable、Flow)。因此,关系查询方法可让您轻松地在数据库中定义任意数量的关联关系。

更多新功能

内置 Enum 类型转换器

现在,如果系统未提供任何类型转换器,Room 将默认使用 "枚举 - 字符串" 双向类型转换器。如果已存在适用于枚举的类型转换器,Room 将优先使用该转换器,而不使用默认转换器。

支持查询回调

现在,Room 提供了一个通用 callback API RoomDatabase.QueryCallback,此 API 会在执行查询时被调用,这将非常有助于我们在 Debug 模式下记录日志。可通过 RoomDatabase.Builder#setQueryCallback() 设置此回调。

如果您希望记录查询以了解数据库中发生了什么,该功能可以帮助您进行记录,示例代码如下:

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

支持原生 Paging 3.0 API

Room 现在支持为返回值类型为 androidx.paging.PagingSource 且带 @Query 注解的方法生成实现。

支持 RxJava3

Room 现在支持 RxJava3 类型。通过依赖 androidx.room:room-rxjava3,您可以声明返回值类型为 Flowable、Single、Maybe 和 Completable 的 DAO 方法。

支持 Kotlin Symbol Processing (KSP)

KSP 用于替代 KAPT,它能够在 Kotlin 编译器上以原生方式运行注解处理器,从而显著缩短构建时间。

对于 Room,使用 KSP 有如下好处:

  • 提高 2 倍的构建速度;
  • 直接处理 Kotlin 代码,更好的支持空安全。

随着 KSP 的稳定,Room 将使用其功能实现 value 类、生成 Kotlin 代码等。

从 KAPT 迁移到 KSP 非常简单,只需使用 KSP 插件替换 KAPT 插件,并使用 KSP 配置 Room 注解处理器,示例代码如下:

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"
}

总结

自动化迁移、关系查询方法、KSP——Room 带来了很多新功能,希望大家和我们一样对所有这些 Room 更新感到兴奋,记得查看并开始在您的应用中使用这些新功能!

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗