第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)
}
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())
}
}
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": [...]
}
]
}
}
性能优化与调试建议
- 查询性能: 利用
@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)验证迁移后的数据正确性。