一、Room:SQLite 的现代化封装 Room 是 Google 在 SQLite 之上构建的 ORM 抽象层。与直接使用 SQLiteOpenHelper 相比,Room 通过编译期注解处理生成实现代码,避免了手写 SQL 字符串的繁琐和运行时反射的性能损耗。Room 的三个核心注解:**@Entity** 定义表结构,**@Dao** 定义数据访问接口,**@Database** 声明数据库。三者在编译期由 Room 的 Annotation Processor(room-compiler)生成对应的 Impl 类,底层依然使用 SupportSQLiteOpenHelper 和 SupportSQLiteDatabase 操作 SQLite。
1.1 Room 整体架构 ┌──────────────────────────────────────────────────┐ │ @Database │ │ (AppDatabase) │ │ - entities: List<Class<*>> │ │ - version: Int │ │ - exportSchema: Boolean │ └──────────────┬───────────────────────────────────┘ │ 编译期生成 ┌──────────────▼───────────────────────────────────┐ │ AppDatabase_Impl │ │ - 实现 createOpenHelper() │ │ - 创建 SupportSQLiteOpenHelper │ │ - 初始化 DAO 实例 │ └──────────────┬───────────────────────────────────┘ │ 持有 ┌──────────────▼──────────────┐ ┌───────────────┐ │ @Dao │ │ @Entity │ │ (UserDao) │ │ (User) │ │ - @Insert │ │ - @PrimaryKey │ │ - @Update │ │ - @ColumnInfo │ │ - @Delete │ │ - @ForeignKey │ │ - @Query │ │ - @Index │ └──────────────┬──────────────┘ └───────────────┘ │ 编译期生成 ┌──────────────▼───────────────────────────────────┐ │ UserDao_Impl │ │ - 实现每个 DAO 方法 │ │ - 绑定参数、执行 SQL、映射结果 │ │ - 处理 Cursor 转换 │ │ - 处理协程/Flow 返回类型 │ └──────────────────────────────────────────────────┘
1.2 Room 编译期代码生成流程 Room 的编译期代码生成由 room-compiler 模块完成,处理阶段如下:
1. Processing Round 1: 收集所有 @Entity、@Dao、@Database 注解 ↓ 2. Processing Round 2: 对每个 @Entity 生成表创建/迁移信息 ↓ 3. Processing Round 3: 对每个 @Dao 生成 Impl 类 ↓ 4. Processing Round 4: 对 @Database 生成 Impl 类
生成的 UserDao_Impl 示例(简化版):
public class UserDao_Impl implements UserDao { private final RoomDatabase __db; private final EntityInsertionAdapter<User> __insertionAdapterOfUser; private final EntityDeletionOrUpdateAdapter<User> __deletionAdapterOfUser; public UserDao_Impl (RoomDatabase db) { this .__db = db; this .__insertionAdapterOfUser = new EntityInsertionAdapter <User>(db) { @Override public String createQuery () { return "INSERT OR REPLACE INTO `users` (`id`,`user_name`,`age`) VALUES (?,?,?)" ; } @Override public void bind (SupportSQLiteStatement stmt, User value) { stmt.bindLong(1 , value.getId()); if (value.getName() == null ) { stmt.bindNull(2 ); } else { stmt.bindString(2 , value.getName()); } stmt.bindLong(3 , value.getAge()); } }; } @Override public void insertUser (final User user) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __insertionAdapterOfUser.insert(user); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } } @Override public LiveData<List<User>> getAllUsers () { final String _sql = "SELECT * FROM users ORDER BY user_name ASC" ; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0 ); return __db.getInvalidationTracker().createLiveData( new String []{"users" }, false , new Callable <List<User>>() { @Override public List<User> call () throws Exception { final Cursor _cursor = __db.query(_statement); try { } finally { _cursor.close(); } } }); } }
二、核心用法与代码示例 2.1 Entity 详细定义 @Entity( tableName = "users" , indices = [ Index(value = ["user_name" ], unique = true), // 单一索引 Index(value = ["age" , "city" ], unique = false) // 复合索引 ], foreignKeys = [ ForeignKey( entity = City::class, parentColumns = ["id" ], childColumns = ["city_id" ], onDelete = ForeignKey.CASCADE, // 级联删除 onUpdate = ForeignKey.CASCADE // 级联更新 ) ] ) data class User ( @PrimaryKey(autoGenerate = true) val id: Int , @ColumnInfo(name = "user_name" ) val name: String, val age: Int , @ColumnInfo(name = "city_id" ) val cityId: Int , @Ignore val avatarUrl: String? = null )
2.2 @Embedded:嵌套对象 data class Address ( val street: String, val city: String, val zipCode: String ) @Entity(tableName = "users" ) data class User ( @PrimaryKey val id: Int , val name: String, @Embedded val address: Address )
如果多个 @Embedded 的字段名冲突,可以用 prefix 区分:
@Entity data class User ( @PrimaryKey val id: Int , @Embedded(prefix = "home_" ) val homeAddress: Address, @Embedded(prefix = "work_" ) val workAddress: Address )
2.3 @Relation:关联查询 data class UserWithOrders ( @Embedded val user: User, @Relation( parentColumn = "id" , entityColumn = "user_id" ) val orders: List<Order> ) data class UserWithProfile ( @Embedded val user: User, @Relation( parentColumn = "id" , entityColumn = "user_id" ) val profile: Profile ) data class PlaylistWithSongs ( @Embedded val playlist: Playlist, @Relation( parentColumn = "id" , entityColumn = "id" , associateBy = Junction( value = PlaylistSongCrossRef::class, parentColumn = "playlist_id" , entityColumn = "song_id" ) ) val songs: List<Song> )
2.4 DAO 方法类型全面解析 @Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser (user: User ) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers (users: List <User >) @Insert fun insertUserSync (user: User ) : Long @Update suspend fun updateUser (user: User ) : Int @Update(entity = User::class) fun updateUserName (userId: Int , @ColumnInfo(name = "user_name" ) name: String ) @Delete suspend fun deleteUser (user: User ) @Query("DELETE FROM users WHERE id = :userId" ) suspend fun deleteUserById (userId: Int ) @Query("SELECT * FROM users" ) suspend fun getAllUsers () : List<User> @Query("SELECT * FROM users WHERE user_name = :name" ) suspend fun getUserByName (name: String ) : User? @Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge" ) suspend fun getUsersByAgeRange (minAge: Int , maxAge: Int ) : List<User> @Query("SELECT * FROM users ORDER BY user_name ASC" ) fun getAllUsersLiveData () : LiveData<List<User>> @Query("SELECT * FROM users WHERE age > :minAge" ) fun getUsersOlderThan (minAge: Int ) : Flow<List<User>> @Query("SELECT * FROM users ORDER BY user_name ASC" ) fun getUsersPagingSource () : PagingSource<Int , User> @Query("SELECT user_name, age FROM users" ) suspend fun getUserNamesAndAges () : List<UserNameAndAge> @Query(""" SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY city_id ORDER BY age DESC) as rn FROM users ) WHERE rn = 1 """ ) suspend fun getOldestUserPerCity () : List<User> @RawQuery suspend fun rawQuery (query: SupportSQLiteQuery ) : List<User> }
2.5 数据库定义和使用 @Database( entities = [User::class, Order::class, City::class], version = 1, exportSchema = true, // 导出 schema 到 JSON 文件(推荐,用于迁移测试) autoMigrations = [AutoMigration(from = 1, to = 2)] // Room 2.4+ 的自动迁移 ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase () { abstract fun userDao () : UserDao abstract fun orderDao () : OrderDao abstract fun cityDao () : CityDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance (context: Context ) : AppDatabase { return INSTANCE ?: synchronized(this ) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class .java, "app.db" ) .addCallback(object : RoomDatabase.Callback() { override fun onCreate (db: SupportSQLiteDatabase ) { super .onCreate(db) ioThread { getInstance(context).userDao().insertUsers(PREDEFINED_USERS) } } }) .build().also { INSTANCE = it } } } } }
三、迁移(Migration)深度讲解 3.1 手动迁移 val MIGRATION_1_2 = object : Migration(1 , 2 ) { override fun migrate (database: SupportSQLiteDatabase ) { database.execSQL("ALTER TABLE users ADD COLUMN email TEXT" ) database.execSQL("CREATE INDEX index_users_email ON users(email)" ) } } val MIGRATION_2_3 = object : Migration(2 , 3 ) { override fun migrate (database: SupportSQLiteDatabase ) { database.execSQL(""" CREATE TABLE users_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_name TEXT NOT NULL, age INTEGER NOT NULL, email TEXT, phone TEXT, created_at INTEGER NOT NULL DEFAULT 0 ) """ ) database.execSQL(""" INSERT INTO users_new (id, user_name, age, email) SELECT id, user_name, age, email FROM users """ ) database.execSQL("DROP TABLE users" ) database.execSQL("ALTER TABLE users_new RENAME TO users" ) } } val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build()
3.2 自动迁移(Room 2.4+) @Database( entities = [User::class], version = 2, autoMigrations = [ AutoMigration(from = 1, to = 2) ] ) abstract class AppDatabase : RoomDatabase ()@Database( entities = [User::class], version = 3, autoMigrations = [ AutoMigration( from = 2, to = 3, spec = AppDatabase.MyAutoMigration::class ) ] ) abstract class AppDatabase : RoomDatabase () { @RenameTable(fromTableName = "users" , toTableName = "app_users" ) @DeleteColumn(tableName = "app_users" , columnName = "temp_column" ) class MyAutoMigration : AutoMigrationSpec { @Override override fun onPostMigrate (db: SupportSQLiteDatabase ) { db.execSQL("UPDATE app_users SET email = '' WHERE email IS NULL" ) } } }
3.3 破坏性迁移(开发阶段) val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .fallbackToDestructiveMigration() .build() .fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigrationFrom(1 , 2 , 3 )
3.4 迁移测试 @RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class .java, listOf(), FakeAppComponent::class .java ) @Test fun migrate1To2 () { var db = helper.createDatabase(TEST_DB, 1 ).apply { execSQL("INSERT INTO users (id, user_name, age) VALUES (1, 'Test', 25)" ) close() } db = helper.runMigrationsAndValidate(TEST_DB, 2 , true , MIGRATION_1_2) db.query("SELECT * FROM users" ).use { cursor -> assertThat(cursor.count).isEqualTo(1 ) cursor.moveToFirst() assertThat(cursor.getString(cursor.getColumnIndex("email" ))).isNull() } } }
四、TypeConverter 自定义类型转换 data class Money (val amount: BigDecimal, val currency: String)class Converters { @TypeConverter fun fromMoney (value: Money ) : String { return "${value.amount} :${value.currency} " } @TypeConverter fun toMoney (value: String ) : Money { val parts = value.split(":" ) return Money(BigDecimal(parts[0 ]), parts[1 ]) } @TypeConverter fun fromDate (value: Date ) : Long = value.time @TypeConverter fun toDate (value: Long ) : Date = Date(value) @TypeConverter fun fromStringList (value: List <String >) : String { return value.joinToString("," ) } @TypeConverter fun toStringList (value: String ) : List<String> { return value.split("," ).map { it.trim() } } } @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase ()@Dao @TypeConverters(UserSpecificConverters::class) interface UserDao { ... }
五、Room 与协程/Flow/Paging 的深度集成 5.1 Room + Flow + Paging 3 @Dao interface UserDao { @Query("SELECT * FROM users ORDER BY user_name ASC" ) fun getUsersPagingSource () : PagingSource<Int , User> @Query("SELECT * FROM users WHERE age > :minAge ORDER BY user_name ASC" ) fun getUsersFlow (minAge: Int ) : PagingSource<Int , User> } class UserViewModel (private val userDao: UserDao) : ViewModel() { val users: Flow<PagingData<User>> = Pager( config = PagingConfig( pageSize = 20 , enablePlaceholders = false , prefetchDistance = 5 ), pagingSourceFactory = { userDao.getUsersPagingSource() } ).flow.cachedIn(viewModelScope) }
5.2 Room Flow 的响应式更新机制 Room 返回 Flow<List<T>> 时,底层依赖 InvalidationTracker 实现响应式更新:
SQL 写操作(INSERT/UPDATE/DELETE) │ ▼ SupportSQLiteDatabase 事务提交 │ ▼ InvalidationTracker.onInvalidated(tableNames) │ ▼ InvalidationTracker.syncTriggers() 遍历所有 Observer,查找涉及的 table │ ▼ LiveData/Flow 的 Observer 收到通知 │ ▼ 后台线程重新执行 @Query SQL │ ▼ 新的数据通过 Flow emit 到收集端
InvalidationTracker 的核心机制:
每个 @Query 返回 Flow/LiveData 的方法,Room 通过 SQL 解析确定所涉及的表。
InvalidationTracker 为每个表创建 shadow table 和 trigger,在写操作时自动更新。
表变化时,InvalidationTracker 通知所有关联的 Observer 去重新查询。
5.3 事务处理 @Transaction @Query("SELECT * FROM users WHERE id = :userId" ) suspend fun getUserWithOrders (userId: Int ) : UserWithOrdersclass UserRepository (private val db: AppDatabase) { suspend fun transferUser (userId: Int , newCityId: Int ) { db.runInTransaction { val user = db.userDao().getUserById(userId) ?: throw IllegalStateException("User not found" ) db.userDao().updateUser(user.copy(cityId = newCityId)) db.auditLogDao().insert( AuditLog(action = "CITY_CHANGE" , userId = userId, newValue = "$newCityId " ) ) } } }
六、Room 性能优化 6.1 数据库预填充 val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .createFromAsset("databases/prepopulated.db" ) .build() .createFromFile(File("/path/to/prepopulated.db" )) .addCallback(object : RoomDatabase.Callback() { override fun onCreate (db: SupportSQLiteDatabase ) { super .onCreate(db) } override fun onOpen (db: SupportSQLiteDatabase ) { super .onOpen(db) } })
6.2 WAL 模式 val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) .build()
WAL(Write-Ahead Logging)模式的优势:
读写不互斥:多个读操作和一个写操作可以并发执行
写入更快:不需要在每次写操作时更新原数据库文件
更少的 fsync 调用
6.3 查询优化 @Entity( indices = [ Index(value = ["age" ]), Index(value = ["user_name" , "city_id" ], unique = true) ] ) @Query("SELECT user_name, age FROM users" ) @Query("SELECT * FROM users ORDER BY id ASC LIMIT :limit OFFSET :offset" ) @Query("EXPLAIN QUERY PLAN SELECT * FROM users WHERE user_name = :name" ) suspend fun explainQuery (name: String ) : List<QueryPlan>
七、Room vs 其他持久化方案 Room vs 原始 SQLite :Room 提供编译期 SQL 校验(错误拼写的 SQL 在编译期即报错)、类型安全的查询结果映射、与 LiveData/Flow/Paging 的无缝融合。Room 生成的代码在编译期即确定(非反射),性能与手写 SQLiteOpenHelper 接近,但开发效率显著提升。
Room vs Realm :Realm 基于 C++ 的底层存储引擎(Zero-copy 架构),在无索引扫描性能上优于 Room(Realm 的对象访问是通过 B+ 树结构的直接指针引用,无需构建中间对象)。但 Realm 生成的 APK 体积更大(增加约 4-9MB),对 Kotlin 协程的支持不如 Room 原生。Realm 使用引用计数管理对象生命周期,大事务中容易导致内存问题。
Room vs ObjectBox :ObjectBox 专为对象存储优化,读写速度极快(基于 Uint8 数组的列存储),但生态和社区活跃度不及 Room。ObjectBox 的查询 API 使用的是自定义的 QueryBuilder,与 SQL 差异较大,学习曲线较高。
Room vs SQLDelight :SQLDelight 是 Square 的跨平台 SQL 方案(支持 Android/iOS/JVM/JS)。与 Room 的主要区别是 SQLDelight 从 .sq 文件生成代码(Schema 由 SQL 定义,类型安全),而 Room 从 Kotlin/Java 注解生成。SQLDelight 的跨平台能力对 KMM 项目非常友好。
对于大多数新项目,Room 是平衡性能、易用性和 Google 官方技术栈的最佳选择。
八、Room 错误处理与调试 8.1 常见错误
8.2 调试技巧 val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .setQueryCallback(object : RoomDatabase.QueryCallback { override fun onQuery (sqlQuery: String , bindArgs: List <Any ?>) { Log.d("ROOM_SQL" , "Query: $sqlQuery , Args: $bindArgs " ) } }, Executors.newSingleThreadExecutor()) .build()
面试常考问题 Q1: Room 如何保证 DAO 方法的线程安全?
A: Room 不会自动保证线程安全——调用方需要自行管理。但 Room 提供了一些内置保护:
suspend 函数 :Room 生成的 Impl 会将 suspend DAO 方法自动在后台线程执行(通过 ArchTaskExecutor.executeOnDiskIO() 或自定义的 Executor)。对于非 suspend 的返回 LiveData/Flow 的方法,查询在后台执行,但观察者在主线程回调。
单例 RoomDatabase :RoomDatabase.Builder.build() 建议单例持有。同一个 RoomDatabase 实例内的 DAO 调用受底层 SQLiteDatabase 的同步锁保护——SQLiteOpenHelper.getWritableDatabase() 使用读写锁。
事务保护 :@Transaction 注解的方法在 Room 生成的 Impl 中会自动包裹 beginTransaction() / setTransactionSuccessful() / endTransaction()。
绝对不能在主线程直接调用非 suspend 的数据库操作方法(除非通过 allowMainThreadQueries() 强制允许,但这只建议在测试中使用)。
源码路径:/room/room-ktx/ 和 /room/room-runtime/src/main/java/androidx/room/。在 CoroutinesRoom.kt 中定义了 suspend 函数如何被调度到后台线程。
Q2: @Insert 的 OnConflictStrategy 有哪几种策略?它们的 SQL 对应是什么?
A: Room 的 OnConflictStrategy 是一个枚举,对应 SQLite 的 ON CONFLICT 子句:
REPLACE(OnConflictStrategy.REPLACE): 生成 SQL:INSERT OR REPLACE INTO ... 行为:冲突时先删除旧行再插入新行。注意:这会改变 rowid!如果 rowid 是其他表的外键,设置为 ON DELETE CASCADE 会级联删除关联数据。
IGNORE(OnConflictStrategy.IGNORE): 生成 SQL:INSERT OR IGNORE INTO ... 行为:冲突时静默跳过,保留旧数据。适合幂等插入操作。
ABORT(默认行为,等同于不写 ON CONFLICT): 生成 SQL:INSERT INTO ...(或 INSERT OR ABORT INTO ...) 行为:冲突时回滚当前语句,但事务继续。这是最安全的行为。
FAIL(OnConflictStrategy.FAIL): 生成 SQL:INSERT OR FAIL INTO ... 行为:冲突时回滚当前语句,但不恢复已修改的数据(与 ABORT 的细微差别)。
ROLLBACK(OnConflictStrategy.ROLLBACK): 生成 SQL:INSERT OR ROLLBACK INTO ... 行为:冲突时回滚整个事务。
选择建议:需要更新全部字段用 REPLACE(但要小心外键级联),幂等插入用 IGNORE,严格的数据一致性场景使用默认 ABORT。
Q3: LiveData 与 Room 结合的典型使用场景?
A: Room 的 DAO 可以直接返回 LiveData<List<T>>,当数据库中的相关表发生插入、更新、删除操作时,Room 的 InvalidationTracker 会自动通知所有活跃的 LiveData 观察者重新查询。这构成了 MVVM 架构中的天然数据驱动链路:
数据库变更(INSERT/UPDATE/DELETE) │ ▼ InvalidationTracker 检测到表变化 │ ▼ LiveData observer.onChanged() 被触发 │ ▼ UI 自动更新
这个链路的优势:
全程无需手动刷新——写操作自动触发 UI 更新。
响应式设计——数据是唯一的真相源(single source of truth)。
生命周期安全——LiveData 自动处理订阅/取消,避免内存泄漏。
现代 Room + Flow 的变体:
@Query("SELECT * FROM users" ) fun getUsersFlow () : Flow<List<User>>val users: StateFlow<List<User>> = userDao.getUsersFlow() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000 ), emptyList())
Flow 相比 LiveData 的优势在于支持更多操作符(debounce、combine、distinctUntilChanged),并且不依赖 Android 平台,可用于 KMM 项目。
Q4: Room 的编译期 SQL 校验是如何工作的?
A: Room 的 Annotation Processor(room-compiler)在编译期对 @Query 中的 SQL 进行静态校验。过程如下:
SQL 解析 :Room 使用内部的 SQL 解析器(androidx.room.parser)解析 @Query 中的 SQL 字符串。
表名校验 :校验 SQL 中的表名是否在 @Database(entities = [...]) 中声明。
列名校验 :校验 SELECT 中的列名、WHERE 条件中的列名是否存在。
类型匹配 :校验绑定参数的类型与 Entity 中定义的类型是否匹配。
返回值类型校验 :校验查询结果的列与返回类型是否匹配。
如果 SQL 中有错误,编译期就会报错 :
error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: user_emial)
这个机制极大减少了运行时 SQL 错误。但需要注意,Room 的 SQL 解析器不覆盖所有 SQLite 语法(不是完整的 SQLite parser),对于复杂的 SQL(如窗口函数、CTE),可能会有误报,可以通过 @SkipQueryVerification 跳过校验:
@SkipQueryVerification @Query("WITH RECURSIVE cte AS (...) SELECT * FROM cte" ) suspend fun recursiveQuery () : List<User>
Q5: Room 数据库的线程模式和自定义 Executor 配置?
A: Room 的线程模型分为两层:
查询执行线程 :RoomDatabase 通过 mQueryExecutor 执行后台查询。ViewModel 中通过 viewModelScope 启动的协程自动在 Dispatchers.IO 上运行 Room 的 suspend 函数。
回调通知线程 :返回 LiveData 时,onChanged 回调自动在主线程调用(通过 LiveData.setValue() 必须在主线程的机制)。返回 Flow 时,数据在主线程 emit(但底层查询仍在后台线程执行)。
自定义线程配置:
val db = Room.databaseBuilder(context, AppDatabase::class .java, "app.db" ) .setQueryExecutor(Executors.newFixedThreadPool(4 )) .setTransactionExecutor(Executors.newSingleThreadExecutor()) .build()
setQueryExecutor:用于执行所有 @Query 和返回 LiveData/Flow 的后台查询。
setTransactionExecutor:用于执行 @Transaction 方法。默认使用同一个 executor。
注意事项:
SQLite 是数据库级锁,写操作是串行的。即使使用多线程池,写操作仍然串行执行。
如果需要支持多个数据库文件,可以创建多个 RoomDatabase 实例,每个有独立的连接池。
核心参考 AOSP 路径:
room/room-compiler/src/main/java/androidx/room/ — 注解处理器,代码生成
room/room-runtime/src/main/java/androidx/room/RoomDatabase.java — 数据库核心类
room/room-runtime/src/main/java/androidx/room/InvalidationTracker.java — 表变化监听
room/room-runtime/src/main/java/androidx/room/CoroutinesRoom.kt — 协程支持
room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt — Kotlin 扩展
room/room-migration/src/main/java/androidx/room/migration/ — 迁移测试支持
room/room-paging/src/main/java/androidx/room/paging/ — Paging 3 集成