### 第4章:Room DAO与Repository模式:增删改查方法、@Transaction、同步与异步查询、LiveData/Flow返回 ---

WorkManager与Room实现离线同步教程 - 第4节

4.1 原理概述

在Android离线同步架构中,DAO(Data Access Object)与Repository承担着数据访问与缓存管理的核心职责。DAO直接与Room数据库交互,提供细粒度的增删改查方法;Repository则作为单一数据源入口,封装DAO与可能的网络数据源,向ViewModel暴露干净的数据接口。

4.1.1 DAO职责

  • 定义SQL操作(CRUD),通过注解 @Insert、@Update、@Delete、@Query 映射到具体方法。
  • 支持同步挂起函数(suspend)与异步响应式返回类型(LiveData、Flow)。
  • 通过 @Transaction 保证多个数据库操作在同一个事务中原子执行。

4.1.2 Repository设计模式

  • 将DAO与网络数据源(如Retrofit)组合,对外暴露统一的数据存取接口。
  • ViewModel只需引用Repository,无需关心底层数据来源是本地还是远程。
  • 在离线同步场景中,Repository负责判断本地缓存是否有效,并在需要时触发WorkManager同步任务。

4.1.3 同步查询 vs 异步查询

特性 同步查询 异步查询(LiveData / Flow)
执行线程 必须在后台线程(挂起函数)或自行管理线程 自动在后台线程执行,结果在主线程返回
返回值 直接返回数据实体或列表 LiveData<T> 或 Flow<T>,可被生命周期感知
数据更新通知 仅触发一次,不会自动更新 数据库变化时自动发出新数据
适用场景 一次性操作(插入、更新、删除)或非UI层调用 UI层实时展示数据列表或详情

4.1.4 @Transaction 注解

当DAO方法包含多个数据库操作(例如先插入一条记录,再更新另一条关联记录)时,使用 @Transaction 确保这些操作在同一数据库事务中执行,要么全部成功,要么全部回滚。Room会将带有 @Transaction 的方法自动包裹在事务中。

警告:只在DAO方法上标注 @Transaction。如果在Repository层手动开启事务,请确保通过 roomDatabase.runInTransaction() 进行,不要嵌套事务。
---

4.2 实现流程

  1. 定义Entity:数据模型类,标注 @Entity。
  2. 定义DAO接口:标注 @Dao,声明CRUD方法。
  3. 定义Repository类:注入DAO,封装业务逻辑。
  4. 构建RoomDatabase:在Application或依赖注入容器中创建数据库实例,提供DAO访问。
  5. 注入Repository到ViewModel:ViewModel通过仓库获取数据,并暴露给UI。
Room DAO ↔ Repository 架构与同步/异步路径 App / ViewModel Repository DAO接口 Room Database SQLite 文件 请求数据 同步: IO 线程 异步: LiveData / Flow @Transaction 保证原子 异步回调(可忽略)
---

4.3 代码示例:NoteEntity / NoteDao / NoteRepository

4.3.1 定义Entity

@Entity(tableName = "notes")
data class NoteEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val content: String,
    val createdAt: Long = System.currentTimeMillis()
)

4.3.2 定义DAO(含@Transaction示例)

@Dao
interface NoteDao {

    // ----- 同步查询(挂起函数) -----
    @Query("SELECT * FROM notes WHERE id = :noteId")
    suspend fun getNoteById(noteId: Long): NoteEntity?

    // ----- 异步查询(LiveData) -----
    @Query("SELECT * FROM notes ORDER BY createdAt DESC")
    fun getAllNotesLiveData(): LiveData<List<NoteEntity>>

    // ----- 异步查询(Flow) -----
    @Query("SELECT * FROM notes WHERE title LIKE :query")
    fun searchNotesFlow(query: String): Flow<List<NoteEntity>>

    // ----- 插入 -----
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(note: NoteEntity): Long

    // ----- 更新 -----
    @Update
    suspend fun updateNote(note: NoteEntity)

    // ----- 删除 -----
    @Delete
    suspend fun deleteNote(note: NoteEntity)

    // ----- @Transaction示例:批量插入并更新状态 -----
    @Transaction
    suspend fun insertNotesBatch(notes: List<NoteEntity>) {
        for (note in notes) {
            insertNote(note)
        }
        // 模拟更新操作:例如更新批次标志
        @Query("UPDATE notes SET batchFlag = 1 WHERE createdAt > :time")
        suspend fun markBatchFlag(time: Long)
        markBatchFlag(notes.first().createdAt)
    }
}

4.3.3 定义Repository

class NoteRepository(private val noteDao: NoteDao) {

    // 对外暴露异步数据流
    val allNotesLiveData: LiveData<List<NoteEntity>> = noteDao.getAllNotesLiveData()

    // 同步插入(挂起函数)
    suspend fun insertNote(note: NoteEntity) = noteDao.insertNote(note)

    // 批量事务操作
    suspend fun insertNotesWithTransaction(notes: List<NoteEntity>) =
        noteDao.insertNotesBatch(notes)

