6、Room数据库:Room配置与Entity、DAO与Repository模式、Room与Flow结合

说实话,Room 是我在 Jetpack 里用得最爽的一个组件。为什么?因为它把 SQLite 那些繁琐的模板代码全给干掉了。你想想看,以前写个数据库操作,要写 SQLiteOpenHelper、ContentValues、Cursor……一堆东西。现在呢?一个注解搞定。

我个人习惯把 Room 看作是「带注解的 SQLite 封装」。它不是在底层替换 SQLite,而是让你用更优雅的方式去操作它。今天我们就来聊聊 Room 的核心玩法。

6.1 Room 配置:三步走

配置 Room 其实就三步:加依赖、写 Entity、写 DAO。咱们一步步来。

首先,在 build.gradle 里加上 Room 的依赖。注意要用 kapt 或者 ksp,因为 Room 需要编译时注解处理。

// 项目级 build.gradle
plugins {
    id 'org.jetbrains.kotlin.kapt' version '1.9.0'
}

// 模块级 build.gradle
dependencies {
    def room_version = "2.6.1"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
}

嗯,这里要注意版本号。我建议你每次用最新稳定版,因为 Room 团队一直在优化编译速度和运行时性能。

6.2 Entity:数据表的 Kotlin 化身

Entity 说白了就是一张表的 Kotlin 类。你定义一个数据类,加上 @Entity 注解,Room 就会自动帮你建表。

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "user_name")
    val name: String,
    val age: Int,
    @ColumnInfo(name = "created_at")
    val createdAt: Long = System.currentTimeMillis()
)

我在项目中遇到过一个问题:有人把 @PrimaryKey 忘了加 autoGenerate = true,结果每次插入都要手动传 id,一传错就崩。所以我的建议是:只要 id 是自增的,就老老实实加上 autoGenerate。

小技巧: 如果你有多个字段需要联合主键,可以用 @Entity(primaryKeys = ["firstName", "lastName"])。不过我个人很少用联合主键,更多是用一个自增 id 做主键,再加 unique 约束。

6.3 DAO:数据访问对象

DAO 是 Room 的灵魂。你定义一个接口,写上 SQL 语句,Room 在编译时帮你生成实现类。

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

    @Insert
    fun insertUser(user: User)

    @Update
    fun updateUser(user: User)

    @Delete
    fun deleteUser(user: User)
}

你看,就这么简单。@Insert、@Update、@Delete 这些注解,Room 会自动生成对应的 SQL。但 @Query 需要你自己写 SQL,这也是 Room 最灵活的地方。

注意: @Insert 默认是「如果冲突就抛异常」。如果你想要「冲突时替换」,可以这样写:@Insert(onConflict = OnConflictStrategy.REPLACE)。我曾经因为没加这个,线上用户数据重复插入导致崩溃……嗯,血的教训。

6.4 Repository 模式:把 DAO 包一层

直接在你的 ViewModel 里调用 DAO 行不行?行,但不推荐。为什么?因为你的 ViewModel 不应该知道数据是从本地数据库来的,还是从网络来的。Repository 就是中间这一层。

class UserRepository(private val userDao: UserDao) {
    fun getAllUsers(): List<User> {
        return userDao.getAllUsers()
    }

    suspend fun insertUser(user: User) {
        userDao.insertUser(user)
    }
}

说白了,Repository 就是一个「数据源管理器」。它可能从 Room 读,也可能从 Retrofit 读。ViewModel 只管调用 repository.getXxx(),不用管数据从哪来。

我个人习惯在 Repository 里做「数据源切换」的逻辑。比如先读缓存,缓存没有再去网络请求,请求回来再存到 Room。这样 ViewModel 的代码就非常干净。

6.5 Room 与 Flow 结合:响应式数据库

这是 Room 最酷的地方。你想想看,以前要监听数据库变化,得写 ContentObserver 或者自己搞回调。现在呢?直接返回 Flow 就行。

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun observeAllUsers(): Flow<List<User>>

    @Insert
    suspend fun insertUser(user: User)
}

注意看,这里返回的是 Flow<List<User>>。每次数据库里的 users 表有变化,Room 会自动发射一个新的 List。你的 UI 层订阅这个 Flow,就能实时更新。

为什么会这样?因为 Room 在底层用了 SQLite 的触发器机制。每次表数据变化,Room 会重新执行查询,然后把结果通过 Flow 发出来。你想想看,这比你自己写轮询优雅了多少倍?

// 在 ViewModel 里
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    val users: StateFlow<List<User>> = repository.observeAllUsers()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

这里我用了 stateIn 把 Flow 转成 StateFlow。为什么要转?因为 UI 层需要「当前值」,而 Flow 是冷流,不订阅就不执行。StateFlow 会保留最后一个值,UI 重建时直接拿,不用等数据库重新查询。

核心要点: Room + Flow 的组合,让你用声明式的方式处理数据库变化。你只需要定义「查询什么」,Room 自动帮你处理「什么时候更新」。这就是响应式编程的魅力。

6.6 避坑指南

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

  • 不要在 DAO 里用 LiveData 返回复杂查询:LiveData 的回调在 UI 线程,复杂查询会卡 UI。用 Flow 可以指定调度器。
  • @Transaction 别忘了加:如果你在一个方法里操作多个表,记得加 @Transaction 注解。我曾经没加,结果并发插入时数据不一致……
  • 数据库迁移要小心:Room 的 migration 机制很强大,但写错了会丢数据。我建议每次改表结构前,先写个测试用例验证迁移逻辑。

好了,Room 的核心内容就这些。配置、Entity、DAO、Repository、Flow,这几个概念串起来,就是一个完整的本地数据层。你想想看,以前写 SQLite 要多少代码?现在用 Room,一个注解搞定建表,一个接口搞定 CRUD,一个 Flow 搞定实时更新。这就是 Jetpack 的威力。

Room 架构组件关系图 UI (Activity/Fragment) ViewModel Repository Room Database DAO Entity Flow 数据流方向:UI → ViewModel → Repository → Room 响应式更新:Room → Flow → ViewModel → UI

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