Room数据库现状盘点:整理现有实体、DAO、数据库版本及迁移历史

迁移的第一步不是写代码,而是对当前Room数据库做完整审计。本章从实体、DAO、版本号和迁移历史四个维度,建立一份可执行的现状清单,为后续迁移提供精确的输入数据。

2.1 现有实体(Entity)清单

以典型笔记类应用为例,现有Room数据库包含三个实体:User、Note、Tag,并存在一对多与多对多关联。

实体 表名 主键 关键字段 关系
User users userId: Long name, email, createdAt 一对多:每个User有多个Note
Note notes noteId: Long title, content, userId, updatedAt 多对一:Note指向User;多对多:通过NoteTagCrossRef关联Tag
Tag tags tagId: Long name, color 多对多:通过NoteTagCrossRef关联Note
NoteTagCrossRef note_tag_cross_ref 复合主键(noteId, tagId) noteId, tagId 关联表,无独立实体
审计要点:仔细检查每个实体中是否包含 @Embedded、@Relation 或 @ColumnInfo 注解,这些会影响DataStore中数据模型的扁平化设计。

2.2 DAO接口分析

当前DAO层定义了核心数据操作模式。所有操作均通过以下三个DAO暴露:

  • UserDao – 用户查询、注册、更新个人资料、统计笔记数量。
  • NoteDao – 笔记的CRUD、按用户分页查询、全文搜索(基于 LIKE)。
  • TagDao – 标签管理、按笔记ID查询标签、批量添加/删除关联。

关键DAO方法签名示例(NoteDao):

@Dao
interface NoteDao {
    @Query("SELECT * FROM notes WHERE userId = :userId ORDER BY updatedAt DESC")
    fun getNotesByUser(userId: Long): Flow<List<Note>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(note: Note): Long

    @Update
    suspend fun updateNote(note: Note)

    @Delete
    suspend fun deleteNote(note: Note)

    @Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%'")
    fun searchNotes(query: String): Flow<List<Note>>
}
迁移关注点:DAO中所有返回 Flow<List<T>> 的方法,在DataStore中需改为 Flow<List<DataStoreModel>>;关系查询(如 @Transaction + @Relation)必须拆解为多次读取或预计算缓存。

2.3 数据库版本号与迁移历史

当前数据库版本为 5,Room.databaseBuilder 的版本声明如下:

@Database(entities = [User::class, Note::class, Tag::class, NoteTagCrossRef::class],
          version = 5,
          exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun noteDao(): NoteDao
    abstract fun tagDao(): TagDao
}

迁移历史共经历4次版本升级(1→5):

版本 变更内容 迁移类型 风险等级
1→2 添加 Note.createdAt 字段(非空默认值) ALTER TABLE ADD COLUMN 低
2→3 添加 Tag 表和 note_tag_cross_ref 表 CREATE TABLE + ALTER TABLE 中(新表创建无风险)
3→4 User.email 类型从 TEXT 改为 VARCHAR(128) 手动迁移:CREATE新表 + INSERT + DROP旧表 高(数据复制,需处理异常)
4→5 Note.content 添加全文索引(FTS4)支持 使用 FTS4 虚拟表 + 触发器 高(虚拟表与原始表同步)

对应的 Migration 对象示例(版本4→5):

val MIGRATION_4_5 = object : Migration(4, 5) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "CREATE VIRTUAL TABLE notes_fts USING fts4(content="notes", title, content)"
        )
        database.execSQL(
            "INSERT INTO notes_fts(docid, title, content) " +
            "SELECT noteId, title, content FROM notes"
        )
        // 创建触发器保持同步
        database.execSQL(
            "CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes " +
            "BEGIN INSERT INTO notes_fts(docid, title, content) " +
            "VALUES (new.noteId, new.title, new.content); END"
        )
        // ... 省略 UPDATE / DELETE 触发器
    }
}
重要风险:版本3→4的“改字段类型”迁移使用“重命名表”方式,若用户在迁移过程中应用被杀死,可能导致数据丢失。迁移到DataStore时,此类历史迁移的副作用将不复存在(DataStore不依赖SQLite Schema),但仍需确保现有SQLite数据被正确导出为Proto/Preferences。

2.4 数据库现状结构图

Room数据库现状全景(版本5) User users userId (PK) name, email createdAt Note notes noteId (PK) title, content userId (FK)→User Tag tags tagId (PK) name, color 1:N(userId) NM:关联表↗ NoteTagCrossRef noteId (FK) tagId (FK) DAO 接口层 UserDao | NoteDao | TagDao 版本迁移历史 v1→v2→v3→v4→v5 当前需迁移数据量:约12万条笔记 / 8千用户 / 2千标签 迁移前必须导出Schema JSON(版本5)以备校验

2.5 迁移准备清单

基于以上盘点,整理出迁移前必须完成的准备项:

  1. 导出当前Schema – 在 build.gradle 中开启 exportSchema = true,获取 AppDatabase_Impl.java 中的SQL创建语句,作为旧数据结构的唯一权威参照。
  2. 备份生产数据库 – 对现有SQLite文件(app_database.db)做完整快照,存储在安全目录或云端。
  3. 编写数据读取器 – 在DataStore迁移前,先用Room的 @Query 将所有实体数据全量读入内存(分批读取防OOM),并转为扁平化的DataStore模型。
  4. 确认迁移策略 – 如果选择 fallbackToDestructiveMigration(),必须确保用户数据已有备份;建议使用自定义 Migration 对版本5→6(虚拟过渡版本)做数据导出。
  5. 标记关联字段 – 对于 NoteTagCrossRef 这类关联表,决定在DataStore中是拆成独立列表还是嵌入到Note对象的 tagIds: List<Long> 字段中。推荐后者以减少多数据源同步问题。
实战技巧:创建一个 RoomDataSnapshot 数据类,包含所有实体集合,作为迁移中间状态。这样就能在测试中对比迁移前后数据是否完全一致。