深入探讨 Room 2.4.0 的最新进展

2022 年 1 月 20 日 谷歌开发者

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


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

△ 深入探讨 Room 2.4.0 的最新进展
  • Bilibili 视频链接

    https://www.bilibili.com/video/BV1LR4y1M7R9/

  • 支持预填充数据库
    https://developer.android.google.cn/training/data-storage/room/prepopulate

  • Kotlin Symbol Processing (KSP)
    https://github.com/google/ksp



自动化迁移


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


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

  • 数据库迁移
    https://developer.android.google.cn/training/data-storage/room/migrating-db-versions


对于 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 无需任何更改。如以下代码:
@Testfun 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 中并不支持过滤、排序、分组或组合键,其设计初衷也是用于数据库中只有一些简单的关系,虽然受限于关系结果,但这是一种快速完成较简单任务的便捷方法。


  • 数据类
    https://www.kotlincn.net/docs/reference/data-classes.html


所以为了支持复杂关系的处理,我们并没有扩展 @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 更新感到兴奋,记得查看并开始在您的应用中使用这些新功能!


您也可以通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!


推荐阅读

如页面未加载,请刷新重试

  点击屏末  | 即刻了解 Room 的更多内容



登录查看更多
0

相关内容

数据库( Database )或数据库管理系统( Database management systems )是按照数据结构来组织、存储和管理数据的仓库。目前数据管理不再仅仅是存储和管理数据,而转变成用户所需要的各种数据管理的方式。
神经结构搜索的研究进展综述
专知会员服务
35+阅读 · 2022年1月12日
【2021新书】ApachePulsar 实战,402页pdf
专知会员服务
69+阅读 · 2021年12月29日
面向任务型的对话系统研究进展
专知会员服务
57+阅读 · 2021年11月17日
专知会员服务
68+阅读 · 2021年10月6日
「因果发现和因果推理」简明介绍,37页ppt
专知会员服务
114+阅读 · 2021年4月5日
【NLPCC教程】图神经网络与网络嵌入前沿进展,142页ppt
专知会员服务
71+阅读 · 2020年10月19日
鲁棒模式识别研究进展
专知会员服务
40+阅读 · 2020年8月9日
欢迎体验 | Android 13 开发者预览版 2
谷歌开发者
1+阅读 · 2022年3月18日
实战 | 在应用中使用 Compose Material 3
谷歌开发者
0+阅读 · 2022年2月21日
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
新版本系统适配: Android 12 中的兼容性变更
谷歌开发者
0+阅读 · 2022年1月13日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
获取数据并绑定到 UI | MAD Skills
谷歌开发者
0+阅读 · 2021年11月1日
使用 Kotlin 重写 AOSP 日历应用
谷歌开发者
0+阅读 · 2021年9月15日
庖丁解牛-图解MySQL 8.0优化器查询解析篇
阿里技术
0+阅读 · 2021年9月10日
国家自然科学基金
3+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
1+阅读 · 2011年12月31日
Arxiv
0+阅读 · 2022年4月20日
Arxiv
0+阅读 · 2022年4月7日
Arxiv
28+阅读 · 2021年9月18日
VIP会员
相关VIP内容
神经结构搜索的研究进展综述
专知会员服务
35+阅读 · 2022年1月12日
【2021新书】ApachePulsar 实战,402页pdf
专知会员服务
69+阅读 · 2021年12月29日
面向任务型的对话系统研究进展
专知会员服务
57+阅读 · 2021年11月17日
专知会员服务
68+阅读 · 2021年10月6日
「因果发现和因果推理」简明介绍,37页ppt
专知会员服务
114+阅读 · 2021年4月5日
【NLPCC教程】图神经网络与网络嵌入前沿进展,142页ppt
专知会员服务
71+阅读 · 2020年10月19日
鲁棒模式识别研究进展
专知会员服务
40+阅读 · 2020年8月9日
相关资讯
欢迎体验 | Android 13 开发者预览版 2
谷歌开发者
1+阅读 · 2022年3月18日
实战 | 在应用中使用 Compose Material 3
谷歌开发者
0+阅读 · 2022年2月21日
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
新版本系统适配: Android 12 中的兼容性变更
谷歌开发者
0+阅读 · 2022年1月13日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
获取数据并绑定到 UI | MAD Skills
谷歌开发者
0+阅读 · 2021年11月1日
使用 Kotlin 重写 AOSP 日历应用
谷歌开发者
0+阅读 · 2021年9月15日
庖丁解牛-图解MySQL 8.0优化器查询解析篇
阿里技术
0+阅读 · 2021年9月10日
相关基金
国家自然科学基金
3+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
1+阅读 · 2011年12月31日
Top
微信扫码咨询专知VIP会员