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 数据流

一次完整的数据写入流程如下:

  1. 调用方(ViewModel / Repository)获取 DAO 实例。
  2. DAO 方法上标注 @Insert / @Update / @Delete,或 @Query 写入 SQL。
  3. Room 在编译时生成 DAO 实现类(如 UserDao_Impl),内部使用 SupportSQLiteOpenHelper 执行预编译语句。
  4. 若事务内调用,Room 自动包裹 beginTransaction 和 setTransactionSuccessful。
  5. 写入完成后通过 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 落地步骤

  1. 添加依赖:在 build.gradle.kts 中引入 Room runtime、compiler、KSP(或 kapt)。
  2. 定义 Entity:为每张数据表创建数据类,标注 @Entity(tableName = "..."),配置主键、索引、外键。
  3. 创建 DAO:定义为接口,在方法上标注 @Insert / @Query / @Update / @Delete。
  4. 声明 RoomDatabase:抽象类继承 RoomDatabase,声明 abstract fun 返回 DAO 实例。
  5. 编写迁移:为每个版本变动添加 Migration 对象,包含 ALTER TABLE 或 CREATE TABLE 语句。
  6. 实例化数据库:使用 Room.databaseBuilder 配置数据库名、版本、迁移、回调。
  7. 在 Repository 中注入 DAO:通过依赖注入(如 Hilt)或手动获取。
  8. 单元测试:使用 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+)直接查看运行时数据库内容,辅助验证迁移与数据完整性。
Room 与 SQLite 架构分层关系图 Room 与 SQLite 架构分层关系 ViewModel / Repository DAO (Data Access Object) @Insert / @Query / @Transaction RoomDatabase 编译时校验 SQL、映射、迁移路径 预生成 DAO_Impl + InvalidationTracker SQLite 引擎 ACID 事务 / B-Tree 索引 / WAL 模式 app.db (SQLite 数据文件) 自动观察机制 InvalidationTracker → 通知 Flow / LiveData 编译时校验 SQL 语法 / 列映射 / 迁移 错误 → 编译失败