Room数据库:从入门到「真香」

在 Android 开发中管理本地数据,我折腾过 SQLiteOpenHelper,也踩过 ContentProvider 的坑。直到 Google 推出了 Room,我才觉得「这才像个正经的 ORM」。说白了,Room 就是 SQLite 的优雅封装,它帮你把 SQL 语句写成了注解和方法调用,还能跟 LiveData、Flow 无缝衔接。

这一章咱们就聊聊 Room 是什么、怎么配、有哪些坑。我敢说,搞清楚了这些,后面做缓存同步才不至于手忙脚乱。

2.1 Room 的核心思想

Room 的本质是一个 ORM(对象关系映射) 框架。它把数据库的表映射成 Kotlin/Java 的实体类,把 SQL 操作映射成 DAO 接口里的方法。你想想看,以前写 SQLite 要手撸 SQL 语句、处理 Cursor、手动拼装对象,现在呢?一个注解就搞定。

一句话总结: Room = SQLite + 编译时注解 + 响应式支持。

我个人习惯把 Room 的架构分成三层:

  • Entity(实体层) —— 对应数据库表结构。
  • DAO(数据访问层) —— 封装 CRUD 操作。
  • Database(数据库实例层) —— 管理数据库的创建、升级、迁移。

这三层串联起来,就是 Room 的完整工作流。下面的 SVG 图可以帮你快速建立直观印象:

Room 核心架构与配置流程 Entity @Entity 注解的类 DAO @Dao 注解的接口 Database @Database 抽象类 1. 定义 Entity 2. 编写 DAO 3. 创建 Database 4. 配置版本 & 迁移 5. 初始化 Database 关键配置项 version: 数据库版本号 exportSchema: true fallbackToDestructiveMigration() addMigrations(Migration...) build() ⚠ 迁移策略: · 增量升级:addMigrations · 暴力重建: fallbackToDestructive Migration()

2.2 三剑客:Entity、DAO、Database

2.2.1 Entity —— 一张表一个类

拿一个最简单的用户表举例:

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "nickname") val name: String,
    val age: Int
)

这里的 @PrimaryKey 指定主键,autoGenerate = true 表示自增。如果你有多个字段组合主键,可以用 @PrimaryKeys 注解。

小建议: 表名和字段名建议用下划线命名法(snake_case),跟传统数据库保持一致。我以前在项目里用过驼峰命名,结果写 rawQuery 时总搞混大小写,后来就统一改成下划线了。

2.2.2 DAO —— 数据操作的门面

定义好实体后,你需要一个接口来操作它:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User)

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUser(id: Int): User?

    @Query("SELECT * FROM users")
    fun getAllUsers(): Flow<List<User>>

    @Delete
    suspend fun delete(user: User)
}

注意看,getAllUsers() 返回的是 Flow<List<User>>。Room 原生支持 Flow 和 LiveData,数据库有变化时会自动通知观察者。这就是后面跟 Compose 状态同步的关键 —— 你不需要手动刷新,数据变了 UI 自动更新。

2.2.3 Database —— 数据库管家

最后一步,定义一个抽象的、继承 RoomDatabase 的类:

@Database(entities = [User::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

entities 里列出所有实体类,version 是你当前数据库的版本号,exportSchema 设为 true 会生成数据库结构的 JSON 文件,方便做版本对比。我个人建议你开启它,调试迁移时特别有用。

2.3 数据库配置与迁移策略

嗯,这里要注意。数据库不是创建一次就完事的。你的 App 会升级,表结构会变,怎么办?Room 提供了一套非常优雅的迁移机制。

2.3.1 基本配置

在 Application 或依赖注入容器中初始化:

val db = Room.databaseBuilder(
    context,
    AppDatabase::class.java,
    "my_database.db"
).build()

这行代码就会创建或打开一个名为 my_database.db 的数据库文件。

2.3.2 版本迁移

假设你的 App 从版本 1 升级到版本 2,需要在 users 表里加一个 email 字段。你可以在 @Database 里把 version 改成 2,然后定义迁移:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''")
    }
}

Room.databaseBuilder(context, AppDatabase::class.java, "my_database.db")
    .addMigrations(MIGRATION_1_2)
    .build()

如果你不做迁移,直接升级版本,Room 会抛异常。但有一种偷懒的做法:

⚠ 危险操作: fallbackToDestructiveMigration() 表示「如果找不到匹配的迁移,就删除旧表重建」。我曾在项目里图省事用过它,结果线上用户的数据全部丢失,被追着骂了三天。记住,这玩意儿只适合开发阶段的 Demo,不能用到生产环境!

2.4 配置清单速查表

为了你以后配置时不翻文档,我把 RoomDatabaseBuilder 里常用的方法列出来:

配置项 说明 推荐值
addMigrations() 添加增量迁移 每个版本对应一个 Migration 对象
fallbackToDestructiveMigration() 找不到迁移则删除重建 ❌ 生产环境禁用
allowMainThreadQueries() 允许在主线程执行查询 ❌ 会卡 UI,别用
enableMultiInstanceInvalidation() 多进程共享时同步数据库变更 多进程场景才需要

2.5 实战中的小经验

最后分享几个我踩过的坑:

  • 记得用协程。 Room 从 2.1 开始完整支持协程(suspend 函数),不要再写异步回调了。我见过不少老项目还在用 LiveData + asyncTask,现在全都可以用 Flow 替代。
  • Schema 文件一定要加到 .gitignore。 那个 JSON 文件只是供 Room 编译器对比用的,你不需要把它提交到 Git。我曾经不小心提交了,每次合并都冲突……
  • 主键不要用可空类型。 比如 Int? 作为主键,Room 会报错。我吃过这个亏,排查了半天才找到原因。
  • Migration 类要定义为 companion object。 这样可以避免每次 build 都 new 一个对象,也方便复用。