Room 数据库基础:实体定义、DAO 接口、数据库类创建与配置
说到 Android 本地数据持久化,大多数项目最终都会选 Room。为什么?因为 SQLite 裸写太痛苦了——手写 SQL、类型转换、生命周期管理……全是脏活。Room 帮你包装了一层,还能配合 LiveData 或 Flow 做响应式监听。
我个人最早接触 Room 是在 Android 8.0 普及那阵,从 GreenDAO 迁移过来的。说实话,第一次见到编译时生成的代码,心里还挺踏实。你想想,一个框架能帮你把重复的 SQL 和类型转换都自动搞定,那可比手写 ContentProvider 省心多了。
实体定义:用 @Entity 描述数据表
实体就是一张表的 Java/Kotlin 类。每个字段对应一列,每个实例对应一行。简单吧?但细节不少。
先看一个典型例子:
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_name") val name: String,
val age: Int? = null,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
重点讲几个坑:
- 主键必填——每个实体至少要有一个 @PrimaryKey。如果你用复合主键,可以用
primaryKeys = ["field1", "field2"] - 字段名映射——默认 Kotlin 属性名就是列名。如果数据库里是下划线命名,用 @ColumnInfo 转换。我见过有人忘了加这个,上线后才发现查询结果全是 null,赶紧加了个迁移脚本。
- 可空性——字段声明为
Int? String?等可选类型,对应数据库允许 null。如果不加问号,Room 会默认生成 NOT NULL。
DAO 接口:用 @Dao 定义数据库操作
DAO(Data Access Object)是你的操作入口。Room 只认接口或抽象类,你别写成普通类,否则编译直接报错。
下面是一个典型的 UserDao:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getAllUsers(): Flow<List<User>>
}
几个要点:
- 挂起函数 vs 同步——想用协程就加
suspend,Room 会自动在后台线程执行。如果不加,就得手动放到 IO 线程。我建议所有写操作都用 suspend,免得 ANR。 - 返回值可以是 Flow 或 LiveData——返回 Flow 后,每次表数据变化都会自动发射新结果。这个特性和我们后面的回归测试数据一致性验证强相关。
- @Insert 的冲突策略——REPLACE、ABORT、IGNORE 三个选项中,我最常用 REPLACE。因为测试场景里经常要覆盖已有数据,用 REPLACE 省去先查后删的步骤。
- @Query 参数绑定——用冒号加变量名,千万别拼字符串。我见过有人直接写
"WHERE id = " + userId,那被 SQL 注入你还得自己扛。
insertUser(user: User) 和 insertUsers(users: List<User>),正确做法是用 vararg 或者分别命名。我那次因为用 List 和 Collection 混用,编译报了“Multiple methods match”错误,排查了半天才发现是集合类型泛型擦除的问题。
数据库类:用 @Database 连接一切
数据库类是 Room 的入口,一般声明为抽象类并继承 RoomDatabase:
@Database(
entities = [User::class, Post::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
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"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
这里需要上心的点:
| 配置项 | 说明 | 个人建议 |
|---|---|---|
entities | 列出所有实体类,一个也不能少 | 每个实体类必须显式声明,没列的直接报错 |
version | 数据库版本号,升级时递增 | 开发阶段可以用 destructive,上线一定要写 Migration |
exportSchema | 是否导出 Schema 到文件,配合 Migration 调试 | 建议开启,方便追踪表结构变化 |
fallbackToDestructiveMigration | 版本不匹配时删除旧表重建 | 只用于开发,生产环境禁止!数据丢了用户会骂死你 |
数据库初始化时我习惯用单例 + applicationContext,避免 Activity 销毁导致泄漏。另外别忘了,Room 默认不支持在主线程访问数据库,如果你非要那么干,加个 .allowMainThreadQueries(),但只推荐在测试中用。
Room 三要素关系总览
下面这张图帮你理清 Entity、DAO、Database 是怎么协作的:
说白了就是:Entity 定义结构,DAO 定义操作,Database 整合配置。三者缺一不可,少了任何一个 Room 就会在编译期报错给你看。
一点补充:编译时验证
Room 最让我喜欢的地方是编译时检查。比如你 @Query 里写了一个不存在的表名,或者 SQL 语法错误,IDE 会直接标红。想象一下,如果是裸写 SQLite,这类错误只能等到运行时才能发现,那测试成本就高了。
在回归测试场景里,这个特性尤其重要。你修改了实体字段,忘了改 DAO 的查询语句,Room 会直接拒绝编译。注意,这里的“拒绝”是指编译失败,不是警告。所以你的 CI 阶段就能抓住问题,而不是等到人工测试才发现。