Room 数据库概述:架构、与 SQLite 的关系、组件组成
做 Android 开发的朋友,没跟 SQLite 打过交道的恐怕不多。但直接操作 SQLite 有多痛苦,用过的人都懂——手写 SQL 语句、手动管理游标、写一堆重复的样板代码……你说它不好用吧,它确实能跑;你说它好用吧,每次改个表结构都能让人崩溃。嗯,Room 就是来解决这些问题的。
Room 是什么?
Room 是 Google 推出的持久化库,属于 Jetpack 家族。它本质上是对 SQLite 的一层抽象。说白了,你还是在跟 SQLite 打交道,但 Room 帮你把那些脏活累活干掉了——编译时校验 SQL、自动生成 DAO 实现、无缝配合 LiveData 和 Flow。
我当年接手一个老项目,里面直接用 SQLiteOpenHelper,光建表就写了三百行,还经常跑出 SQLiteException。后来我换了 Room,干干净净,一个 @Entity 搞定表定义,一个 @Dao 搞定查询。说实话,早该换了。
Room 架构概览
Room 的设计思路很清晰:三层结构。你只需要关心顶层,剩下的 Room 帮你包揽。
- Entity — 数据实体,映射到数据库中的一张表。每个字段就是表的一列。
- DAO — 数据访问对象,定义你需要的所有 CRUD 操作。
- Database — 数据库持有者,连接 Entity 和 DAO,提供数据库实例。
这三层关系有点像:你写个类(Entity),告诉 Room 你的数据长什么样;再写个接口(DAO),告诉 Room 你要怎么操作这些数据;最后写个抽象类(Database),把两者注册进去。剩下的编译、生成、运行,Room 全包了。
Room 与 SQLite 的关系
很多人问:Room 是不是替换了 SQLite?当然不是。Room 底层还是 SQLite,你把 APK 的数据库文件拖出来,照样能用 SQLite Browser 打开。
那 Room 到底做了什么呢?我给你列个对比,你一看就明白了。
| 对比项 | 传统 SQLite (SQLiteOpenHelper) | Room |
|---|---|---|
| SQL 校验 | 运行时才报错 | 编译时查错,写错表名立即编译失败 |
| 样板代码 | 大量 ContentValues、Cursor 处理 | 注解自动生成,几行搞定 |
| 类型安全 | 手拼 SQL 容易类型错乱 | DAO 方法参数强类型,编译期约束 |
| 与 LiveData/Flow 集成 | 需手动注册 ContentObserver | 天然支持,返回 LiveData 自动感知变化 |
| 迁移升级 | 自己写 onUpgrade,复杂版本差 | Migration 类 + 自动测试,老版本数据几乎不停服 |
Room 核心组件
1. Entity —— 数据模型
Entity 就是一个普通的 Kotlin/Java 类,加上 @Entity 注解。每个 Entity 对应一张表,每个字段对应一列。表名默认是类名,你也可以用 tableName 指定。
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "full_name") val name: String,
val age: Int
)
这里有个坑:如果你用 val 不可变字段,Room 会要求你给出默认值,或者直接用构造器传参。我习惯给 id 一个默认 0,因为 autoGenerate 会让 Room 自动分配。
2. DAO —— 数据访问对象
DAO 是一个接口,里面写 @Insert、@Query、@Update、@Delete 方法。Room 在编译时生成实现类。
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): LiveData<List<User>>
@Delete
suspend fun delete(user: User)
}
LiveData 或 Flow,Room 会自动在后台线程运行查询,但如果你用 List 直接返回,那必须在非主线程调用,否则会崩。写 suspend 函数配合 Kotlin 协程是最稳的方案。
3. Database —— 数据库持有者
Database 是一个抽象类,继承 RoomDatabase,加上 @Database 注解,列出所有 Entity 和版本号。通常这里还暴露 DAO 的获取方法。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"myapp.db"
).build().also { INSTANCE = it }
}
}
}
}
这里注意 exportSchema 参数:建议设为 true 并配置 schema 输出路径,这样你可以在编译产物里看到 Room 自动生成的数据库 schema 文件,用来做版本迁移测试特别有用。
- Entity 定义表结构,注意主键、索引、外键(Room 支持
@ForeignKey) - DAO 定义操作,用
suspend或LiveData避免主线程卡顿 - Database 是单例,用
synchronized或 Dagger/Hilt 提供 - 编译时 Room 会校验 SQL,如果在
@Query里写错列名,编译器直接报红
说实话,我第一次用 Room 的时候,觉得不就是个 ORM 吗?用了才发现它的设计很克制——没有搞全自动 ORM 那套脏话,让你写 SQL,但把重复的胶水代码全砍了。尤其是编译时 SQL 校验,这个功能救了我好几次。记得有一次我在 @Query 里写了个 SELECT * FRM ...(拼写错误),IDE 直接飘红,编译都过不去。要是在原始 SQLite 里,这种错误只能等测试跑崩才能发现。
好了,这一章把 Room 的骨架搭起来了。后面我们会深入每个组件的性能细节,比如 DAO 的查询优化、索引的使用、事务的坑等等。你先把这张图刻在脑子里:Application → RoomDatabase → DAO + Entity → SQLiteDatabase,这层结构贯穿整门课程。