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 实现流程
- 定义Entity:数据模型类,标注
@Entity。 - 定义DAO接口:标注
@Dao,声明CRUD方法。 - 定义Repository类:注入DAO,封装业务逻辑。
- 构建RoomDatabase:在Application或依赖注入容器中创建数据库实例,提供DAO访问。
- 注入Repository到ViewModel:ViewModel通过仓库获取数据,并暴露给UI。
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日志 快速定位问题。
- 性能优化重点:索引、批量事务、合理选择异步查询类型。