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 图可以帮你快速建立直观印象:
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 一个对象,也方便复用。