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 | @Update | Int(影响行数) | 根据主键更新 |
@Delete | @Delete | Int(影响行数) | 根据主键删除 |
@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 协程版)。两者都是观察者模式,但有以下区别:
| 特性 | LiveData | Flow |
|---|---|---|
| 线程安全 | 只能在主线程观察 | 天然挂起,可指定调度器 |
| 生命周期感知 | 自动感知 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。
调试与验证
在 Android Studio 的 App Inspection 中可查看生成的 Room 数据库 schema,确认建表语句是否正确包含索引、外键。常用调试步骤:
- 在
build.gradle中启用 schema 导出:room { schemaLocation "$projectDir/schemas" } - 编译后查看
schemas/<version>/...json中表的indices和foreignKeys字段。 - 运行
@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 内多个变化只取最后一个