    // 同步查询用于后台WorkManager
    suspend fun getNoteByIdSync(id: Long) = noteDao.getNoteById(id)

    // 搜索Flow
    fun searchNotes(query: String) = noteDao.searchNotesFlow(query)
}

4.3.4 在ViewModel中使用

class NoteViewModel(private val repository: NoteRepository) : ViewModel() {

    val notes: LiveData<List<NoteEntity>> = repository.allNotesLiveData

    fun addNote(title: String, content: String) {
        viewModelScope.launch {
            repository.insertNote(
                NoteEntity(title = title, content = content)
            )
        }
    }

    fun syncNotesFromServer() {
        viewModelScope.launch(Dispatchers.IO) {
            // 假设从网络获取列表
            val remoteNotes = fetchRemoteNotes()
            repository.insertNotesWithTransaction(remoteNotes)
        }
    }
}
---

4.4 调试与验证

4.4.1 开启Room SQL日志

在Application的onCreate中添加:

if (BuildConfig.DEBUG) {
    Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db")
        .openHelperFactory(SupportFactory()) // 可选:支持查询计划
        .fallbackToDestructiveMigration()      // 开发时谨慎使用
        .build()
        .apply {
            // 注意:Room 2.4+ 使用 setQueryCallback
            this.setQueryCallback({ sql, bindArgs ->
                Log.d("RoomSQL", "SQL: $sql, args: $bindArgs")
            }, Executors.newSingleThreadExecutor())
        }
}

4.4.2 使用Database Inspector

  • 在Android Studio中打开 View → Tool Windows → App Inspection。
  • 选择 Database Inspector,运行App后自动显示已连接的数据库。
  • 可浏览表结构、执行自定义SQL查询、观察LiveData/Fow更新。

4.4.3 测试@Transaction是否生效

在DAO事务中故意抛出异常,验证所有操作均被回滚:

@Transaction
suspend fun testTransactionRollback(notes: List<NoteEntity>) {
    notes.forEach { insertNote(it) }
    // 手动抛异常模拟失败
    require(notes.size < 100) { "Limit exceeded" }
}

观察数据库中未插入任何数据即表示事务回滚成功。

---

4.5 常见错误与避坑

错误 原因 解决方案
Cannot access database on the main thread 直接在主线程调用同步查询(非挂起函数) 将查询方法改为 suspend 或使用 LiveData/Flow
LiveData不更新UI 在非主线程调用 postValue 或在非生命周期观察 确保 LiveData 由Room自动管理,不要在Repository手动调用 postValue
@Transaction 方法内调用其他非事务DAO方法 部分操作在事务外执行,破坏原子性 将所有相关操作集中到一个方法中,并只在该方法上加 @Transaction
Database schema 变更导致崩溃 未实现Migration或使用了 fallbackToDestructiveMigration 编写增量迁移脚本,生产版禁用自动清除
Flow 在Activity重建后不重置 未使用 stateIn 或 repeatOnLifecycle 收集 使用 viewModelScope 或 lifecycle.repeatOnLifecycle 收集Flow
提示:对于需要在UI层实时更新的数据,优先使用 Flow 而非 LiveData。Flow支持背压策略,且与Jetpack Compose集成更自然。但若项目仍大量使用 LiveData 则可保持统一。
---

4.6 性能与稳定性优化

4.6.1 使用索引优化查询

在需要频繁查询的字段上添加索引,例如按创建时间排序:

@Entity(tableName = "notes",
    indices = [Index(value = ["createdAt"], orders = [Index.Order.DESC])])
data class NoteEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    ...
)

4.6.2 批量操作务必包裹事务

避免在循环中逐条插入/更新,使用 @Transaction 一次性提交:

@Transaction
suspend fun bulkInsert(notes: List<NoteEntity>) {
    // 内部insertNote会被自动合并到同一个事务
    notes.forEach { insertNote(it) }
}

4.6.3 避免在主线程观察LiveData

始终在ViewModel中通过 viewModelScope 收集Flow,或使用 Transformations.map 处理LiveData数据,确保所有数据库操作在后台线程执行。

4.6.4 应用Fragment生命周期合理释放

使用 viewLifecycleOwner 观察LiveData,避免在Fragment onDestroy后仍持有观察者导致内存泄漏。

4.6.5 数据库版本迁移策略

不要在Release后使用 fallbackToDestructiveMigration()。应编写 Migration 类:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE notes ADD COLUMN syncStatus INTEGER NOT NULL DEFAULT 0")
    }
}
---

4.7 章节总结

  • DAO 是数据库操作的唯一入口,支持挂起函数(同步)和 LiveData/Flow(异步)。
  • Repository 封装DAO,对外提供统一数据源,适合与WorkManager离线同步结合。
  • @Transaction 确保多步操作原子性,必须标注在DAO方法上。
  • 调试时使用 Database Inspector 和 SQL日志 快速定位问题。
  • 性能优化重点:索引、批量事务、合理选择异步查询类型。