4. Room DAO 层实现:插入、查询、更新、删除操作、@Transaction 保证原子性、LiveData/Flow 支持
4.1 原理概述
DAO(Data Access Object)是 Room 持久化库的核心接口,它将 SQL 操作映射为类型安全的 Kotlin/Java 方法。DAO 层隔离了数据库细节,提供面向对象的 CRUD 操作,并天然支持响应式数据流(LiveData / Flow)。
本章围绕以下能力展开:
- 插入(Insert) — 自动生成 INSERT 语句,支持单条/批量插入与冲突策略。
- 查询(Query) — 通过原生 SQL 或@Query 注解,返回任意类型(List、LiveData、Flow)。
- 更新(Update) — 按主键更新实体,支持批量更新。
- 删除(Delete) — 按主键删除或自定义 WHERE 条件。
- @Transaction — 确保多个 DAO 操作在同一数据库事务中原子执行。
- LiveData / Flow — 实现数据库变更自动通知观察者,避免手动轮询。
4.2 插入操作 (@Insert)
DAO 中使用 @Insert 注解可将实体对象插入数据库。支持三种冲突策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| OnConflictStrategy.REPLACE | 冲突时替换整行 | 需要幂等插入 |
| OnConflictStrategy.IGNORE | 冲突时跳过该行 | 批量插入,忽略重复 |
| OnConflictStrategy.ABORT | 冲突时抛出异常 | 严格数据校验 |
提示:@Insert 方法可以返回 long(单条插入的行 ID)或 Long[](批量插入的行 ID 数组)。
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTask(task: TaskEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertTasks(tasks: List<TaskEntity>): List<Long>
4.3 查询操作 (@Query)
@Query 注解允许你编写任意 SQL 语句,Room 会在编译期校验语法和参数。支持参数绑定(:paramName 或 ?)和多种返回类型。
- List 或数组 — 同步返回,需在后台线程调用。
- LiveData — 观察数据变化,自动在主线程通知。
- Flow — 响应式流,配合协程使用。
@Query("SELECT * FROM tasks WHERE priority >= :minPriority ORDER BY created_at DESC")
fun getTasksByPriority(minPriority: Int): List<TaskEntity>
@Query("SELECT * FROM tasks WHERE status = :status")
fun getTasksLiveData(status: String): LiveData<List<TaskEntity>>
@Query("SELECT * FROM tasks WHERE assignee = :assignee")
fun getTasksFlow(assignee: String): Flow<List<TaskEntity>>
4.4 更新操作 (@Update)
@Update 按实体主键匹配更新,仅更新非 null 或非默认值的字段(取决于你的配置)。支持单个或批量。更新失败返回 0。
@Update
fun updateTask(task: TaskEntity): Int // 返回受影响行数
@Update
fun updateTasks(tasks: List<TaskEntity>)
4.5 删除操作 (@Delete)
@Delete 按主键删除实体。若需要自定义 WHERE 条件,使用 @Query 编写 DELETE 语句。
@Delete
fun deleteTask(task: TaskEntity): Int
@Query("DELETE FROM tasks WHERE is_expired = 1")
fun deleteExpiredTasks(): Int
4.6 @Transaction 保证原子性
当多个 DAO 操作需要作为一个原子单元执行时(例如:先删除后插入,或同时更新两张表),使用 @Transaction 注解。Room 会在同一个数据库事务中执行该方法,任一操作失败则全部回滚。
警告:@Transaction 方法不能是 suspend 函数或挂起内部协程,因为 Room 的事务管理基于 Jdbc 连接,不能跨线程。如果需要协程,请使用 withTransaction 挂起函数(需 Room 2.3+)。
@Transaction
fun replaceTasksForCategory(categoryId: Long, newTasks: List<TaskEntity>) {
deleteOldTasks(categoryId)
insertTasks(newTasks)
}
// 或者使用 suspend 版本(Room 2.3+)
@Transaction
suspend fun replaceTasksForCategorySuspend(categoryId: Long, newTasks: List<TaskEntity>) {
deleteOldTasks(categoryId)
insertTasks(newTasks)
}
4.7 LiveData / Flow 支持
DAO 方法返回 LiveData 或 Flow 时,Room 会监听数据库变化,在表数据变更时自动重新查询并发射新值。这使 UI 层可以简单地订阅而无需手动刷新。
| 返回类型 | 生命周期感知 | 线程 | 适用场景 |
|---|---|---|---|
| LiveData | 是(自动取消) | 主线程观察 | 传统 UI(Activity/Fragment) |
| Flow | 否(需手动 collect) | 协程调度 | Jetpack Compose / 响应式管道 |
// ViewModel 中观察 Flow
class TaskViewModel(private val dao: TaskDao) : ViewModel() {
val allTasks: Flow<List<TaskEntity>> = dao.getAllTasksFlow()
init {
viewModelScope.launch {
allTasks.collect { tasks ->
// 每次数据变化更新 UI
}
}
}
}
4.8 调试与常见错误
- @Transaction 未加导致事务隔离 — 同时写入多表时可能部分成功,部分失败,数据不一致。
- LiveData 在非 UI 线程通知 — Room 保证 LiveData 在主线程分发,但如果你自己构建 LiveData,需调用 postValue。
- 主键冲突未处理 — 未设置冲突策略时默认 ABORT,插入重复主键会崩溃。应明确指定 REPLACE 或 IGNORE。
- Flow 冷流特性 — 需确保有订阅者(collect)时查询才执行。使用 stateIn 或 shareIn 可在多个订阅者间共享。
- 编译期 SQL 错误 — 如果 @Query 中的 SQL 语法错误,Room 会在编译时报错,而非运行时。
4.9 性能与稳定性优化
- 批量操作 — 使用 List 参数一次性插入/更新/删除,减少事务次数。
- 索引 — 为经常查询的列(如 status、priority)建立索引,加速@Query 性能。
- 流式查询 — 大量数据返回时考虑 PagingSource,避免一次性加载到内存。
- @Transaction 内部避免耗时 — 事务内不执行网络请求、文件 IO 等长操作,防止锁住数据库。
- LiveData vs Flow — 在 Compose 项目中优先使用 Flow + collectAsState,减少 LiveData 的模板代码。
最佳实践:将所有写操作封装在 Repository 中,并用 @Transaction 包裹复合写操作;查询优先返回 Flow 以便与协程共存。