15、Room数据库:从入门到实战
Room 这个组件,说实话刚出来的时候我还有点抵触。毕竟 SQLite 用了那么多年,突然来个「官方 ORM」,总觉得多此一举。直到我在一个项目里被 CursorLoader 和 SQLiteOpenHelper 的模板代码折磨得够呛……嗯,真香了。
今天我们就来聊聊 Room。它本质上是对 SQLite 的一层封装,但封装得恰到好处。你不需要写一堆重复的代码,也不用担心线程安全问题。我个人觉得,它是 Google 近几年做得最靠谱的库之一。
Room 的核心组件
Room 由三部分组成:Entity、Dao 和 Database。这三者缺一不可,配合起来就像一套完整的 MVC 架构。
| 组件 | 作用 | 类比 |
|---|---|---|
| Entity | 定义数据表结构 | Java Bean + 表映射 |
| Dao | 定义数据库操作 | SQL 语句的接口 |
| Database | 数据库持有者 | SQLiteOpenHelper 的替代 |
核心思想:Room 在编译期会检查你的 SQL 语句是否正确。这个特性帮我省了不少事——以前写 SQLite 时,运行时才发现 SQL 写错了,那叫一个尴尬。
Entity:定义数据表
Entity 说白了就是一个普通的 Kotlin 数据类,加上一些注解。每个 Entity 对应一张表,每个字段对应一列。
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "user_name")
val name: String,
@ColumnInfo(name = "age")
val age: Int,
@ColumnInfo(name = "email")
val email: String? = null
)
这里有几个点要注意:
- @PrimaryKey 是必须的。每个表都要有主键,这是数据库的基本要求。
- autoGenerate = true 表示自增。我习惯用 Long 类型做主键,省心。
- @ColumnInfo 可以自定义列名。如果你后端接口返回的是 user_name,这里就对应上。
- 字段类型支持基本类型、String、Date 等。复杂类型需要用 @TypeConverter 转换。
小技巧:如果你不想某个字段存到数据库,用 @Ignore 注解。比如一个临时计算的字段,不需要持久化。
Dao:数据库操作的接口
Dao 是一个接口,里面定义了你对数据库的所有操作。Room 会在编译期自动生成实现类。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User): Long
@Update
fun updateUser(user: User)
@Delete
fun deleteUser(user: User)
}
我个人比较喜欢 @Insert 的 onConflict 参数。设置成 REPLACE 后,如果主键冲突就直接覆盖,省得先查再插。
@Query 里写的是原生 SQL。Room 会在编译期解析这些 SQL,如果表名或字段名写错了,编译直接报错。这个特性我太爱了——以前运行时才发现 SQL 写错,调试起来特别痛苦。
注意:Dao 里的方法默认在调用线程执行。如果你在 UI 线程调用,Room 会抛异常。解决办法是用 LiveData 或协程。
Database:数据库的入口
Database 是一个抽象类,继承自 RoomDatabase。它定义了数据库的版本、包含哪些 Entity,以及提供 Dao 的实例。
@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
}
}
}
}
这里有个经典的单例模式。为什么要用单例?因为创建数据库实例的开销不小,而且多个实例会导致数据不一致。@Volatile 和 synchronized 保证了线程安全。
exportSchema 建议设为 false,除非你需要做数据库迁移的版本控制。我一般只在正式项目里才开这个。
与 LiveData 结合:响应式查询
这才是 Room 的杀手锏。把 Dao 的返回值改成 LiveData,数据变化时 UI 会自动更新。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsersLiveData(): LiveData<List<User>>
}
在 ViewModel 里这样用:
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao = AppDatabase.getDatabase(application).userDao()
val allUsers: LiveData<List<User>> = userDao.getAllUsersLiveData()
}
然后在 Activity 或 Fragment 里观察:
viewModel.allUsers.observe(this) { users ->
// 更新 UI,比如刷新 RecyclerView
adapter.submitList(users)
}
为什么会这么方便?因为 Room 在后台维护了一个观察者模式。当数据库中的数据发生变化时,它会自动通知所有观察者。你不需要手动刷新,也不需要写 ContentObserver。
我记得有一次,我在一个聊天应用里用了这个特性。新消息插入数据库后,聊天列表自动刷新,完全不用写额外的通知代码。那感觉,就像写前端 Vue 一样爽。
避坑指南:LiveData 的回调是在主线程执行的。所以不要在回调里做耗时操作。如果你需要做复杂的数据处理,建议用 Transformations.map 或 switchMap 转换一下。
知识体系总览
下面这张图概括了 Room 的整体架构和数据流向。我画的时候特意把 LiveData 的观察机制也加进去了,这样你就能理解为什么数据变化后 UI 会自动更新。
实战建议
最后分享几个我在项目中积累的经验:
- 数据库版本升级:用 Migration 类处理。不要删表重建,用户数据丢了会骂娘的。
- 测试:Room 支持内存数据库,测试时用 Room.inMemoryDatabaseBuilder,速度快,用完即毁。
- 复杂查询:如果 SQL 太复杂,可以考虑用 RawQuery。但要注意,Room 不会检查 RawQuery 的语法。
- 性能:批量插入用 @Insert 的 List 参数,比循环插入快一个数量级。
我的习惯:每个 Entity 都加一个 createdAt 和 updatedAt 字段。虽然 Room 不会自动维护,但手动更新一下,排查问题时会很有帮助。
好了,Room 的核心内容就这些。说白了,它就是让你用更少的代码、更安全的方式操作 SQLite。配合 LiveData 使用后,你会发现数据驱动的 UI 写起来特别顺手。