目录
  1. 1. 一、Room:SQLite 的现代化封装
    1. 1.1. 1.1 Room 整体架构
    2. 1.2. 1.2 Room 编译期代码生成流程
  2. 2. 二、核心用法与代码示例
    1. 2.1. 2.1 Entity 详细定义
    2. 2.2. 2.2 @Embedded:嵌套对象
    3. 2.3. 2.3 @Relation:关联查询
    4. 2.4. 2.4 DAO 方法类型全面解析
    5. 2.5. 2.5 数据库定义和使用
  3. 3. 三、迁移(Migration)深度讲解
    1. 3.1. 3.1 手动迁移
    2. 3.2. 3.2 自动迁移(Room 2.4+)
    3. 3.3. 3.3 破坏性迁移(开发阶段)
    4. 3.4. 3.4 迁移测试
  4. 4. 四、TypeConverter 自定义类型转换
  5. 5. 五、Room 与协程/Flow/Paging 的深度集成
    1. 5.1. 5.1 Room + Flow + Paging 3
    2. 5.2. 5.2 Room Flow 的响应式更新机制
    3. 5.3. 5.3 事务处理
  6. 6. 六、Room 性能优化
    1. 6.1. 6.1 数据库预填充
    2. 6.2. 6.2 WAL 模式
    3. 6.3. 6.3 查询优化
  7. 7. 七、Room vs 其他持久化方案
  8. 8. 八、Room 错误处理与调试
    1. 8.1. 8.1 常见错误
    2. 8.2. 8.2 调试技巧
  9. 9. 面试常考问题
JetPack全家桶(五)之Room数据持久化库

一、Room:SQLite 的现代化封装

Room 是 Google 在 SQLite 之上构建的 ORM 抽象层。与直接使用 SQLiteOpenHelper 相比,Room 通过编译期注解处理生成实现代码,避免了手写 SQL 字符串的繁琐和运行时反射的性能损耗。Room 的三个核心注解:**@Entity** 定义表结构,**@Dao** 定义数据访问接口,**@Database** 声明数据库。三者在编译期由 Room 的 Annotation Processor(room-compiler)生成对应的 Impl 类,底层依然使用 SupportSQLiteOpenHelperSupportSQLiteDatabase 操作 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 示例(简化版):

// 编译期生成:UserDao_Impl.java
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 {
// 遍历 Cursor 并映射为 User 对象
// ...
} 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 // Address 的字段展开到 users 表
)
// users 表实际列:id, name, street, city, zipCode

如果多个 @Embedded 的字段名冲突,可以用 prefix 区分:

@Entity
data class User(
@PrimaryKey val id: Int,
@Embedded(prefix = "home_") val homeAddress: Address,
@Embedded(prefix = "work_") val workAddress: Address
)
// 实际列:id, home_street, home_city, home_zipCode, work_street, work_city, work_zipCode

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 // 不是 List,Room 知道是一对一
)

// 多对多关系(通过 Junction)
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 // 返回插入的 rowId

// --- 更新 ---
@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>

// 返回 LiveData(自动监听数据库变化)
@Query("SELECT * FROM users ORDER BY user_name ASC")
fun getAllUsersLiveData(): LiveData<List<User>>

// 返回 Flow(Kotlin 协程方式监听数据库变化)
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): Flow<List<User>>

// 返回 PagingSource(配合 Paging 3 使用)
@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>

// 使用 ROW_NUMBER 窗口函数(SQLite 3.25+)
@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>

// RAW 查询(完全手写 SQL 字符串)
@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(), // 可以传入自定义的 FrameworkSQLiteOpenHelperFactory
FakeAppComponent::class.java
)

@Test
fun migrate1To2() {
// 1. 创建版本 1 的数据库并插入测试数据
var db = helper.createDatabase(TEST_DB, 1).apply {
execSQL("INSERT INTO users (id, user_name, age) VALUES (1, 'Test', 25)")
close()
}

// 2. 运行迁移到版本 2
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)

// 3. 验证迁移后数据正确
db.query("SELECT * FROM users").use { cursor ->
assertThat(cursor.count).isEqualTo(1)
cursor.moveToFirst()
// email 列在版本 2 新增,应该有默认值或 null
assertThat(cursor.getString(cursor.getColumnIndex("email"))).isNull()
}
}
}

四、TypeConverter 自定义类型转换

// 自定义类型
data class Money(val amount: BigDecimal, val currency: String)

// TypeConverter:Java/Kotlin 类型 <-> SQLite 支持类型
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() }
}
}

// 在 @Database 中注册
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

// 也可以在单个 Entity 或 DAO 上指定不同的 TypeConverter
@Dao
@TypeConverters(UserSpecificConverters::class)
interface UserDao { ... }

五、Room 与协程/Flow/Paging 的深度集成

5.1 Room + Flow + Paging 3

// DAO 定义
@Dao
interface UserDao {
// 返回 PagingSource(普通分页)
@Query("SELECT * FROM users ORDER BY user_name ASC")
fun getUsersPagingSource(): PagingSource<Int, User>

// 返回 RemoteMediator(离线优先,配合网络层)
@Query("SELECT * FROM users WHERE age > :minAge ORDER BY user_name ASC")
fun getUsersFlow(minAge: Int): PagingSource<Int, User>
}

// ViewModel 中使用
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 的核心机制:

  1. 每个 @Query 返回 Flow/LiveData 的方法,Room 通过 SQL 解析确定所涉及的表。
  2. InvalidationTracker 为每个表创建 shadow table 和 trigger,在写操作时自动更新。
  3. 表变化时,InvalidationTracker 通知所有关联的 Observer 去重新查询。

