4. Room 数据库核心组件配置:Entity、Dao、Database、类型转换器、迁移
Room 是 Android 官方持久化库,在 SQLite 之上提供编译时校验的抽象层。核心组件包括 Entity(数据表映射)、Dao(数据访问对象)、Database(数据库容器)、类型转换器(自定义类型支持)和 迁移(模式演进)。本章从原理出发,逐步讲解每个组件的配置流程、关键 API 及常见陷阱。
4.1 组件职责与依赖关系
| 组件 | 职责 | 关键注解 / 基类 |
|---|---|---|
| Entity | 定义数据库表结构与列约束 | @Entity, @PrimaryKey, @ColumnInfo |
| Dao | 提供对表的增删改查方法,支持协程挂起 | @Dao, @Insert, @Query, suspend |
| Database | 持有 DAO 实例,管理数据库创建与版本 | @Database, Room.databaseBuilder() |
| TypeConverter | 将非基本类型(如 Date、List)映射为 SQLite 可存储类型 | @TypeConverter, @TypeConverters |
| Migration | 处理数据库版本升级时表结构变更 | Migration(startVersion, endVersion) |
4.2 Entity:表映射与列约束
Entity 类中的每个字段对应数据库表的一列(忽略带有 @Ignore 注解的字段)。必须指定 @PrimaryKey,可设置 autoGenerate = true。常用配置:
@Entity(
tableName = "user",
indices = [Index(value = ["email"], unique = true)],
foreignKeys = [ForeignKey(
entity = Address::class,
parentColumns = ["id"],
childColumns = ["address_id"],
onDelete = ForeignKey.CASCADE
)]
)
data class User(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "full_name") val name: String,
val email: String,
@ColumnInfo(name = "created_at") val createdAt: Long?,
val addressId: Long?,
@Ignore val tempField: String? = null
)
要点:使用
@ColumnInfo(name = ...) 自定义列名,避免与 Kotlin 关键字冲突;@Ignore 标记不持久化的字段;索引可加速查询但会降低写入性能。
4.3 Dao:协程友好的数据访问
Dao 推荐使用 suspend 函数或 Flow 返回值以配合协程。注解支持 SQL 与 Room 编译期校验。
@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 user WHERE id = :id")
suspend fun getUserById(id: Long): User?
@Query("SELECT * FROM user ORDER BY created_at DESC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM user WHERE email LIKE :email")
suspend fun getUserByEmail(email: String): User?
}
4.4 Database:单例与构建
Database 类必须是抽象类并继承 RoomDatabase,通过 @Database 声明实体和版本号。构建时应使用单例模式避免资源泄漏。
@Database(
entities = [User::class, Address::class],
version = 2,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun addressDao(): AddressDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database.db"
)
.addTypeConverter(Converters)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration() // 仅用于开发阶段
.build()
INSTANCE = instance
instance
}
}
}
}
避坑:禁止在非数据库线程上调用 DAO 方法。Room 默认不允许在主线程操作数据库(除非显式调用
.allowMainThreadQueries(),但仅用于测试)。协程的 Dispatchers.IO 是标准选择。
4.5 类型转换器:自定义类型的序列化
SQLite 仅存储 NULL、INTEGER、REAL、TEXT、BLOB。当实体字段包含 Date、List<String> 等类型时,需要类型转换器。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromStringList(value: String?): List<String>? {
return value?.split(",")?.map { it.trim() }
}
@TypeConverter
fun toStringList(list: List<String>?): String? {
return list?.joinToString(",")
}
}
在 Database 类上或实体字段级别添加 @TypeConverters 注解:
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
4.6 迁移:版本升级与数据保全
迁移通过实现 Migration 对象并编写 ALTER TABLE 或 CREATE TABLE SQL 语句完成。必须提供 startVersion 和 endVersion。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0"
)
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `address` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `city` TEXT NOT NULL)"
)
database.execSQL(
"ALTER TABLE user ADD COLUMN address_id INTEGER REFERENCES address(id) ON DELETE CASCADE"
)
}
}
构建时注册迁移:
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
调试技巧:在
build.gradle.kts 中启用 schema 导出 kapt { arguments { arg("room.schemaLocation", "$projectDir/schemas") } },可在 schemas/ 目录下查看版本对比 JSON,帮助验证迁移正确性。
4.7 常见错误与性能优化
- 错误:“Migration didn't properly handle: ……” —— 迁移的
startVersion/endVersion与数据库版本不匹配。每次升级必须写至少一个迁移,且版本号递增不可跳跃。 - 错误:“Cannot figure out how to save this field into database” —— 缺少对应类型的 TypeConverter。检查实体字段类型是否都被覆盖。
- 性能优化:为频繁查询的列建立索引;DAO 的
Flow查询会自动在数据变化时重新发射,避免手动刷新;@Transaction注解保证多个 DAO 操作原子性。 - 稳定性:单例 Database 实例防止多线程重复创建;避免在
Application.onCreate中初始化耗时数据库,可改用AppStartup或懒加载。
4.8 完整配置清单(检查项)
| 步骤 | 操作 | 常见遗漏 |
|---|---|---|
| 1 | 定义 Entity 并添加 @Entity、@PrimaryKey | 未设置 autoGenerate 或未处理 nullable |
| 2 | 创建 Dao 接口,使用 suspend 或 Flow | 在主线程调用未处理异步 |
| 3 | 编写 Database 抽象类,声明 entities 列表和版本 | 忘记在 @Database 中列出所有 Entity |
| 4 | 为自定义类型添加 TypeConverter 类 | 只添加了单向转换 |
| 5 | 编写 Migration 对象,注册到 builder | 没有安装迁移,使用 fallbackToDestructiveMigration() 会导致数据丢失 |
实战建议:在开发阶段使用
fallbackToDestructiveMigration() 可快速迭代表结构,但发布前必须移除该调用并编写完整的迁移代码。始终在 Pre-populated database 场景中测试迁移逻辑。