数据库(Database)类:创建、版本迁移、回调
说实话,Room 的 Database 类是整个本地存储的入口。你用 DAO 写的那些 SQL,最后都得靠这个抽象类来启动。我最早接触 Room 时,以为它就是个简单的 SQLite 封装,结果在版本迁移上摔了好几跤……所以这一节,咱们把 Database 类的三个关键点掰开看:如何创建、怎么迁移、回调里能做什么。
一句话总结: Room 的 Database 类是一个抽象类,你只需要写一个继承它的类,加几个注解,Room 就会自动帮你管理数据库的创建、升级和回调。
4.1 创建数据库——三个核心要素
先看最基础的写法。我习惯把 Database 类定义成一个单例,避免数据库实例重复打开造成内存泄漏。代码如下:
@Database(
entities = [User::class, Book::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"myapp.db"
).build()
INSTANCE = instance
instance
}
}
}
}
这里面有几点我特别想强调:
- entities:所有实体类都要列出来。我见过有人漏写一个实体,结果运行时直接崩——Room 找不到对应的表。
- version:每改一次表结构,version 必须加 1。否则 Room 会抛
IllegalStateException。 - exportSchema:设为 true 会在编译期生成 schema 文件(JSON格式),方便你对比版本差异。我记得有一次线上数据库出了 bug,就是靠这个 schema 文件定位到字段类型变了。
- 单例 + @Volatile:确保多线程环境下只创建一个实例。别问我为什么不用 Dagger Hilt 单例——那是另一个话题,但基础写法你得会。
4.2 版本迁移——三种策略,看你选哪个
数据库版本升级是 Room 里最容易翻车的地方。我归纳了三种策略,按推荐程度排个序:
| 策略 | 适用场景 | 优点 | 风险点 |
|---|---|---|---|
| ⚡ AutoMigration(自动迁移) | 简单修改:加字段、改字段名(带@ColumnInfo) | 零代码,编译期自动处理 | 复杂操作(合并表、删除字段)不支持 |
| 🔧 Migration 对象(手动迁移) | 任意 SQL 操作:改表、迁移数据 | 完全可控,可写复杂 SQL | 代码量稍大,要自己写 ALTER TABLE |
| 🚨 fallbackToDestructiveMigration(放弃数据) | 开发调试、数据不重要、表结构大改 | 无迁移逻辑,直接用新表 | 用户数据全丢,生产环境慎用 |
我个人建议: 生产环境优先用 Migration 对象。虽然样板代码多一点,但你能清楚每一步做了什么。AutoMigration 我一般只用于开发中快速迭代的小改。
先看一个手动迁移的例子。比如我在 v1 时有个 User 表,v2 要加一个 age 字段:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0")
}
}
// 构建时传入 migration
Room.databaseBuilder(context, AppDatabase::class.java, "myapp.db")
.addMigrations(MIGRATION_1_2)
.build()
这里要注意:ALTER TABLE 在 SQLite 里只支持加字段,不能直接加 NOT NULL、PRIMARY KEY。如果想改约束,得用重命名旧表+创建新表+复制数据的套路。我以前就是踩了“想给已有字段加 NOT NULL”的坑,后来老老实实写了 10 行 SQL 才搞定。
再说 AutoMigration。从 Room 2.4 开始支持,用法很简单:
@Database(
entities = [User::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() { ... }
// 还需要一个自动迁移规范类(可选,用于复杂规则)
@AutoMigration(from = 1, to = 2, spec = MyAutoMigrationSpec::class)
class MyAutoMigrationSpec : AutoMigrationSpec { }
但你得理解,自动迁移只处理“安全”的变化——比如新增字段(且实体里定义了 @ColumnInfo(defaultValue = "xxx"))。如果包含删除表、合并表等操作,Room 编译就会报错。所以它适合简单场景,我不建议在生产环境依赖它处理大版本升级。
4.3 回调——Callback 能帮你做什么
RoomDatabase.Callback 提供了三个生命周期钩子:onCreate、onOpen、onDestructiveMigration。我通常用它们来做以下事情:
- onCreate:数据库首次创建时执行。适合插入预设数据,比如国家列表、配置参数。
- onOpen:每次打开数据库时执行。可以用来做数据校验、清理缓存,或者记录日志。
- onDestructiveMigration:当启用了 fallbackToDestructiveMigration 且版本不匹配时触发。你可以在这里清理外部缓存、标记需要重新同步等。
举个实际例子。有一次我需要给新用户预置一些默认分类数据,我就在 onCreate 中调用 DAO 批量插入:
val callback = object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 注意:此时数据库已创建,但 DAO 实例还不能直接用
// 需要先获取 Database 再拿到 DAO
INSTANCE?.let { database ->
val defaultCategories = listOf(
Category(0, "工作"),
Category(0, "学习"),
Category(0, "生活")
)
database.categoryDao().insertAll(defaultCategories)
}
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
Log.d("Room", "数据库已打开,当前版本 ${db.version}")
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
// 数据被清空,可以通知后台清除远程缓存
Log.w("Room", "发生了 destructive migration,本地数据已丢失")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "myapp.db")
.addCallback(callback)
.build()
注意: 在 onCreate 里直接操作 DAO 有个坑——如果数据库还没完全初始化完就插入,可能出现死锁。我建议要么在 onCreate 中手动执行 SQL(通过 SupportSQLiteDatabase),要么在数据库创建完成后延迟执行。我自己更倾向于用后一种:在单例 getInstance 返回后,再通过协程异步插入预设数据。
4.4 知识结构图:Database 类全貌
下面这张图把 Database 类的三大模块串起来,你可以照着它梳理自己的代码:
4.5 避坑指南——那些年我踩过的坑
- 版本号忘了加:有一次我在实体里加了个字段,没改 version,Room 直接抛异常。自那以后我养成习惯:改完实体立马同步升级 version。
- fallbackToDestructiveMigration 上了生产:我亲眼见过同事发布了一个版本,数据库从 v3 到 v4,忘记写 Migration,用户全量数据被清空……那次事故之后,我们在 CI 里加了 schema 校验,强行要求每个版本必须提供 Migration。
- Callback 内操作 DAO 出现死锁:前面说过了,我后来改用
SupportSQLiteDatabase执行 SQL,或者等数据库构建完成后用协程延迟执行。
总结一下: Database 类是 Room 的上帝入口。你把 entities 列全、version 写对、Migration 配好、Callback 用对,数据库层面就不会出大问题。剩下的交给 Room 自动生成的代码,它比你手写 SQLiteOpenHelper 要可靠得多。