2. Room实体设计与DAO定义:数据表结构设计、主键策略、类型转换器、DAO接口查询方法
Room是Android Jetpack中的持久化库,它在SQLite之上提供了一层抽象。理解Room的核心组件——Entity(实体)、DAO(数据访问对象)和TypeConverter(类型转换器)——是实现稳定本地存储的基础。本章将深入解析这些组件的设计原理、实现流程及常见陷阱。
2.1 实体设计:构建数据模型
每个用 @Entity 注解的Kotlin数据类都代表SQLite中的一张表。类的属性对应表的列。通过合理的表和列设计可以显著提升查询效率与数据一致性。
最佳实践: 始终显式指定
tableName,避免因类名混淆(例如数据库保留关键字或大小写问题)导致的运行时错误。使用 @ColumnInfo(name = "...") 明确列名映射。
实体定义示例
@Entity(
tableName = "users",
indices = [Index(value = ["email"], unique = true)]
)
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Date, // 需要TypeConverter
@ColumnInfo(name = "tags")
val tags: List<String> // 需要TypeConverter
)
2.2 主键策略:保证数据唯一性
每个实体必须定义至少一个主键。主键策略的正确选择直接影响插入性能和更新逻辑。
| 策略 | 注解/配置 | 适用场景 |
|---|---|---|
| 自动生成ID | @PrimaryKey(autoGenerate = true) | 无业务含义的代理键(推荐) |
| 复合主键 | @Entity(primaryKeys = ["firstName", "lastName"]) | 多字段唯一标识联合实体 |
| 自定义主键 | @PrimaryKey(无autoGenerate) | 使用业务ID(如服务器端ID) |
注意: 使用
autoGenerate = true 时,主键类型必须为 Long 或 int 的包装类型,否则编译会报错。复合主键时,插入冲突会触发 OnConflictStrategy。
2.3 类型转换器:桥接Room与自定义类型
Room原生支持基本数据类型、封装类以及 String、byte[] 等。对于 Date、List<String> 等复杂类型,必须通过 @TypeConverter 定义序列化/反序列化逻辑。
实现流程
- 创建包含
@TypeConverter静态方法的类。 - 在数据库类使用
@TypeConverters注解声明转换器作用域(全局或指定Dao/Entity)。
class Converters {
@TypeConverter
fun fromDate(date: Date?): Long? = date?.time
@TypeConverter
fun toDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
@TypeConverter
fun fromStringList(list: List<String>?): String? = list?.joinToString(",")
@TypeConverter
fun toStringList(data: String?): List<String>? = data?.split(",")?.filter { it.isNotEmpty() }
}
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
运行原理: Room在编译时扫描
@TypeConverters 作用域,将转换器方法注册到内部映射表。当从数据库读取或写入实体时,自动调用对应的转换方法。性能开销通常可以忽略。
2.4 DAO接口与查询方法
DAO(Data Access Object)是定义数据库操作的核心接口。Room支持两种查询方式:便捷方法(@Insert、@Update、@Delete)和自定义 @Query。
标准DAO定义示例
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User): Long
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users WHERE email = :email")
suspend fun getUserByEmail(email: String): User?
@Query("SELECT * FROM users ORDER BY full_name ASC")
fun getAllUsers(): LiveData<List<User>>
@Query("SELECT * FROM users WHERE created_at BETWEEN :start AND :end")
suspend fun getUsersBetweenDates(start: Long, end: Long): List<User>
}
| 方法 | 返回类型 | 线程 | 典型用途 |
|---|---|---|---|
suspend fun insert | Long (插入行ID) | IO | 协程异步插入 |
fun getAllUsers() | LiveData<List<User>> | 自动后台 | 响应式UI观察 |
suspend fun getUser | User? | IO | 一次数据获取 |
fun getUsersFlow() | Flow<List<User>> | 自动后台 | Kotlin协程响应式 |
2.5 调试、常见错误与优化
调试方法
- 开启查询日志: 在
Room.databaseBuilder()中加入.setQueryCallback(RoomDatabase.QueryCallback() { sql, bindArgs -> Log.d("Room", "SQL: $sql, Args: $bindArgs") })。 - 导出Schema: 在
build.gradle中配置room.schemaLocation,编译后自动输出JSON文件,可用于版本对比和验证。
常见错误
- Missing TypeConverter: Room找不到从
List<String>到String的转换方法,编译时抛出error: Cannot figure out how to save this field into database。 - 主键冲突: 插入重复主键时未指定
@Insert(onConflict = OnConflictStrategy.REPLACE),抛出SQLiteConstraintException。 - 线程违规: 在主线程调用非
suspend或非LiveData的DAO方法,抛出IllegalStateException: Cannot access database on the main thread。
性能与稳定性优化
- 索引优化: 在频繁查询的列上使用
@Entity(indices = [Index("email")]),加速检索。 - 事务优化: 批量插入/更新时使用
@Transaction和suspend函数,保持原子性。 - 预打包数据库: 对于只读/低频更新的静态数据,使用
createFromAsset()或createFromFile()预填充数据库。 - 避免内存泄漏: 观察
LiveData或Flow时,在ViewModel或LifecycleOwner中注册,确保自动清理。
2.6 完整实现流程(从定义到运行)
- 定义Entity: 使用
@Entity,配置主键与索引,添加@ColumnInfo。 - 实现TypeConverter: 为复杂类型(
Date、List、自定义枚举)编写双射转换方法。 - 声明DAO: 使用
@Dao注解接口,定义suspend或LiveData方法。 - 配置Database: 继承
RoomDatabase,使用@Database注解并传入entities数组和版本号,挂载@TypeConverters。 - 构建实例: 使用
Room.databaseBuilder(context, AppDatabase::class.java, "app_database.db").build()获取单例。 - 执行操作: 通过
database.userDao()调用方法,在协程或后台线程中操作。
验证提示: 在测试环境中可以使用
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).allowMainThreadQueries().build() 快速验证DAO与转换器逻辑。生产环境务必移除 allowMainThreadQueries()。