Room 数据库基础配置:实体定义、DAO 接口、数据库升级、类型转换器、预填充
Room 是 Jetpack 官方提供的对象关系映射(ORM)持久化库,在 SQLite 之上构建了三层抽象:Entity(实体层)定义表结构,DAO(数据访问对象层)封装数据操作,Database(数据库层)管理实例与升级。本专题以原理、配置、调试、优化为主线,逐项拆解基础配置的核心实践。
_Impl 类,将 Entity ↔ 表的映射、DAO 方法 ↔ SQL 语句的绑定、Database ↔ 版本迁移全部自动化,大幅减少样板代码并保证类型安全。
一、实体定义(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排除。
二、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 | 返回值 | 线程要求 |
|---|---|---|---|
| @Insert | INSERT | Long / Unit / List<Long> | IO 线程 |
| @Update | UPDATE | Int(影响行数)/ Unit | IO 线程 |
| @Delete | DELETE | Int / Unit | IO 线程 |
| @Query | 自定义 SQL | 任意类型 / Flow / LiveData | IO 线程(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。常用策略有两种:
- 破坏性迁移(FallbackToDestructiveMigration):调用
fallbackToDestructiveMigration(),当版本不匹配时直接删除旧表重建。适用于开发 / 无重要数据的场景。 - 编写 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 类型 | 转换方法示例 |
|---|---|---|
| Date | INTEGER (时间戳) | Date ↔ Long |
| List<String> | TEXT (逗号分隔) | List ↔ String |
| 枚举 (Enum) | TEXT (名称) 或 INTEGER (序数) | Enum ↔ String / Int |
| Bitmap | BLOB | Bitmap ↔ 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()
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 的结合使用,解决线程调度与挂起问题。