Room 数据库基础配置:实体定义、DAO 接口、数据库升级、类型转换器、预填充

Room 是 Jetpack 官方提供的对象关系映射(ORM)持久化库,在 SQLite 之上构建了三层抽象:Entity(实体层)定义表结构,DAO(数据访问对象层)封装数据操作,Database(数据库层)管理实例与升级。本专题以原理、配置、调试、优化为主线,逐项拆解基础配置的核心实践。

核心原则:Room 在编译时通过注解处理器生成 _Impl 类,将 Entity ↔ 表的映射、DAO 方法 ↔ SQL 语句的绑定、Database ↔ 版本迁移全部自动化,大幅减少样板代码并保证类型安全。
Room 三层架构与扩展配置 @Entity User, Product… 表名 · 主键 · 列名 @Dao UserDao Insert · Update · Query @Database AppDatabase 版本 · 实体列表 @TypeConverter Date ↔ Long, List ↔ String 预填充数据库 createFromAsset / Callback 数据库升级 Migration / destructive App 层 Repository → ViewModel 全部在编译时生成实现代码

一、实体定义(Entity)

实体是用 @Entity 注解的 Kotlin / Java 数据类,每个实体映射一张 SQLite 表。Room 会自动将类的字段与表列对应,并提供 @PrimaryKey、@ColumnInfo、@Ignore 等控制注解。

@Entity(tableName = "user")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "full_name")
    val name: String,
    val age: Int,
    @Ignore
    val tempField: String? = null
)
  • 表名:省略 tableName 时默认使用类名(不区分大小写)。
  • 主键:必选一个字段标记 @PrimaryKey;复合主键需使用 primaryKeys 属性。
  • 列名:@ColumnInfo(name = "…") 显式指定;不指定则用字段名。
  • 忽略字段:不希望持久化的字段用 @Ignore 排除。
常见错误:实体类中必须至少有一个构造函数,且所有持久化字段要么在构造函数中声明,要么提供 setter/getter。Room 不支持非公开字段的直接访问。

二、DAO 接口(数据访问对象)

DAO 是定义数据库操作的接口,使用 @Dao 注解。Room 会根据注解和 SQL 语句自动生成实现。

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user WHERE id = :userId")
    suspend fun getUserById(userId: Int): User?

    @Query("SELECT * FROM user ORDER BY name ASC")
    fun getAllUsers(): Flow<List<User>>
}
DAO 方法类型对应 SQL返回值线程要求
@InsertINSERTLong / Unit / List<Long>IO 线程
@UpdateUPDATEInt(影响行数)/ UnitIO 线程
@DeleteDELETEInt / UnitIO 线程
@Query自定义 SQL任意类型 / Flow / LiveDataIO 线程(Flow 自动切换)
性能优化:返回 Flow<T> 的 DAO 方法会自动在数据变更时重新发送查询结果,无需手动刷新。避免在 DAO 中执行耗时 SQL(如复杂的 JOIN),可将这类查询放在数据库的 @Transaction 方法中。

三、Database 类与实例创建

数据库类需要继承 RoomDatabase,并用 @Database 注解声明实体列表和版本号。它是一个抽象类,通常作为单例提供。

@Database(
    entities = [User::class, Product::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun productDao(): ProductDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

重要参数:exportSchema 控制是否输出数据库 Schema JSON 到指定目录(用于版本迁移验证),开发时可设为 false,生产建议开启。

四、数据库升级(Migration)

当 @Database 中的 version 自增时,Room 会检查是否有匹配的 Migration 对象。若没有,默认抛出 IllegalStateException。常用策略有两种:

  1. 破坏性迁移(FallbackToDestructiveMigration):调用 fallbackToDestructiveMigration(),当版本不匹配时直接删除旧表重建。适用于开发 / 无重要数据的场景。
  2. 编写 Migration 对象:用 SQL 语句安全迁移数据。
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE user ADD COLUMN email TEXT")
    }
}

// 在 build() 前加入
.addMigrations(MIGRATION_1_2)
// 或使用破坏性降级
.fallbackToDestructiveMigration()
调试技巧:通过 Room.databaseBuilder(...).openHelperFactory(...) 可注册自定义 SQLiteOpenHelper,在迁移前后打印日志。Android Studio 的 Database Inspector 可直接查看当前数据库版本与内容。

五、类型转换器(TypeConverter)

Room 默认支持基本类型及其包装类、String、ByteArray、Date 等。若需存储自定义类型(如 List<String>、Date 等),必须编写 @TypeConverter。

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 fromListString(list: List<String>?): String? {
        return list?.joinToString(",")
    }
}

// 在 Database 类上声明全局类型转换器
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
类型存储 SQLite 类型转换方法示例
DateINTEGER (时间戳)Date ↔ Long
List<String>TEXT (逗号分隔)List ↔ String
枚举 (Enum)TEXT (名称) 或 INTEGER (序数)Enum ↔ String / Int
BitmapBLOBBitmap ↔ ByteArray
常见错误:类型转换器必须声明为 @TypeConverters(…) ) 的一个类,且其中的方法必须为 @TypeConverter(注意区分单复数)。Room 不会自动处理 null,转换函数需要兼容 null 参数。

六、预填充数据库

若应用首次安装时需要提供初始数据(如国家列表、字典),Room 支持从 asset 或文件系统预填充已有的 SQLite 数据库文件。

// 从 assets 目录预填充
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
    .createFromAsset("prepopulated.db")
    .build()

// 从文件系统预填充
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
    .createFromFile(File(context.filesDir, "my_data.db"))
    .build()

预填充文件必须与 @Database(entities=…) 中声明的所有表结构完全一致(列名、类型、约束),否则 Room 会在首次构建时崩溃。也可使用 RoomDatabase.Callback 在 onCreate 回调中执行初始 INSERT。

val callback = object : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        // 插入初始数据
        db.execSQL("INSERT INTO user (name, age) VALUES ('Admin', 30)")
    }
}

Room.databaseBuilder(…)
    .addCallback(callback)
    .build()
性能优化:预填充数据库的创建时机在首次访问数据库时(而非应用启动)。若初始数据量很大,建议将预填充文件放在 asset 中并使用 createFromAsset,Room 会复制文件而非逐条插入。Callback 方式适合数据量小(<100 条)的场景。

七、调试与常见错误汇总

问题原因解决方案
编译报错“Cannot figure out how to save this field”未提供类型转换器或字段类型不受支持添加 @TypeConverter 或改用可支持类型
运行时“Migration didn't properly handle”自定义 Migration 中的 SQL 未完全同步检查表结构差异,使用 exportSchema=true 生成 JSON 辅助对比
预填充后数据为空预填充文件版本号与 Database version 不匹配确保预填充文件的 schema 与当前 version 对应,或使用 .createFromAsset(…, "version1.db")
DAO 方法不能在主线程调用Room 默认禁止主线程数据库操作使用 suspend 函数 + 协程,或 .allowMainThreadQueries()(仅测试)
数据库文件无法打开SQLite 版本不兼容或文件损坏检查 context.databaseList(),尝试 .fallbackToDestructiveMigration()

调试利器:在运行时添加 Room.databaseBuilder(…).enableQueryLogging()(仅 debug 构建)可在 Logcat 中打印所有 SQL 语句。配合 Database Inspector 可直接浏览表内容、执行查询。

至此,Room 的五个基础配置步骤全部完成:实体定义映射表结构,DAO 封装数据操作,Database 管理版本与实例,类型转换器解决自定义类型存储,预填充提供启动数据。下一专题将深入介绍 Kotlin Coroutines 与 Room 的结合使用,解决线程调度与挂起问题。