第2章 Room数据库搭建:Entity定义、DAO接口设计、数据库升级与编译时检查

在 Jetpack Compose 离线数据绑定架构中,Room 承担唯一的数据持久化层角色。本章聚焦 Room 数据库的完整搭建流程,涵盖 Entity 实体映射、DAO 数据访问对象设计、Schema 版本迁移策略以及最重要的编译时正确性验证机制。理解这些基础构件是后续实现离线数据绑定、响应式 Query 与跨进程安全的前提。

2.1 Entity 定义 —— 数据模型的数据库映射

原理: Room 的 @Entity 注解将 Kotlin 数据类映射为 SQLite 表。每个实体对应一张表,每个属性默认对应一个列。主键必须用 @PrimaryKey 标识,自增主键需设置 autoGenerate = true。关联关系通过外键(@ForeignKey)或嵌套对象(@Embedded)表达。

注解/属性 用途 常见错误
@Entity(tableName = "...") 指定表名,默认使用类名 表名包含 SQL 保留字
@PrimaryKey 标记主键字段 未设置主键导致编译失败
@ColumnInfo(name = "...") 自定义列名 列名与 Kotlin 属性不一致导致查询错误
@Embedded 嵌套对象展开存储为多列 嵌套对象中的字段名冲突
@Ignore 忽略该字段,不存入数据库 忽略后仍试图在 DAO 中使用
✅ 最佳实践: 所有实体类应声明默认构造函数(空或带默认值参数),且字段建议使用不可变类型(val)。启用 @ColumnInfo(index = true) 为频繁查询的列建立索引,提升离线绑定场景的查询性能。
// 完整的 Entity 示例
@Entity(
    tableName = "tasks",
    indices = [Index(value = ["due_date"]), Index(value = ["status"])],
    foreignKeys = [
        ForeignKey(
            entity = UserEntity::class,
            parentColumns = ["id"],
            childColumns = ["assignee_id"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class TaskEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    @ColumnInfo(name = "title") val title: String,
    val description: String,
    @ColumnInfo(name = "due_date") val dueDate: Long,
    @ColumnInfo(name = "status") val status: String,
    val assigneeId: Long? = null,
    @Ignore val localSynced: Boolean = false
)

2.2 DAO 接口设计 —— 查询、插入、更新与删除

原理: DAO(Data Access Object)使用接口或抽象类定义,Room 在编译时生成具体实现。核心注解包括 @Query、@Insert、@Update、@Delete。支持返回 Flow 或 LiveData 实现响应式数据绑定——这是离线数据绑定的关键入口。

// 典型的 DAO 接口设计
@Dao
interface TaskDao {
    // 响应式查询:返回 Flow> 供 Compose 收集
    @Query("SELECT * FROM tasks ORDER BY due_date ASC")
    fun getAllTasksFlow(): Flow>

    @Query("SELECT * FROM tasks WHERE id = :taskId")
    suspend fun getTaskById(taskId: Long): TaskEntity

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: TaskEntity): Long

    @Update
    suspend fun updateTask(task: TaskEntity)

    @Delete
    suspend fun deleteTask(task: TaskEntity)

    // 批量操作
    @Insert
    suspend fun insertAll(tasks: List)
}
💡 响应式绑定原理: 当 DAO 方法返回 Flow<T> 时,Room 内部维护一个 InvalidationTracker,在任意增删改操作后自动重新查询。Compose 的 collectAsState() 直接订阅该 Flow,实现 UI 随数据库变化自动更新,无需手动刷新。

DAO 设计原则:

  • 优先使用 suspend 函数以避免阻塞主线程;Room 会自动将数据库操作放入后台线程。
  • 对于复杂 JOIN 查询,使用 @Transaction 保证多表操作的一致性。
  • 返回 Flow 的查询应避免在 DAO 层执行耗时转换,保持数据层职责单一。

2.3 数据库升级 —— 版本管理与迁移策略

原理: 每次修改 Entity 结构(添加/删除/修改列或表)都需要递增数据库版本。Room 提供三种升级方案:Migration 类(显式 SQL)、fallbackToDestructiveMigration()(破坏式重建)、AutoMigration(Room 2.4+ 自动生成)。

方案 适用场景 数据保留 编译时检查
手动 Migration 复杂变更(重命名、关联约束) 是 手动验证
AutoMigration (spec) 新增列/表、删除列(支持自动) 是 编译时自动生成
fallbackToDestructiveMigration 开发阶段、本地测试 否(清空数据) 无
// 手动迁移示例:从版本1升级到版本2(新增 priority 列)
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")
    }
}

