Room 三剑客:Entity、DAO 与 Database
说实话,Room 这个库我挺喜欢的。它不是那种给你一堆新概念让你重新学一遍的东西,反而更像是在 Android 本地存储领域做了一个「标准化」的事。说白了,就是把我们以前写 SQLite 那套原始操作,用注解的方式包装得干干净净。
Entity、DAO、Database,这就是 Room 的三个核心部件。我管它们叫「三剑客」。你搞懂了这三者的关系和职责,Room 的基本功就算扎实了。下面我挨个聊聊。
1. Entity:你的数据模型
Entity 就是数据库里的表。在 Room 里,一个 Kotlin 数据类配上 @Entity 注解,就能映射成一张表。我曾经见过刚接触 Room 的同事把 Entity 当成普通的 Bean 来写,结果字段类型乱用,导致运行时崩溃。嗯,这里得注意。
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val age: Int,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
几个关键点我提一下:
- 主键:每个 Entity 必须有一个
@PrimaryKey。支持单个字段或复合主键(用primaryKeys属性)。 - 字段映射:Kotlin 的字段名默认就是列名。想改名?用
@ColumnInfo指定。 - 忽略字段:不想存到数据库的字段,加
@Ignore注解就行。我一般在 Entity 里放一些临时计算用的字段就会用到它。
2. DAO:数据操作的契约
DAO 是 Data Access Object 的缩写,我更喜欢叫它「数据访问层」。它是一个接口,你用注解声明增删改查的方法,Room 在编译期帮你生成实现代码。这不是运行时反射,是编译期代码生成,所以性能很不错。
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): UserEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity): Long
@Update
suspend fun updateUser(user: UserEntity)
@Delete
suspend fun deleteUser(user: UserEntity)
}
这里要说的东西不少。
- @Query:最灵活的注解。写原生 SQL,Room 会在编译期校验语法。我遇到过因为表名拼写错误导致编译失败的,嗯,这种错总比运行时炸掉好。
- @Insert:支持传入单个对象或集合。冲突策略我常用
ON_REPLACE,省得单独处理去重。 - @Update 和 @Delete:根据主键匹配。如果你只传了一个 UserEntity 但主键不对应任何记录,更新和删除不会报错,但也不会生效。这个要注意。
suspend 函数但忘了加协程依赖,结果 Room 直接编译不过。记住了,Room 的 DAO 方法支持 suspend 函数,但前提是你的项目引入了 kotlinx-coroutines 依赖。
3. Database:数据库的入口
Database 是一个抽象类,继承 RoomDatabase。它把 Entity 和 DAO 串起来,还负责版本管理、迁移、创建等任务。
@Database(
entities = [UserEntity::class, AddressEntity::class],
version = 1,
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,
"my_app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
几个重点:
- entities 数组:列出所有 Entity 类。Room 会根据这些类创建对应的表。
- version:数据库版本号。每次改动表结构都要加 1,并配合 Migration 或 fallbackToDestructiveMigration 使用。
- exportSchema:设为 true 会在编译期输出数据库 schema 的 JSON 文件。这个文件在版本迁移时特别有用,我建议开启。
- 单例模式:Database 实例建议用单例。创建开销不小,多个实例还可能引发并发问题。
4. 三者如何协作?
你想想看,整个流程是这样的:
- 你定义 Entity 来描述数据模型和表结构。
- 你定义 DAO 接口来声明对表的操作。
- 你创建 Database 抽象类,把 Entity 和 DAO 注册进去。
- 使用 Database 实例获取 DAO,然后调用 DAO 的方法操作数据。
Room 在编译期会做三件事:生成 DAO 的实现类、创建表结构、校验 SQL 语法。这个过程我挺放心的,毕竟编译期能抓到的错,总比运行时抓要好。
5. 知识结构速览
下面这个表格总结了三剑客的核心要点,方便你快速回顾:
| 组件 | 职责 | 关键注解/方法 | 我的建议 |
|---|---|---|---|
| Entity | 映射数据库表结构 | @Entity, @PrimaryKey, @ColumnInfo, @Ignore | 字段类型尽量简单,避免复杂对象 |
| DAO | 定义数据操作接口 | @Insert, @Update, @Delete, @Query | 多用 suspend 函数,配合协程使用 |
| Database | 数据库的创建与管理 | @Database, Room.databaseBuilder, Migration | 单例模式创建,开启 exportSchema |
6. Room 三剑客协作流程图
下面这张图帮你理清三者的关系和协作流程。看完应该能对 Room 的整体结构有个直观认识。
7. 避坑指南
最后,分享几个我摸爬滚打换来的教训:
- Entity 字段不要用
Int存时间戳?我建议用Long,否则 2038 年你会后悔的。 - DAO 里的
@Query尽量写具体的列名,别用SELECT *。为什么?因为表结构变更时,列名冲突很难排查。 - Database 版本升级一定写 Migration。有一次我直接 fallbackToDestructiveMigration,用户数据全丢了……从那以后我再也不敢偷懒了。
Room 的三剑客就是这么回事。从 Entity 到 DAO 再到 Database,环环相扣,结构清晰。你只要把它们之间的关系搞懂了,后面的离线架构、数据同步、版本迁移,学起来都会顺很多。