15、Room数据库:从入门到实战

Room 这个组件,说实话刚出来的时候我还有点抵触。毕竟 SQLite 用了那么多年,突然来个「官方 ORM」,总觉得多此一举。直到我在一个项目里被 CursorLoader 和 SQLiteOpenHelper 的模板代码折磨得够呛……嗯,真香了。

今天我们就来聊聊 Room。它本质上是对 SQLite 的一层封装,但封装得恰到好处。你不需要写一堆重复的代码,也不用担心线程安全问题。我个人觉得,它是 Google 近几年做得最靠谱的库之一。

Room 的核心组件

Room 由三部分组成:Entity、Dao 和 Database。这三者缺一不可,配合起来就像一套完整的 MVC 架构。

组件 作用 类比
Entity 定义数据表结构 Java Bean + 表映射
Dao 定义数据库操作 SQL 语句的接口
Database 数据库持有者 SQLiteOpenHelper 的替代

核心思想:Room 在编译期会检查你的 SQL 语句是否正确。这个特性帮我省了不少事——以前写 SQLite 时,运行时才发现 SQL 写错了,那叫一个尴尬。

Entity:定义数据表

Entity 说白了就是一个普通的 Kotlin 数据类,加上一些注解。每个 Entity 对应一张表,每个字段对应一列。

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

这里有几个点要注意:

  • @PrimaryKey 是必须的。每个表都要有主键,这是数据库的基本要求。
  • autoGenerate = true 表示自增。我习惯用 Long 类型做主键,省心。
  • @ColumnInfo 可以自定义列名。如果你后端接口返回的是 user_name,这里就对应上。
  • 字段类型支持基本类型、String、Date 等。复杂类型需要用 @TypeConverter 转换。

小技巧:如果你不想某个字段存到数据库,用 @Ignore 注解。比如一个临时计算的字段,不需要持久化。

Dao:数据库操作的接口

Dao 是一个接口,里面定义了你对数据库的所有操作。Room 会在编译期自动生成实现类。

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): List<User>
    
    @Query("SELECT * FROM users WHERE id = :userId")
    fun getUserById(userId: Long): User?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User): Long
    
    @Update
    fun updateUser(user: User)
    
    @Delete
    fun deleteUser(user: User)
}

我个人比较喜欢 @Insert 的 onConflict 参数。设置成 REPLACE 后,如果主键冲突就直接覆盖,省得先查再插。

@Query 里写的是原生 SQL。Room 会在编译期解析这些 SQL,如果表名或字段名写错了,编译直接报错。这个特性我太爱了——以前运行时才发现 SQL 写错,调试起来特别痛苦。

注意:Dao 里的方法默认在调用线程执行。如果你在 UI 线程调用,Room 会抛异常。解决办法是用 LiveData 或协程。

Database:数据库的入口

Database 是一个抽象类,继承自 RoomDatabase。它定义了数据库的版本、包含哪些 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 getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

这里有个经典的单例模式。为什么要用单例?因为创建数据库实例的开销不小,而且多个实例会导致数据不一致。@Volatile 和 synchronized 保证了线程安全。

exportSchema 建议设为 false,除非你需要做数据库迁移的版本控制。我一般只在正式项目里才开这个。

与 LiveData 结合:响应式查询

这才是 Room 的杀手锏。把 Dao 的返回值改成 LiveData,数据变化时 UI 会自动更新。

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsersLiveData(): LiveData<List<User>>
}

在 ViewModel 里这样用:

class UserViewModel(application: Application) : AndroidViewModel(application) {
    private val userDao = AppDatabase.getDatabase(application).userDao()
    val allUsers: LiveData<List<User>> = userDao.getAllUsersLiveData()
}

然后在 Activity 或 Fragment 里观察:

viewModel.allUsers.observe(this) { users ->
    // 更新 UI,比如刷新 RecyclerView
    adapter.submitList(users)
}

为什么会这么方便?因为 Room 在后台维护了一个观察者模式。当数据库中的数据发生变化时,它会自动通知所有观察者。你不需要手动刷新,也不需要写 ContentObserver。

我记得有一次,我在一个聊天应用里用了这个特性。新消息插入数据库后,聊天列表自动刷新,完全不用写额外的通知代码。那感觉,就像写前端 Vue 一样爽。

避坑指南:LiveData 的回调是在主线程执行的。所以不要在回调里做耗时操作。如果你需要做复杂的数据处理,建议用 Transformations.map 或 switchMap 转换一下。

知识体系总览

下面这张图概括了 Room 的整体架构和数据流向。我画的时候特意把 LiveData 的观察机制也加进去了,这样你就能理解为什么数据变化后 UI 会自动更新。

Room 架构与数据流 Entity 数据表定义 @Entity, @PrimaryKey Dao 数据库操作接口 @Query, @Insert, @Update Database 数据库持有者 @Database, 单例模式 映射 提供实例 LiveData 响应式数据 自动通知观察者 返回 LiveData UI (Activity/Fragment) 观察 LiveData,自动更新 observe() 增删改操作 数据流:Entity → Dao → Database → LiveData → UI(自动更新)

实战建议

最后分享几个我在项目中积累的经验:

  • 数据库版本升级:用 Migration 类处理。不要删表重建,用户数据丢了会骂娘的。
  • 测试:Room 支持内存数据库,测试时用 Room.inMemoryDatabaseBuilder,速度快,用完即毁。
  • 复杂查询:如果 SQL 太复杂,可以考虑用 RawQuery。但要注意,Room 不会检查 RawQuery 的语法。
  • 性能:批量插入用 @Insert 的 List 参数,比循环插入快一个数量级。

我的习惯:每个 Entity 都加一个 createdAt 和 updatedAt 字段。虽然 Room 不会自动维护,但手动更新一下,排查问题时会很有帮助。

好了,Room 的核心内容就这些。说白了,它就是让你用更少的代码、更安全的方式操作 SQLite。配合 LiveData 使用后,你会发现数据驱动的 UI 写起来特别顺手。

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