3、Room数据库入门:Room的配置、实体、DAO、数据库类创建,以及基本CRUD操作
聊到离线优先,绕不开本地持久化。Android 平台上,Room 就是官方钦定的本地数据库方案。说白了,它是 SQLite 的一个轻量级封装,让咱不用写一堆 Cursor、ContentValues,直接操作 Kotlin 对象就行。
我刚开始做离线功能时,还真踩过裸写 SQLite 的坑。手拼 SQL 语句,查出来的数据还要手动转成对象……维护起来简直噩梦。后来切到 Room,代码量直接砍半,编译期还能检查 SQL 语法错误,舒服多了。
3.1 添加Room依赖:Gradle配置
先在项目的 build.gradle.kts(或者 .gradle)里配上 Room 的依赖。我个人习惯用 KSP 替代 KAPT,编译速度更快。
记得 Room 版本号统一,不然容易出兼容性问题。
// build.gradle.kts (Module :app)
plugins {
id("com.google.devtools.ksp") version "1.9.0-1.0.13" // 根据你的 Kotlin 版本调整
}
android {
// ... 其他配置
}
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version")
ksp("androidx.room:room-compiler:$room_version")
}
为什么用 KSP?它比 KAPT 少生成很多中间代码,编译时间能省 30% 左右。嗯,项目大了就能感受到差距。
3.2 定义实体(Entity):让数据变成表
Entity 就是 Room 里的「表定义」。拿一个最简单的 User 类来举例:
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String,
val age: Int
)
@Entity标注这个类是一个数据库表。tableName可指定表名,默认用类名。@PrimaryKey必须指定主键。autoGenerate = true让 Room 自动生成自增 ID。- 每个属性对应一列,类型会自动映射到 SQLite 类型。
我建议给 ID 字段默认值 0,插入时不传 ID,Room 会自动生成。曾经有个同事忘了给默认值,插入时总报空指针……后来排查了半天。
3.3 创建DAO接口:数据访问对象
DAO 是操作数据库的「门面」。它是一个接口,用注解声明 SQL 操作。
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY id DESC")
suspend fun getAllUsers(): List<User>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
| 注解 | 对应SQL | 返回值 |
|---|---|---|
@Insert | INSERT | Long(新ID)或 Unit |
@Update | UPDATE | Int(影响行数) |
@Delete | DELETE | Int(影响行数) |
@Query | 自定义SQL | 任意类型 |
所有 DAO 方法我都习惯加上 suspend,方便在协程里直接调用。不过要注意,Room 的查询如果返回 LiveData 或 Flow,就不需要 suspend,因为它们是响应式的。
小心事务问题: 多个写操作如果互相关联,最好用 @Transaction 包裹。我曾经把一个批量插入拆成了循环调用 insert,结果中途崩溃导致数据不完整……后来老老实实用 @Transaction 统一处理。
3.4 构建Database类:数据库中枢
Database 类是一个抽象类,继承 RoomDatabase。它是整个数据库的「单例入口」。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
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
}
}
}
}
@Database声明数据库包含哪些 Entity,以及版本号。别忘了entities数组里列出所有 Entity 的 class。version用于数据库迁移,后面会专门讲。exportSchema设为 false 可以关闭 schema 导出(调试时可设为 true)。- 单例模式避免多次创建数据库实例,防止内存泄漏。
我习惯在 Application 层初始化数据库,用 applicationContext 传参,别传 Activity 的 Context,否则可能泄漏。
3.5 基本CRUD操作:动动手
现在 Entity、DAO、Database 都齐了,写个简单的使用示例:
// 在 ViewModel 或 Repository 里
class UserRepository(private val userDao: UserDao) {
suspend fun addUser(name: String, email: String, age: Int): Long {
val user = User(name = name, email = email, age = age)
return userDao.insertUser(user)
}
suspend fun getAll(): List<User> {
return userDao.getAllUsers()
}
suspend fun updateUser(user: User) {
userDao.updateUser(user)
}
suspend fun deleteUser(user: User) {
userDao.deleteUser(user)
}
}
调用时记得在协程里跑:
viewModelScope.launch {
val id = repository.addUser("张三", "zhangsan@example.com", 28)
Log.d("RoomDemo", "插入成功,ID = $id")
}
你想想看,如果不用 Room,这得写多少 try-catch 和 Cursor 循环?Room 帮我们把数据库操作简化成了 suspend 函数,爽快多了。
3.6 完整结构一览
为了让你更直观地理解 Room 各组件的关系,我画了一张图:
从图里能看出来,App 层调用 DAO,DAO 通过 Database 拿到 Entity 对应的表操作,底层依然是 SQLite。整个链路清晰,耦合低。
3.7 避坑小总结
曾经我犯过的错:
- 忘记在
@Database的 entities 里注册所有 Entity,导致运行时报错表不存在。 - DAO 方法里用了
suspend,但调用时忘记在协程里执行,直接挂起线程……然后 ANR 了。 - 数据库版本更新后,没有写 Migration,直接
fallbackToDestructiveMigration(),结果生产环境数据全丢了。现在老老实实写 Migration。
调试时,可以在 Room.databaseBuilder 中加一句 .allowMainThreadQueries(),但仅限测试!线上千万别用。
好了,Room 的基础配置、实体、DAO、Database 以及 CRUD 操作都在这里了。接下来就是把这些知识点串进项目里,配合 Compose 的 ViewModel 和 Flow,离线的数据流就能跑起来了。