5、Database数据库类:@Database注解、抽象类定义、单例模式实现、数据库版本管理
好,咱们今天来聊聊 Room 里最核心的“大管家”——Database 类。你想想看,前面我们定义了 Entity(表结构)、DAO(数据操作接口),但总得有个东西把它们串起来吧?这个串场的就是 Database 类。
说白了,它就是整个 Room 数据库的入口。你所有的增删改查,都得先拿到这个 Database 实例,然后才能调用 DAO。所以,怎么写好这个类,直接决定了你的数据库稳不稳、好不好用。
5.1 @Database 注解:告诉 Room 你的“家底”
我们先看注解。@Database 是 Room 框架用来识别数据库配置的“身份证”。你必须在抽象类上加上它,并告诉 Room 三件事:
- entities:你的数据库里有哪些表(Entity 类)
- version:当前数据库的版本号
- exportSchema:是否导出数据库 Schema 到文件(建议开启,方便追踪版本变化)
我个人习惯,在项目初期就把 exportSchema 设为 true,这样后面做迁移时,能清楚看到每个版本的表结构变化。嗯,这里要注意,如果你设为 true,记得在 build.gradle 里配置 schema 输出目录,不然会报错。
@Database(
entities = [UserEntity::class, BookEntity::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// ...
}
你看,代码很简洁。但背后 Room 会帮你生成一个实现类,比如 AppDatabase_Impl,它负责创建数据库、管理版本、调用 DAO 实现。你只管定义抽象方法就行。
5.2 抽象类定义:为什么必须是抽象类?
你可能会问:为什么 Database 类必须是抽象的?
原因很简单:Room 需要帮你生成实现代码。如果你写成一个普通类,Room 就没法插手了。抽象类意味着“我只定义接口,具体实现交给 Room 框架”。
在抽象类里,你需要声明一个或多个抽象方法,返回你定义的 DAO 接口。比如:
abstract fun userDao(): UserDao
abstract fun bookDao(): BookDao
Room 会为每个抽象方法生成对应的 DAO 实现。我在项目中遇到过,有人把 DAO 方法写成了非抽象方法,结果 Room 编译报错,提示“必须声明为 abstract”。嗯,这个坑我踩过,大家注意。
5.3 单例模式实现:别让数据库实例满天飞
数据库实例是个“重量级”对象。创建一次就要打开文件、加载表结构、初始化连接池。如果你每次用都 new 一个,内存和性能都会出问题。
所以,单例模式是标准做法。我一般用双重检查锁定(Double-Checked Locking)配合 @Volatile 来保证线程安全。
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"my_database.db"
).build()
INSTANCE = instance
instance
}
}
}
这里有几个细节:
- 用
context.applicationContext而不是context,防止 Activity 泄漏。 synchronized保证并发安全,但只锁第一次创建。@Volatile确保 INSTANCE 的可见性,避免线程读到旧值。
我曾经在线上项目里,因为忘了加 @Volatile,导致高并发下创建了两个实例,结果数据库文件被锁死,用户数据丢失。从那以后,我再也不敢省这个关键字了。
5.4 数据库版本管理:从 v1 到 v2 的“平滑升级”
版本管理是 Room 里最容易出问题的地方。你想想看,用户手机上装的是 v1 的数据库,你发了个新版本,表结构变了,怎么办?
Room 提供了两种策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| fallbackToDestructiveMigration | 直接删除旧表,重建新表 | 开发阶段、测试环境、数据不重要时 |
| addMigrations | 编写迁移脚本,保留旧数据 | 生产环境、用户数据必须保留 |
我个人强烈建议:生产环境永远不要用 fallbackToDestructiveMigration。为什么?因为用户的数据是无价的。你一个删除操作,可能就让用户几年的笔记、订单、聊天记录全没了。
正确的做法是写 Migration 对象:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 比如新增一个 age 列
database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
// 在 build 时传入
Room.databaseBuilder(...)
.addMigrations(MIGRATION_1_2)
.build()
嗯,这里要注意:Migration 里的 SQL 必须是纯 SQLite 语法,不能使用 Room 的注解。而且,如果你有多个版本升级,比如从 v1 到 v3,你需要写两个 Migration(1→2 和 2→3),Room 会自动按顺序执行。
INSERT INTO ... SELECT 来迁移数据,结果忘了处理主键冲突,导致用户数据重复。建议在迁移前先备份数据库,或者用 BEGIN TRANSACTION 包裹,出错了可以回滚。
5.5 整体结构图:Database 类的工作流程
为了让你更直观地理解,我画了一张图。它展示了 Database 类如何串联 Entity、DAO 和版本管理。
从这张图你可以看到:Database 类在最上层,它通过抽象方法暴露 DAO;DAO 再操作 Entity 对应的表。而版本管理则像“安全网”,确保数据库升级时数据不丢失。
data.local 包来放所有数据库相关代码,这样后期维护时,一眼就能找到。
好了,关于 Database 类,核心就是这些。记住:注解要写全、抽象方法要定义对、单例要线程安全、版本迁移要谨慎。把这四点做好,你的 Room 数据库就稳了。
公众号:蓝海资料掘金营,微信deep3321