5.3 事务处理

// 在 DAO 中定义事务方法
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserWithOrders(userId: Int): UserWithOrders

// 在 Repository 中使用
class 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") // 从 assets 复制预填充数据库
.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) // 启用 WAL 模式
.build()

WAL(Write-Ahead Logging)模式的优势:

  • 读写不互斥:多个读操作和一个写操作可以并发执行
  • 写入更快:不需要在每次写操作时更新原数据库文件
  • 更少的 fsync 调用

6.3 查询优化

// 1. 使用索引
@Entity(
indices = [
Index(value = ["age"]),
Index(value = ["user_name", "city_id"], unique = true)
]
)

// 2. 只查询需要的列
@Query("SELECT user_name, age FROM users") // 而非 SELECT *

// 3. 使用 LIMIT 和 OFFSET 进行分页
@Query("SELECT * FROM users ORDER BY id ASC LIMIT :limit OFFSET :offset")

// 4. 使用 EXPLAIN QUERY PLAN 分析查询(开发阶段)
@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 常见错误

// 1. 主线程访问数据库
// 错误:Room 默认不允许主线程访问(除非使用 allowMainThreadQueries())
// 解决:使用 suspend 函数或手动在后台线程调用

// 2. schema 导出版本不匹配
// 设置 exportSchema = true,保存到 app/schemas/

// 3. Migration 缺失
// 确保为所有版本对提供 Migration,或使用 fallbackToDestructiveMigration()

// 4. ForeignKey 约束失败
// 确保引用的父表数据存在,或在插入时使用 OnConflictStrategy

8.2 调试技巧

// 1. 启用 SQL 日志
// 在 build.gradle 或运行时配置
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()

// 2. 导出数据库文件进行调试
// adb exec-out run-as <package> cat /data/data/<package>/databases/app.db > app.db
// 使用 DB Browser for SQLite 或 sqlite3 工具查看

// 3. 使用 App Inspection (Android Studio 4.0+)
// View -> Tool Windows -> Database Inspector
// 可以实时查看/修改数据库内容,执行 SQL 查询

面试常考问题

Q1: Room 如何保证 DAO 方法的线程安全?

A: Room 不会自动保证线程安全——调用方需要自行管理。但 Room 提供了一些内置保护:

  1. suspend 函数:Room 生成的 Impl 会将 suspend DAO 方法自动在后台线程执行(通过 ArchTaskExecutor.executeOnDiskIO() 或自定义的 Executor)。对于非 suspend 的返回 LiveData/Flow 的方法,查询在后台执行,但观察者在主线程回调。

  2. 单例 RoomDatabaseRoomDatabase.Builder.build() 建议单例持有。同一个 RoomDatabase 实例内的 DAO 调用受底层 SQLiteDatabase 的同步锁保护——SQLiteOpenHelper.getWritableDatabase() 使用读写锁。

  3. 事务保护@Transaction 注解的方法在 Room 生成的 Impl 中会自动包裹 beginTransaction() / setTransactionSuccessful() / endTransaction()

  4. 绝对不能在主线程直接调用非 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 子句:

  • REPLACEOnConflictStrategy.REPLACE):
    生成 SQL:INSERT OR REPLACE INTO ...
    行为:冲突时先删除旧行再插入新行。注意:这会改变 rowid!如果 rowid 是其他表的外键,设置为 ON DELETE CASCADE 会级联删除关联数据。

  • IGNOREOnConflictStrategy.IGNORE):
    生成 SQL:INSERT OR IGNORE INTO ...
    行为:冲突时静默跳过,保留旧数据。适合幂等插入操作。

  • ABORT(默认行为,等同于不写 ON CONFLICT):
    生成 SQL:INSERT INTO ...(或 INSERT OR ABORT INTO ...
    行为:冲突时回滚当前语句,但事务继续。这是最安全的行为。

  • FAILOnConflictStrategy.FAIL):
    生成 SQL:INSERT OR FAIL INTO ...
    行为:冲突时回滚当前语句,但不恢复已修改的数据(与 ABORT 的细微差别)。

  • ROLLBACKOnConflictStrategy.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 自动更新

这个链路的优势:

  1. 全程无需手动刷新——写操作自动触发 UI 更新。
  2. 响应式设计——数据是唯一的真相源(single source of truth)。
  3. 生命周期安全——LiveData 自动处理订阅/取消,避免内存泄漏。

现代 Room + Flow 的变体:

// DAO
@Query("SELECT * FROM users")
fun getUsersFlow(): Flow<List<User>>

// ViewModel
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 进行静态校验。过程如下:

  1. SQL 解析:Room 使用内部的 SQL 解析器(androidx.room.parser)解析 @Query 中的 SQL 字符串。
  2. 表名校验:校验 SQL 中的表名是否在 @Database(entities = [...]) 中声明。
  3. 列名校验:校验 SELECT 中的列名、WHERE 条件中的列名是否存在。
  4. 类型匹配:校验绑定参数的类型与 Entity 中定义的类型是否匹配。
  5. 返回值类型校验:校验查询结果的列与返回类型是否匹配。

如果 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 的线程模型分为两层:

  1. 查询执行线程:RoomDatabase 通过 mQueryExecutor 执行后台查询。ViewModel 中通过 viewModelScope 启动的协程自动在 Dispatchers.IO 上运行 Room 的 suspend 函数。

  2. 回调通知线程:返回 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 集成
打赏
  • 微信
  • 支付宝

评论