Room 数据库的核心机制
Room 是什么
Room 是 Jetpack 提供的 SQLite ORM(对象关系映射)库。它在 SQLite 上架了一层抽象:
- 开发者:定义数据类和接口,写 SQL 查询
- Room:在编译期生成 DAO 实现、验证 SQL 语法、处理线程切换
最关键的优势是编译期 SQL 验证:SQL 写错了,编译就不过,不会等到运行时崩溃。
三个核心组件
Room 由三部分构成:
Entity(数据表)
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_name") val name: String,
val email: String,
@Ignore val token: String = "" // 不存入数据库的字段
)
@Entity:标记这个类对应一张数据表@PrimaryKey:主键,autoGenerate = true代表自增@ColumnInfo:自定义列名(不写则用字段名)@Ignore:该字段不映射到数据库列
关系型数据(一对多):
// 一个 User 拥有多个 Post
data class UserWithPosts(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val posts: List<Post>
)
DAO(数据访问对象)
DAO 是接口或抽象类,Room 在编译期生成实现。
@Dao
interface UserDao {
// 插入,冲突策略:替换旧数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long // 返回插入的行 ID
// 批量插入
@Insert
suspend fun insertAll(users: List<User>)
// 更新(根据主键匹配)
@Update
suspend fun updateUser(user: User)
// 删除
@Delete
suspend fun deleteUser(user: User)
// 查询所有用户,返回 Flow(自动在数据变化时通知)
@Query("SELECT * FROM users ORDER BY user_name ASC")
fun getAllUsers(): Flow<List<User>>
// 条件查询
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
// 模糊搜索
@Query("SELECT * FROM users WHERE user_name LIKE '%' || :name || '%'")
fun searchUsers(name: String): Flow<List<User>>
// 跨表关联查询
@Transaction
@Query("SELECT * FROM users")
fun getUsersWithPosts(): Flow<List<UserWithPosts>>
}
@Transaction 修饰的方法会在一个数据库事务中执行,对于 @Relation 查询是必须的(避免数据不一致)。
Database(数据库)
@Database(
entities = [User::class, Post::class], // 包含的所有表
version = 2, // 版本号(升级时需要 Migration)
exportSchema = true // 导出 schema 文件(推荐)
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2) // 数据库迁移
.build()
.also { INSTANCE = it }
}
}
}
}
单例模式确保全局只有一个数据库连接,@Volatile + synchronized 保证线程安全的懒加载。
数据库迁移
每次修改 Entity(增删字段、改表名),必须升级版本号并提供 Migration,否则 Room 会抛出异常。
// 版本 1 → 2:users 表新增 phone 列
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT NOT NULL DEFAULT ''"
)
}
}
// 版本 2 → 3:新增 posts 表
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
""")
}
}
跨多版本迁移:Room 会自动链式执行。如果数据库从版本 1 升到版本 3,Room 会依次执行 MIGRATION_1_2 和 MIGRATION_2_3。
如果不提供 Migration,只能用 fallbackToDestructiveMigration()——这会删除所有数据重建数据库,只适合开发阶段使用。
与协程的集成
Room 原生支持协程(suspend fun)和 Flow:
// Repository 层(封装数据访问逻辑)
class UserRepository(private val userDao: UserDao) {
// Flow 是冷流:只在有收集者时才查询,数据变化自动推送
val allUsers: Flow<List<User>> = userDao.getAllUsers()
suspend fun insert(user: User) {
userDao.insertUser(user) // suspend 函数,在 IO 线程执行
}
suspend fun getUserById(id: Int): User? {
return userDao.getUserById(id)
}
}
// ViewModel 中使用
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users: LiveData<List<User>> = repository.allUsers
.asLiveData() // Flow → LiveData 转换
fun addUser(user: User) = viewModelScope.launch {
repository.insert(user)
// Room 自动在 IO 线程执行,无需手动切换
}
}
Room 的 suspend DAO 方法会自动切换到 IO 线程,不需要手动 withContext(Dispatchers.IO)。
TypeConverter:存储复杂类型
数据库只支持基本类型,要存储 Date、List<String> 等复杂类型,需要 TypeConverter:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromStringList(value: String?): List<String>? {
return value?.split(",")
}
@TypeConverter
fun toStringList(list: List<String>?): String? {
return list?.joinToString(",")
}
}
// 在 Database 注解中注册
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()
测试 Room
Room 提供了内存数据库用于单元测试:
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
// 内存数据库:测试结束后自动销毁,不影响真实数据
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
userDao = database.userDao()
}
@After
fun closeDb() {
database.close()
}
@Test
fun insertAndRead() = runTest {
val user = User(name = "Alice", email = "alice@example.com")
userDao.insertUser(user)
val users = userDao.getAllUsers().first()
assertEquals(1, users.size)
assertEquals("Alice", users[0].name)
}
}
常见问题
Q:Room 为什么不能在主线程查询?
SQLite 查询可能耗时几十到几百毫秒,在主线程执行会导致 UI 卡顿。Room 默认在主线程调用会直接抛出 IllegalStateException,强迫开发者走正确的线程模型。(可用 .allowMainThreadQueries() 关闭检查,但只用于测试。)
Q:Flow 和 LiveData 哪个更好?
Flow 是纯 Kotlin 的解决方案,功能更强(操作符丰富、可背压控制)。新项目推荐 Flow;已有 LiveData 的项目可以用 .asLiveData() 无缝过渡。在 Compose 中直接用 collectAsState() 收集 Flow。
Q:外键约束怎么设置?
@Entity(
foreignKeys = [ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE // 删除 User 时级联删除 Post
)],
indices = [Index("user_id")] // 外键列需要索引以提升查询性能
)
data class Post(...)