Room DAO 层编写:任务持久化核心操作
DAO(Data Access Object)是 Room 持久化层与业务逻辑层之间的桥梁,它通过编译时生成的实现类将 Kotlin/Java 方法调用映射为 SQL 语句。本章按课程需求实现五个核心方法:插入任务、按状态查询任务、更新任务状态、删除任务、获取待恢复任务列表。下面先从接口定义出发,再逐一讲解每个方法的原理、注解用法、返回值类型,最后给出完整的 DAO 接口代码和调用示例。
DAO 方法一览
| 方法名 | SQL 语义 | Room 注解 | 返回值类型 | 说明 |
|---|---|---|---|---|
insertTask |
INSERT INTO task | @Insert |
Long(rowId) |
插入一条新任务,返回自动生成的主键 ID |
queryTasksByStatus |
SELECT * FROM task WHERE status = :status | @Query |
List<Task> 或 LiveData<List<Task>> |
按指定状态查询任务列表 |
updateTaskStatus |
UPDATE task SET status = :newStatus WHERE id = :taskId | @Query |
int(影响行数) |
更新单个任务的状态 |
deleteTask |
DELETE FROM task WHERE id = :taskId | @Query |
int(影响行数) |
根据主键删除任务 |
getPendingTasks |
SELECT * FROM task WHERE status IN ('QUEUED','RETRYING') ORDER BY priority DESC | @Query |
List<Task> |
获取需要恢复的待处理任务列表,按优先级降序排列 |
实现流程
- 定义任务实体
TaskEntity(已在第 3 章完成)。 - 创建 DAO 接口
TaskDao,标记@Dao注解。 - 在
AppDatabase的抽象方法中暴露taskDao()。 - 在 Repository 或 ViewModel 中调用 DAO 方法。
完整 DAO 接口代码
@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity): Long
@Query("""
SELECT * FROM task
WHERE status = :status
ORDER BY created_at DESC
""")
fun queryTasksByStatus(status: String): LiveData<List<TaskEntity>>
@Query("UPDATE task SET status = :newStatus WHERE id = :taskId")
suspend fun updateTaskStatus(taskId: Long, newStatus: String): Int
@Query("DELETE FROM task WHERE id = :taskId")
suspend fun deleteTask(taskId: Long): Int
@Query("""
SELECT * FROM task
WHERE status IN ('QUEUED', 'RETRYING')
ORDER BY priority DESC, created_at ASC
""")
suspend fun getPendingTasks(): List<TaskEntity>
}
关键注解与实现原理
- @Insert:Room 自动生成 INSERT 语句。当
onConflict = REPLACE时,如果主键冲突则替换整行(适用于幂等插入)。返回值Long为插入行的 rowId。 - @Query:用于执行自定义 SQL。编译时校验语法,参数通过
:paramName绑定。返回LiveData时,Room 会在数据表变化时自动通知观察者——这是实现任务恢复响应式更新的基础。 - 挂起函数:DAO 方法声明为
suspend,确保在协程作用域内执行,避免阻塞主线程。 - 排序策略:
getPendingTasks按优先级降序、创建时间升序排序,让高优先级任务优先恢复。
提示: 若项目使用 Kotlin Flow,可直接将返回类型改为
Flow<List<TaskEntity>>,Room 同样支持 Flow 的响应式查询。这在 WorkManager 恢复流程中可用于实时监听待处理任务的变化。
调试与常见错误
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
编译错误:Not sure how to convert Cursor to... |
@Query 返回类型与查询列不匹配 | 确认 SELECT 字段数与实体构造函数参数一致;若只返回部分字段,使用 @ColumnInfo 或定义 POJO |
| 插入时主键冲突 | 未指定 onConflict 策略 |
在 @Insert 中设置 onConflict = OnConflictStrategy.REPLACE 或 IGNORE |
| SQL 语法错误 | SQL 中表名/字段名拼写错误或未用反引号包裹保留字 | 检查 TaskEntity 中 @Entity(tableName = "task") 对应表名;给列名加反引号 `status` |
| 挂起函数在非协程中调用 | ViewModel 中直接调用 suspend 方法未启动协程 |
使用 viewModelScope.launch { dao.insertTask(...) } 或 lifecycleScope.launch |
性能与稳定性优化
- 批量插入:如需一次插入多个任务,使用
@Insert接受List<TaskEntity>,Room 会自动生成事务执行批量插入,效率远高于循环单条插入。 - 索引优化:在
status和priority列上创建联合索引,可大幅提升getPendingTasks查询性能:
@Entity(
tableName = "task",
indices = [
Index(value = ["status", "priority"])
]
)
data class TaskEntity(...)
- 事务保证:若需要在一个原子操作中更新状态并插入日志,使用
@Transaction注解标记 DAO 方法,Room 会将其包裹在数据库事务中。 - 避免大对象查询:当待恢复任务列表中包含大量 Blob 字段(如序列化的 WorkSpec)时,考虑使用只返回 ID 和状态的轻量查询,再按需加载完整对象。
DAO 层架构示意
下图展示了 Room DAO 在整体持久化架构中的位置,以及本课程实现的五个方法如何参与任务恢复流程:
实践建议: 在实现
getPendingTasks 时,可以额外添加一个参数 maxCount: Int 限制每次恢复的任务数量,避免一次性加载过多任务导致内存压力。例如 LIMIT :maxCount。
注意: Room 不允许在主线程访问数据库(除非调用
allowMainThreadQueries(),但强烈不推荐生产环境使用)。所有 DAO 调用务必在协程或 ListenableFuture 中执行,否则会抛出 IllegalStateException。