// 自动迁移(Room 2.4+),需要写 spec 类
@AutoMigration(from = 2, to = 3)
class AppAutoMigration : AutoMigrationSpec {
    // 可重写 onPostMigrate 处理默认值等逻辑
}

// 在 Database 声明中注册迁移
@Database(
    entities = [TaskEntity::class, UserEntity::class],
    version = 3,
    exportSchema = true // 开启 schema 导出以支持自动迁移
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao

    companion object {
        val MIGRATIONS = arrayOf(MIGRATION_1_2, AppAutoMigration())
    }
}
⚠️ 常见错误: 开发阶段频繁修改 Entity 但忘记更新 version 或注册 Migration,导致 IllegalStateException: Room cannot verify the data integrity。调试期间可临时使用 fallbackToDestructiveMigration(),但上线前必须实现正确的迁移路径。

2.4 编译时检查 —— 生成代码与错误预防

原理: Room 是编译时 ORM,通过 kapt(Kotlin Annotation Processing Tool)或 ksp(Kotlin Symbol Processing)在编译期扫描 @Entity、@Dao、@Database 注解,自动生成 TaskDao_Impl、AppDatabase_Impl 等实现类。同时检查 SQL 语法、表名/列名一致性、主键完整性、DAO 返回类型与 Query 的匹配性。

编译时检查涵盖以下维度:

  • SQL 语法验证: @Query 中的 SQL 语句在编译时被解析,错误拼写或表名不存在将直接报错。
  • 类型匹配: DAO 方法返回值与 Query 结果列类型必须兼容(例如 Long 列不能赋值给 String)。
  • 主键约束: 每个 Entity 必须至少有一个 @PrimaryKey;复合主键需用 @PrimaryKeys。
  • 迁移完整性: 如果 exportSchema = true,Room 会对比历史 Schema JSON 文件,自动迁移仅在变更符合自动规范时才通过编译。
  • DAO 函数签名: 非 suspend 函数不能使用 @Insert 返回 Long 等阻塞操作(Room 会建议改为 suspend)。
// 开启 schema 导出后生成的 JSON 片段(位于 app/schemas/ 目录)
{
  "formatVersion": 1,
  "database": {
    "version": 3,
    "identityHash": "abc123...",
    "entities": [
      {
        "tableName": "tasks",
        "createSql": "CREATE TABLE IF NOT EXISTS `tasks` ...",
        "fields": [...]
      }
    ]
  }
}
Room 数据库编译时检查与升级流程 @Entity 定义 @Dao 接口 @Database 抽象类 kapt / ksp 处理器 (RoomAnnotationProcessor) *_Impl 实现类 Schema JSON 文件 AutoMigration 实现 编译时检查 SQL语法 / 类型匹配 / 主键 / 迁移 💡 编译时检查是离线数据绑定的安 任何 Entity/DAO 变更若导致生成代码失败,编译立即中断,防止运行时崩溃。 开启 exportSchema = true 后,Room 可自动校验迁移脚本与历史 Schema 的一致性。

性能优化与调试建议

  • 查询性能: 利用 @Index 覆盖常用过滤列(如 status、due_date)。避免在 @Query 中使用 LIKE '%keyword%',无法走索引。
  • Database 单例: 使用 Room.databaseBuilder 创建单一实例,避免多实例竞争。推荐搭配 Dagger/Hilt 或手动单例模式。
  • 调试技巧: 开启 Room 日志 RoomDatabase.Builder<?>.enableMultiInstanceInvalidation() 并配合 setQueryCallback 观察 SQL 执行详情。
  • 编译时代价: kapt 会增加编译时间,建议新项目迁移至 ksp(Room 2.6+ 支持),ksp 处理速度比 kapt 快 2~3 倍。
📘 升级检查清单:
1. 修改 Entity 后,递增 @Database(version = ...)。
2. 若 exportSchema = true,提交旧的 schema JSON 文件到 VCS。
3. 实现对应的 Migration 类或 AutoMigrationSpec,并注册到 Room.databaseBuilder。
4. 运行单元测试(如 MigrationTestHelper)验证迁移后的数据正确性。