2、Room核心组件:Entity、DAO、Database的概念与职责
Room 说白了就是一个在 Android 上帮你管理本地数据库的框架。它把 SQLite 的繁琐操作包了一层,让你用注解就能定义表、写查询。
但光知道怎么用不够——你得先理解它的三大核心组件:Entity、DAO、Database。这三个东西各管一摊,缺一个 Room 就跑不起来。
2.1 Entity:它就是一张表
Entity 是 Room 里最基础的数据模型。你定义一个数据类,加上 @Entity 注解,Room 就会为它在 SQLite 里创建一张表。每个字段(非静态、非 transient)默认对应一列。
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
@ColumnInfo(name = "email_address") val email: String?
)
说几个我踩过的坑:
- 主键必须有。你不写
@PrimaryKey编译就报错。如果需要复合主键,用@PrimaryKeys({"col1", "col2"})。 - 字段默认必须可空,或者给默认值。否则插入时 Room 会抱怨列不能为 null。
- 我习惯给每个表加一个
createdAt和updatedAt字段,用@ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")自动填充。
💡 提示:如果字段名和列名不一致,务必用 @ColumnInfo(name = "...") 映射。我见过有人直接改了 Kotlin 属性名没加注解,结果数据库里旧数据全丢了。
2.2 DAO:你的 SQL 管家
DAO(Data Access Object)把所有数据库操作封装起来。你定义一个接口,在方法上写 @Query、@Insert、@Update、@Delete,Room 编译时自动生成实现代码。
@Dao
interface UserDao {
@Insert
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 name ASC")
fun getAllUsers(): Flow<List<User>>
}
注意几点:
- 尽量用挂起函数(suspend)或返回 Flow/LiveData。Room 会自动在后台线程执行。
- @Query 里的 SQL 语句编译时就会检查——表名、字段名写错了直接报错。这是个强大的提前发现问题的手段。
- 我以前在一个项目里把所有查询挤在一个 DAO 里,后来改需求时找个方法要找半天。后来我养成习惯:每个 Entity 单独一个 DAO 文件。
⭐ 重点:DAO 方法的返回值如果是 Flow,Room 会在表数据变化时自动重新发送最新数据——这也是 Room 和 Compose 状态同步的基础。
2.3 Database:整个数据库的大门
Database 是一个抽象类,继承自 RoomDatabase。它通过注解声明所有 Entity 和 DAO,并提供一个单例方法供应用获取实例。
@Database(
entities = [User::class, Address::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 getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database.db"
).build()
INSTANCE = instance
instance
}
}
}
}
这里有几个关键点:
- entities 列表必须包含所有使用到的 Entity 类,漏一个编译报错。
- version 号:每次改表结构(增删字段、改约束)都必须升版本,并配合 Migration 或 fallbackToDestructiveMigration。
- exportSchema = true:会在 build 目录生成一个 JSON 文件记录历史版本 schema,方便迁移调试。我建议线上版本打开,开发阶段随意。
- 数据库实例应该是单例,避免多次创建导致资源浪费。我一般用双重检查锁定(就像上面那样)。
⚠️ 警告:千万不要在 getDatabase() 里传 Activity 或 Fragment 的 context——这会导致内存泄漏。请使用 applicationContext。
2.4 三个组件如何协同工作
看下面的流程就清楚了:
- 你调用
AppDatabase.getDatabase(context).userDao().getAllUsers() - Room 拿到 Database 实例,再拿到 UserDao 的实现(编译生成的对象)。
- DAO 执行 SQL 查询,把结果集的每一行映射回 User Entity 对象。
- 如果返回的是 Flow,Room 还会监听 users 表的变化,一旦有 insert/update/delete 就重新发射数据。
说白了,Entity 定义形状,DAO 定义操作,Database 定义环境。三者缺一不可。
📌 个人习惯:我通常在一个模块(比如 :data)里放一个 database 包,里面按功能分三个子包:entity、dao、database,再加一个 migration 包放版本迁移脚本。这样结构清晰,新同事一看就懂。
2.5 关于代码生成的细节
Room 在编译时做了很多事。我遇到过一个问题:我改了 Entity 的字段名,但忘了改 DAO 里的 @Query 列名,结果运行时崩溃——因为编译时 Room 只检查了 Entity 注解,没检查 SQL 字符串里的列名(直到运行时才发现)。
怎么避免?写单元测试!Room 官方提供了 Room.inMemoryDatabaseBuilder 来创建内存数据库,你可以直接在测试里跑 DAO 方法验证。
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: UserDao
@Before
fun createDb() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
dao = db.userDao()
}
@After
fun closeDb() = db.close()
@Test
fun insertAndQuery() = runTest {
val user = User(1, "Alice", 30, "alice@example.com")
dao.insertUser(user)
val result = dao.getUserById(1)
assertEquals(user, result)
}
}
嗯,这里要特别注意:数据库迁移是 Room 最容易被忽略的坑。版本升级时如果没写 Migration,Room 会直接抛 IllegalStateException。我建议从第一个版本就把 exportSchema = true 打开,然后每次改版都写一个 Migration 类。
| 组件 | 注解/基类 | 核心职责 | 编译时产物 |
|---|---|---|---|
| Entity | @Entity | 定义表结构、列、主键、索引 | 普通数据类(无特殊) |
| DAO | @Dao | 封装 CRUD + 自定义查询 | DAO_Impl 实现类 |
| Database | RoomDatabase | 管理连接、版本、DAO 获取 | Database_Impl 实现类 |
最后再啰嗦一句:Room 和 SQLiteOpenHelper 的区别就在于它帮你做了大量代码生成和编译时检查。我用原生的 SQLiteOpenHelper 写过几个项目,每次改表结构都要手动改 SQL 和 Cursor 映射,还容易出 bug。换了 Room 之后,幸福感提升不少——当然,前提是你得把 Entity、DAO、Database 这三件套理解透。