Room数据库(上):架构、注解与响应式查询
各位同学,今天我们来聊聊Room。说实话,Room是我在Android开发中最喜欢的库之一。它不像某些框架那样「过度设计」,也不像裸写SQLite那样原始。它恰到好处地解决了本地持久化的问题。
我最早接触Room是在2017年,那时候项目里还在用GreenDAO。说实话,每次写实体类都要生成一堆代码,烦得很。后来Google推出了Room,我试用了一下,嗯,真香。尤其是编译时校验SQL语法这个特性,帮我省了不少线上bug。
1. Room的整体架构
Room本质上是对SQLite的抽象封装。它分为三层:
- Entity(实体层):对应数据库中的表
- DAO(数据访问层):定义增删改查操作
- Database(数据库层):持有数据库连接,提供DAO实例
这三层的关系,我画了张图,你一看就明白:
你看,这个架构其实很清晰。App层只需要跟Database打交道,拿到DAO之后直接操作,完全不用关心底层SQLite的细节。我个人觉得,这种分层设计最大的好处是——测试起来特别方便。你可以轻松mock DAO,写单元测试。
2. 核心注解详解
2.1 @Entity — 定义表结构
每个@Entity注解的类,对应数据库中的一张表。我见过不少新手把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 = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
这里有几个要点:
- tableName:指定表名,不写的话默认用类名
- @PrimaryKey:主键,autoGenerate = true 表示自增
- @ColumnInfo:指定列名,不写的话默认用字段名
2.2 @Dao — 数据访问对象
DAO是接口或者抽象类。你只需要定义方法,Room会在编译时自动生成实现代码。我刚开始用的时候还担心性能问题,后来看了生成的代码,嗯,Room生成的SQLite操作代码非常高效。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
@Update
fun updateUser(user: User)
@Delete
fun deleteUser(user: User)
}
注意看@Insert里的onConflict = OnConflictStrategy.REPLACE。这个参数很实用——当主键冲突时,是替换还是忽略?我曾经在项目中遇到过一个问题:用户重复注册时,如果不加这个参数,会直接崩溃。后来我统一加上了REPLACE策略,世界清净了。
2.3 @Database — 数据库入口
Database是一个抽象类,需要继承RoomDatabase。它负责创建数据库实例,并提供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 getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database.db"
).build()
INSTANCE = instance
instance
}
}
}
}
@Volatile和synchronized。为什么?因为数据库实例应该是全局唯一的,否则会创建多个连接,导致数据不一致。我见过有人忘了加同步,结果在并发场景下数据库被重复创建,数据全乱了。
3. 数据库迁移策略
App迭代过程中,数据库表结构肯定会变。比如加个字段、改个类型什么的。Room提供了两种迁移方式:
3.1 自动迁移(推荐)
从Room 2.4.0开始,支持自动迁移。你只需要在@Database注解里声明:
@Database(
entities = [User::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() { ... }
但自动迁移只支持简单的场景,比如新增字段、删除字段。如果涉及到表的重构,还是得手动写。
3.2 手动迁移
手动迁移需要实现Migration类:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
}
}
// 在构建数据库时添加
Room.databaseBuilder(context, AppDatabase::class.java, "app_database.db")
.addMigrations(MIGRATION_1_2)
.build()
.fallbackToDestructiveMigration()作为兜底策略,但生产环境慎用。
4. TypeConverter — 处理复杂类型
Room默认只支持基本类型和String。如果你要存Date、List、Map这些复杂类型,就得用TypeConverter。
举个例子,存一个日期:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
// 在Database上注册
@Database(entities = [User::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
你想想看,如果没有TypeConverter,每次存Date都得手动转Long,取出来再转回来,多麻烦。Room帮你自动完成了这个转换。
我项目中还遇到过存List
class StringListConverter {
@TypeConverter
fun fromStringList(value: List<String>?): String? {
return value?.joinToString(",")
}
@TypeConverter
fun toStringList(value: String?): List<String>? {
return value?.split(",")?.map { it.trim() }
}
}
5. 响应式查询:Flow与LiveData
这是Room最强大的特性之一。你不需要手动刷新数据,Room会在数据库发生变化时自动通知UI。
5.1 使用LiveData
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsersLiveData(): LiveData<List<User>>
}
在ViewModel里观察:
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao = AppDatabase.getInstance(application).userDao()
val allUsers: LiveData<List<User>> = userDao.getAllUsersLiveData()
}
5.2 使用Flow(推荐)
如果你用Kotlin协程,Flow是更好的选择。它天然支持协程,而且生命周期更可控。
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsersFlow(): Flow<List<User>>
}
在ViewModel里:
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao = AppDatabase.getInstance(application).userDao()
val allUsers: StateFlow<List<User>> = userDao.getAllUsersFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
为什么我推荐Flow?说白了,LiveData在非UI层使用有点别扭,而且不支持背压。Flow跟协程配合得天衣无缝,你可以在数据流上做各种变换,比如map、filter、combine,非常灵活。
6. 总结
Room的核心思想其实很简单:用注解定义结构,用DAO封装操作,用Database管理连接。再加上TypeConverter处理复杂类型,Flow/LiveData实现响应式查询,一套完整的本地持久化方案就出来了。
嗯,这一章的内容就到这里。记住,Room的编译时校验是你最好的朋友——写错SQL?编译直接报错,不会等到运行时才崩溃。这个特性,我用过的都说好。
公众号:蓝海资料掘金营,微信deep3321