第3章:Room数据库与Flow集成——DAO返回Flow、响应式查询、表结构设计、迁移策略

3.1 原理:响应式持久层的核心设计

Room 是 Android Jetpack 的持久化库,它在 SQLite 之上提供了编译时验证、自动 DAO 生成、实体关系映射等能力。当 DAO 方法的返回类型声明为 Flow<T> 时,Room 会在底层挂载一个 InvalidationTracker —— 每当所查询的表发生 INSERT、UPDATE、DELETE 时,Room 自动重新执行 SQL 并通过 Flow 发射新结果。这种“表级变化 → SQL 重查 → Flow 推送”的机制,使 UI 层无需手动轮询或手动刷新,即可获得与数据库状态实时同步的数据流。

Room + Flow 响应式查询工作流 Room + Flow 响应式查询工作流 UI / ViewModel DAO.flowAll() InvalidationTracker SQLite 表 重新执行SQL 关键流程: 1. ViewModel 通过 Flow 订阅 DAO 方法 → 2. InvalidationTracker 监听该表的所有变更 3. 表数据变更触发重新执行 SQL → 4. Room 自动将新结果通过 Flow Collector 发出 5. ViewModel 收到新数据,通过 StateFlow 下发给 UI

3.2 表结构设计关键点

实体类使用 @Entity 注解,主键、索引、外键、列默认值等均在编译期处理。设计时应遵循:

  • 使用 @PrimaryKey(autoGenerate = true) 或复合主键 @PrimaryKeys
  • 为频繁查询的列添加 @Index,避免全表扫描
  • 避免嵌套对象;如需嵌套,使用 @Embedded 或 @Relation
  • 字段类型与 SQLite 类型映射:String ↔ TEXT、Long/Int ↔ INTEGER、Double ↔ REAL、Boolean ↔ INTEGER (0/1)

3.3 DAO 返回 Flow 的实现流程

  1. 定义实体:
  2. 声明 DAO 接口:返回类型使用 Flow<List<Entity>>
  3. 构建 Database:使用 @Database 注解,包含实体和版本号
  4. 集成到 Repository:Repository 暴露 Flow,ViewModel 通过 .stateIn() 转为 StateFlow
  5. 迁移管理:通过 Migration 对象处理版本升级

代码示例:完整 DAO + Flow 模式

// 实体类
@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val isCompleted: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)

// DAO 接口
@Dao
interface TaskDao {
    @Query("SELECT * FROM tasks ORDER BY createdAt DESC")
    fun observeAllTasks(): Flow<List<Task>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(task: Task)

    @Update
    suspend fun update(task: Task)

    @Delete
    suspend fun delete(task: Task)
}

// Database
@Database(entities = [Task::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

// Repository
class TaskRepository(private val dao: TaskDao) {
    val allTasks: Flow<List<Task>> = dao.observeAllTasks()
}

// ViewModel
class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
    val tasks: StateFlow<List<Task>> = repository.allTasks
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )
}

3.4 迁移策略

Room 提供了 Migration 类来执行渐进式 schema 变更。迁移策略分为三类:

策略 适用场景 风险 推荐度
渐进式 Migration 从版本 N→N+1 需为每个版本编写迁移 SQL(可能繁琐) ⭐️⭐️⭐️⭐️⭐️
fallbackToDestructiveMigration 测试/开发阶段 会清空用户数据 ⭐️⭐️
AutoMigration (Room 2.4+) 纯添加字段/表,无复杂修改 无法处理列重命名/类型变更 ⭐️⭐️⭐️⭐️

注意: 迁移操作必须在 Room.databaseBuilder() 时配置,且所有迁移必须按版本顺序添加。如果已有用户数据,fallbackToDestructiveMigration 会导致永久丢失,线上版本禁止使用。

渐进式迁移代码示例:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 添加新列
        database.execSQL("ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 0")
        // 创建新索引
        database.execSQL("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 新建表
        database.execSQL("""
            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                name TEXT NOT NULL,
                taskId INTEGER NOT NULL,
                FOREIGN KEY (taskId) REFERENCES tasks(id) ON DELETE CASCADE
            )
        """.trimIndent())
    }
}

// 构建 Database 时添加迁移
val db = Room.databaseBuilder(
    context = appContext,
    klass = AppDatabase::class.java,
    name = "app_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()

3.5 Android Studio 调试与 schema 导出

在 @Database 中设置 exportSchema = true,并在 build.gradle 配置 schema 输出目录:

// app/build.gradle
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments["room.schemaLocation"] = "$projectDir/schemas"
            }
        }
    }
}

// 或使用 KSP
ksp {
    arg("room.schemaLocation", "$projectDir/schemas")
}

这将生成 JSON 格式的 schema 文件。在迁移测试时,可以参照这些 schema 编写正确的 ALTER 语句。同时,Android Studio 的 App Inspection 工具可以实时查看 Room 数据库的表结构和数据变化,配合 Flow 的日志(如 .catch { e -> Log.e(...) })定位查询异常。

3.6 常见错误与性能优化

  • Flow 在后台线程上发射数据 — Room 默认在 Dispatchers.IO 执行查询,如需主线程收集,使用 .flowOn(Dispatchers.Main.immediate)。
  • 查询过于频繁 — 如果单表每秒多次写入,Flow 会被大量重发。解决方案:使用 debounce 合并短时间内多次变更。
  • JOIN 查询引起的不必要刷新 — Room 无法判断哪个表变更;考虑拆分查询或使用 @Transaction 结合 Flow 减少耦合。
  • 主线程上调用挂起函数 — DAO 的 suspend 方法(insert/update/delete)必须在协程中调用,避免使用 runBlocking。
  • stateIn 的 SharingStarted 策略 — 使用 WhileSubscribed(5000) 在用户离开屏幕 5 秒后释放上游,避免后台持续监听。

性能最佳实践: 对于列表型查询,返回 Flow<List<Entity>> 时尽量只查询必要列(避免 SELECT *),并使用 LIMIT + OFFSET 做分页。Room 的 Paging 3 集成也使用了类似的 Flow 机制,适合大数据集。

结合 Paging 3 的分页查询示例:

@Dao
interface TaskDao {
    @Query("SELECT * FROM tasks ORDER BY createdAt DESC")
    fun observeTasksPaged(): PagingSource<Int, Task>

    @Query("SELECT * FROM tasks WHERE id = :taskId")
    fun observeTaskById(taskId: Long): Flow<Task?>
}

// 在 Repository 中通过 Pager 生成 Flow
val pager = Pager(
    config = PagingConfig(pageSize = 20, enablePlaceholders = false),
    pagingSourceFactory = { dao.observeTasksPaged() }
).flow