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 — 实现数据库变更自动通知观察者,避免手动轮询。
Room DAO 层 CRUD 与响应式数据流结构 Room DAO 层核心结构 DAO 接口 @Insert — 插入 @Query — 查询 @Update — 更新 @Delete — 删除 @Transaction LiveData / Flow Room 编译器 自动生成实现类 SQL 语句预编译 调用 Database 实例 @Transaction 包裹 数据变更通知机制 数据消费 LiveData (UI 线程) Flow (协程) PagingSource (分页) 触发自动刷新 说明:DAO 接口中声明 CRUD 方法,Room 编译器在编译期生成实现。 @Transaction 保证多个写操作原子执行;LiveData/Flow 让 UI 层自动响应数据变化。

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 以便与协程共存。