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