4. SQLite 与 Room 架构:关系型存储、ORM 映射、编译时校验
4.1 方案边界
SQLite 与 Room 属于关系型本地存储方案,适用于结构化、有强关联、需要 ACID 事务保证的数据场景。此方案的两端边界如下:
- 上游边界:Repository / UseCase 层通过 DAO 接口访问数据,不直接操作 SQLite API。
- 下游边界:数据最终落地为 SQLite 数据库文件(
.db或.sqlite),受 Android 文件系统管理。 - 横向边界:不覆盖 Key-Value 缓存(如 DataStore)、大文件存储(如内部文件系统)、对象序列化全量快照(如 Proto DataStore)。
- 依赖:仅依赖 Android SDK + Room 注解处理器,无需引入三方 ORM 库。
4.2 分层职责
Room 将 SQLite 操作拆解为三层,各层职责不可混淆:
| 层 | 组件 | 职责 |
|---|---|---|
| 数据层 | Entity | 数据表的结构定义,包含列名、类型、主键、外键、索引注解 |
| 访问层 | DAO (Data Access Object) | 声明式 SQL 操作(增删改查),支持 LiveData/Flow 响应式查询 |
| 数据库层 | RoomDatabase | 管理数据库实例、版本迁移、线程池、预填充、回调 |
架构原则: Entity 只映射表结构,DAO 只暴露操作方法,RoomDatabase 只管理生命周期与迁移。业务逻辑不得侵入任何一层。
4.3 数据流
一次完整的数据写入流程如下:
- 调用方(ViewModel / Repository)获取 DAO 实例。
- DAO 方法上标注
@Insert/@Update/@Delete,或@Query写入 SQL。 - Room 在编译时生成 DAO 实现类(如
UserDao_Impl),内部使用SupportSQLiteOpenHelper执行预编译语句。 - 若事务内调用,Room 自动包裹
beginTransaction和setTransactionSuccessful。 - 写入完成后通过
InvalidationTracker通知观察者(LiveData / Flow)刷新。
查询数据流(以 Flow 为例):
// DAO 定义
@Query("SELECT * FROM user WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): Flow<List<User>>
// 编译后生成代码逻辑:
// 1. 创建 SQLiteStatement 并绑定参数
// 2. 执行查询,映射 Cursor 列到 User 对象
// 3. 注册 Invalidator,当 user 表变化时重新发出数据
ORM 映射细节: Room 通过注解处理器在编译时生成
Cursor 到 Entity 的转换代码,@ColumnInfo(name = ...) 解决字段名与列名不一致问题。所有映射均在 XXX_Impl 类中硬编码,避免运行时反射。
4.4 编译时校验
Room 在 APT(Annotation Processing Tool)阶段执行以下校验,将错误提前到编译期:
- SQL 语法校验:
@Query内的 SQL 语句会被解析,若表名、列名或语法错误直接报编译失败。 - 字段映射完整性:Entity 字段必须与查询结果列一一对应,缺少列或类型不匹配会报错。
- 主键 / 外键约束:缺失主键、外键引用表不存在或列类型不匹配均会编译错误。
- 迁移路径验证:若提供了
@Database(version = n)且未提供从版本 1 到 n 的完整Migration,编译警告(可通过fallbackToDestructiveMigration抑制)。 - DAO 方法返回值校验:如
@Insert返回Long但表无自动增长,编译无法通过。
// 编译时发现的典型错误示例
@Query("SELECT * FROM user WHERE name = :name")
fun findByName(name: String): User // 若返回多个结果,编译报错:query returns multiple rows
@Query("SELECT name, age FROM user")
fun getPartial(): User // 编译报错:columns don't match entity fields 'id'
4.5 选型权衡
| 维度 | 原生 SQLite | Room |
|---|---|---|
| 开发效率 | 低:需要手动管理 Cursor、ContentValues、事务 | 高:ORM 映射 + 编译期校验减少运行时 bug |
| 运行时性能 | 略优:无 ORM 转换开销 | 接近原生:映射代码预生成,仅在查询时转换 |
| 迁移成本 | 手动执行 ALTER TABLE | 提供 Migration 类,版本管理统一 |
| 响应式支持 | 需自行实现 ContentObserver | 原生支持 LiveData / Flow / RxJava |
| 复杂查询 | 完全自由 | 手写 SQL 仍保留,无限制 |
| 测试难度 | 依赖真实数据库,Mock 困难 | 提供 Room.inMemoryDatabaseBuilder,单元测试方便 |
陷阱提示: Room 不适用于列数超过 100 的宽表——编译期生成的映射代码会膨胀 APK 大小。此时应考虑使用 View(虚拟表)或降级为原生 SQLite。
4.6 风险点
- 迁移断裂:用户从旧版本直接升级到跨越多个大版本时,若中间迁移链缺失,Room 会触发
fallbackToDestructiveMigration导致数据丢失。 - 联合查询性能:Room 不会自动优化 JOIN 顺序,复杂多表关联可能导致全表扫描。
- TypeConverter 反序列化开销:自定义类型(如 Date、List)通过
@TypeConverter序列化为文本,每次读取都会解析,高频字段应单独建表。 - 协程与线程隔离:Room 要求 DAO 方法标记
suspend或使用@Transaction时确保在主线程外调用,否则抛出IllegalStateException。
4.7 落地步骤
- 添加依赖:在
build.gradle.kts中引入 Room runtime、compiler、KSP(或 kapt)。 - 定义 Entity:为每张数据表创建数据类,标注
@Entity(tableName = "..."),配置主键、索引、外键。 - 创建 DAO:定义为接口,在方法上标注
@Insert/@Query/@Update/@Delete。 - 声明 RoomDatabase:抽象类继承
RoomDatabase,声明abstract fun返回 DAO 实例。 - 编写迁移:为每个版本变动添加
Migration对象,包含ALTER TABLE或CREATE TABLE语句。 - 实例化数据库:使用
Room.databaseBuilder配置数据库名、版本、迁移、回调。 - 在 Repository 中注入 DAO:通过依赖注入(如 Hilt)或手动获取。
- 单元测试:使用
Room.inMemoryDatabaseBuilder创建内存数据库,验证 DAO 操作。
// 典型的 Room 数据库构建示例
@Database(entities = [User::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build().also { INSTANCE = it }
}
}
}
4.8 验收标准
| 验收项 | 通过条件 | 验证方式 |
|---|---|---|
| 编译期校验 | 修改 SQL 表名/列名后立即报编译错误 | 故意写错表名,运行 ./gradlew compileDebugKotlin |
| CRUD 完整性 | 插入 1000 条记录,查询、更新、删除均正确 | Instrumented Test + 断言 |
| 事务原子性 | 并发写入出错时回滚,数据一致 | 模拟崩溃写入,重启后检查数据 |
| 迁移兼容性 | 从旧版本数据库升级后,所有历史数据完整迁移 | 创建 v1.db 预填充数据,运行 v2 应用后读取比对 |
| 响应式更新 | 修改一行数据后,观察的 Flow 发出新值 | 插入后延迟 500ms 断言 Flow 收到更新 |
| ORM 映射正确性 | Entity 字段类型与 SQLite 列类型转换无误 | 存储 Date、枚举、List 等自定义类型后读回 |
验收工具: 使用
Database Inspector(Android Studio Arctic Fox+)直接查看运行时数据库内容,辅助验证迁移与数据完整性。