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)
Room 核心组件配置流程 Entity @Entity Dao @Dao + suspend Database @Database TypeConverter @TypeConverter Migration Migration(1,2) @TypeConverters 注册 Room.databaseBuilder() .addTypeConverter(Converters) .addMigrations(MIGRATION_1_2).build()

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 场景中测试迁移逻辑。