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 会自动按顺序执行。

⚠️ 避坑指南: 我曾经在 Migration 里用了 INSERT INTO ... SELECT 来迁移数据,结果忘了处理主键冲突,导致用户数据重复。建议在迁移前先备份数据库,或者用 BEGIN TRANSACTION 包裹,出错了可以回滚。

5.5 整体结构图:Database 类的工作流程

为了让你更直观地理解,我画了一张图。它展示了 Database 类如何串联 Entity、DAO 和版本管理。

Room Database 类核心结构 AppDatabase (抽象类) @Database(entities, version) userDao() / bookDao() 抽象方法 → Room 生成实现 UserEntity / BookEntity @Entity 定义表结构 版本管理 Migration(1→2) → ALTER TABLE / 数据迁移 → 保留用户数据 单例模式 双重检查锁定 + @Volatile

从这张图你可以看到:Database 类在最上层,它通过抽象方法暴露 DAO;DAO 再操作 Entity 对应的表。而版本管理则像“安全网”,确保数据库升级时数据不丢失。

💡 我的小建议: 在项目初期,就把 Database 类、DAO、Entity 的包结构规划好。我习惯用 data.local 包来放所有数据库相关代码,这样后期维护时,一眼就能找到。

好了,关于 Database 类,核心就是这些。记住:注解要写全、抽象方法要定义对、单例要线程安全、版本迁移要谨慎。把这四点做好,你的 Room 数据库就稳了。


公众号:蓝海资料掘金营,微信deep3321