Room 实体与 DAO 定义

在 Room 架构中,实体(Entity) 对应数据库中的表,数据访问对象(DAO) 封装了对表的操作。本章将深入讲解实体注解、主键与索引的配置、DAO 接口方法设计,以及如何通过 LiveData/Flow 实现响应式查询。

实体注解

每个实体类使用 @Entity 标记,并指定表名(可选)。常用注解如下:

注解作用示例用法
@Entity声明该类为数据库表@Entity(tableName = "users")
@PrimaryKey定义主键(支持自动生成)@PrimaryKey(autoGenerate = true)
@ColumnInfo自定义列名、类型、是否索引等@ColumnInfo(name = "user_name", index = true)
@Ignore忽略字段,不存入数据库@Ignore val tempData: String
@Embedded嵌入子对象字段(扁平化存储)@Embedded val address: Address
@Index在表级声明索引或唯一约束@Entity(indices = [Index(value = ["email"], unique = true)])
@ForeignKey定义外键及级联操作见下方示例
提示: Room 2.4.0+ 支持 @PrimaryKey 与 @ColumnInfo 中的 defaultValue 属性,可在建表时指定默认值。

主键与索引

主键配置

单字段主键推荐使用 autoGenerate = true 配合 Long/Int 类型:

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

复合主键需在 @Entity 中声明 primaryKeys:

@Entity(primaryKeys = ["username", "email"])
data class User(
    val username: String,
    val email: String,
    val age: Int
)

索引与唯一约束

索引可提升查询速度,但会增加写入开销。常用三种定义方式:

  • 单列索引:在字段上加 @ColumnInfo(index = true)
  • 组合索引:在 @Entity(indices = ...) 中声明
  • 唯一索引:设置 unique = true
@Entity(
    tableName = "user_profiles",
    indices = [
        Index(value = ["phone"], unique = true),
        Index(value = ["country", "city"], unique = false)
    ]
)
data class UserProfile(
    @PrimaryKey val id: Long,
    val phone: String,
    val country: String,
    val city: String
)
避坑: 复合索引的列顺序影响查询效率,将选择性高的列放在前面。Room 不会自动为外键创建索引,若外键频繁用于 JOIN,请手动添加索引。

DAO 接口方法设计

DAO 是一个接口,每个方法对应一个数据库操作。Room 支持五种操作注解以及自定义 @Query:

注解用法返回值类型说明
@Insert@Insert(onConflict = OnConflictStrategy.REPLACE)Long 或 List<Long>插入单条或多条,返回新行的 rowId
@Update@UpdateInt(影响行数)根据主键更新
@Delete@DeleteInt(影响行数)根据主键删除
@Query@Query("SELECT * FROM users WHERE age > :minAge")任意 POJO、LiveData、Flow 等自定义 SQL,支持参数传递
@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun insert(user: User): Long

    @Update
    suspend fun update(user: User): Int

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM users ORDER BY id DESC")
    suspend fun getAllUsers(): List<User>

    @Query("SELECT * FROM users WHERE id = :userId")
    fun getUserById(userId: Long): LiveData<User>

    @Query("SELECT * FROM users WHERE age >= :minAge")
    fun getUsersOlderThan(minAge: Int): Flow<List<User>>
}
最佳实践: 对于非阻塞操作(如 UI 数据展示),优先使用 LiveData 或 Flow 作为返回值,Room 会自动在后台线程执行查询并在数据变化时通知观察者。

LiveData / Flow 返回值

Room 支持将 DAO 方法返回值声明为 LiveData<T> 或 Flow<T>(Kotlin 协程版)。两者都是观察者模式,但有以下区别:

特性LiveDataFlow
线程安全只能在主线程观察天然挂起,可指定调度器
生命周期感知自动感知 LifecycleOwner需配合 .flowWithLifecycle() 或 repeatOnLifecycle
操作符有限(map、switchMap)丰富的协程操作符(map, filter, combine 等)
数据发射首次订阅立即发射当前值默认冷流,订阅后才开始发射;可用 stateIn 转为热流
异常处理需封装 Result 或自定义原生 catch、retry 等

在 ViewModel 中观察 Flow 的典型模式:

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    val users: StateFlow<List<User>> = userDao.getAllUsersFlow()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}
常见错误: 将 List<User> 返回类型声明为 LiveData 时,若表中有千条记录而不加分页,可能导致 OOM。建议使用 Paging 3 + PagingData 配合 Flow。
Room 实体与 DAO 交互流程 @Entity 表映射:字段、主键、索引 @PrimaryKey / autoGenerate @ColumnInfo(name, index) @Entity(indices = [...]) @ForeignKey(...) @Embedded / @Ignore @Dao SQL 操作定义 @Insert → Long / List<Long> @Update → Int @Delete → Int @Query("SQL...") → List<T> @Query → LiveData / Flow 观察者 LiveData<T> (生命周期感知) Flow<T> (冷流 / 热流) StateFlow<T> (带状态的热流) PagingData<T> (分页感知) 💡 箭头方向:实体类定义 → DAO 接口方法 → 响应式返回值(LiveData/Flow)供 UI 或后台任务订阅

调试与验证

在 Android Studio 的 App Inspection 中可查看生成的 Room 数据库 schema,确认建表语句是否正确包含索引、外键。常用调试步骤:

  1. 在 build.gradle 中启用 schema 导出:room { schemaLocation "$projectDir/schemas" }
  2. 编译后查看 schemas/<version>/...json 中表的 indices 和 foreignKeys 字段。
  3. 运行 @Query 方法并使用 database.execSQL("EXPLAIN QUERY PLAN ...") 检查是否命中索引。
// 在测试中验证索引效果
@Test
fun verifyIndexUsage() {
    val sql = "EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com'"
    val cursor = db.execSQL(sql)
    // 检查输出是否包含 "USING INDEX" 字样
}

性能与稳定性优化

  • 索引设计:为 WHERE、ORDER BY、JOIN 字段建立合适索引;避免在低选择性列(如布尔值)上建索引。
  • 避免大事务:@Insert 传入大列表时,Room 会自动生成批量 SQL,但仍建议控制在 500 条以内并使用 InsertStrategy。
  • Flow 的线程安全:Room 的 Flow 查询默认在后台调度器执行,但需确保 Flow 的收集端(如 ViewModel)切换回主线程:.flowOn(Dispatchers.IO)。
  • 数据更新通知:@Query 返回 LiveData 或 Flow 时,Room 会自动监听表中任意行的变化。若表被频繁写入(如 100ms 内多次插入),会导致过多重绘,此时可考虑使用 distinctUntilChanged() 或设置节流阀。
// Flow 中加入防抖
fun getUserFlow(userId: Long): Flow<User> = userDao.getUserByIdFlow(userId)
    .debounce(200) // 200ms 内多个变化只取最后一个