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 架构与数据流 Application / ViewModel RoomDatabase DAO (接口+实现) Entity (表映射) SQLiteDatabase 说明:箭头方向表示数据请求流向;RoomDatabase 自动连接 DAO、Entity 和底层 SQLiteDatabase

Room 与 SQLite 的关系

很多人问:Room 是不是替换了 SQLite?当然不是。Room 底层还是 SQLite,你把 APK 的数据库文件拖出来,照样能用 SQLite Browser 打开。

那 Room 到底做了什么呢?我给你列个对比,你一看就明白了。

对比项传统 SQLite (SQLiteOpenHelper)Room
SQL 校验运行时才报错编译时查错,写错表名立即编译失败
样板代码大量 ContentValues、Cursor 处理注解自动生成,几行搞定
类型安全手拼 SQL 容易类型错乱DAO 方法参数强类型,编译期约束
与 LiveData/Flow 集成需手动注册 ContentObserver天然支持,返回 LiveData 自动感知变化
迁移升级自己写 onUpgrade,复杂版本差Migration 类 + 自动测试,老版本数据几乎不停服
我的建议:如果你的项目还在用 raw SQLite,赶紧考虑 Room。原因很简单——你省掉的那些代码时间和调试 SQL 的时间,加起来比 Room 的迁移成本高多了。我有个朋友项目里一个 SQL 查询跑了两年没出过事,上周突然抛异常,查了半天是一个表字段类型改了但查询没改……换 Room 编译期就发现了。

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)
}
我曾经踩过的坑:如果 DAO 方法返回 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,这层结构贯穿整门课程。