返回 Android
Android
9 分钟阅读

Android Jetpack Room

一款Android官方推出的,放在Jetpack Package里面的数据库ORM框架

Room 是 Google 官方在 Jetpack 中推出的 SQLite ORM 框架,底层仍然是 Android 唯一的客户端关系型数据库 SQLite,但把「手写 SQL + SQLiteOpenHelper」里大量重复、易错的样板代码收敛成了三层结构:Entity(表结构)→ Dao(数据访问)→ Database(入口与版本管理)

在 MVVM 架构里,Room 通常落在 Repository 的本地数据源一侧,配合 SSOT 原则,让 UI 只观察 ViewModel 分发的状态,而数据的持久化读写统一走 Dao。

Android Jetpack Room 架构总览:Entity → Dao → Database 三层结构,以及在 MVVM 中经由 Repository 连接 SQLite 的数据流

核心架构:三层各司其职

职责编译期做什么
@Entity定义表结构,一个类对应一张表校验字段、主键、索引声明
@Dao声明增删改查与自定义 SQL校验 SQL 语法与返回值类型是否匹配
@Database聚合 Entity 与 Dao,管理版本与迁移生成具体实现类,连接 SQLite

Room 最大的价值不是「少写几行代码」,而是在编译期就把 SQL 错误拦住——表名写错、字段对不上、返回类型不匹配,都会在 build 阶段报错,而不是等到运行时 Crash。

相关 Room 信息导入

dependencies {
    val room_version = "2.8.1"
 
    implementation("androidx.room:room-runtime:$room_version")
 
    // Kotlin 支持(推荐 KSP,比 kapt 更快)
    ksp("androidx.room:room-compiler:$room_version")
 
    // Java 支持
    annotationProcessor("androidx.room:room-compiler:$room_version")
 
    // Kotlin 拓展:suspend、Flow、withTransaction 等
    implementation("androidx.room:room-ktx:$room_version")
 
    // RxJava3 支持
    implementation("androidx.room:room-rxjava3:$room_version")
 
    // Test Code 支持
    testImplementation("androidx.room:room-testing:$room_version")
 
    // Paging3 支持
    implementation("androidx.room:room-paging:$room_version")
}

Gradle Plugins 导入

plugins {
    id 'androidx.room' version "$room_version" apply false
}
plugins {
    id 'androidx.room'
}
 
android {
    ...
    room {
        schemaDirectory "$projectDir/schemas"
    }
}

schemaDirectory 会把每个版本的数据库 schema 导出为 JSON 文件,迁移测试和 Code Review 时非常有用——能直观看到表结构从 v1 到 v2 到底变了什么。

Entity 层:表结构的声明

@Entity(
    tableName = "users",
    indices = [Index(value = ["email"], unique = true)],
    foreignKeys = [
        ForeignKey(
            entity = Department::class,
            parentColumns = ["id"],
            childColumns = ["department_id"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
 
    @ColumnInfo(name = "user_name")
    val name: String,
 
    val email: String,
 
    @ColumnInfo(defaultValue = "0")
    val departmentId: Long = 0
)

几个容易踩坑的点:

  • 主键@PrimaryKey(autoGenerate = true) 插入时不传 id,SQLite 自增;若用复合主键,改用 @Entity(primaryKeys = ["uid", "gid"])
  • 字段名映射:Kotlin 属性名和列名不一致时用 @ColumnInfo(name = "...");不写则默认用属性名。
  • 不可变 data class:Room 推荐用 val 的 data class,查询结果是新对象,不会意外修改缓存。

TypeConverter:Room 不认识的类型

Room 原生只支持基本类型和少数容器(如 ByteArray)。Date枚举List<String> 等需要 @TypeConverter

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
 
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time
}
 
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }

Converter 可以挂在 Database 或单个 Entity / Dao 上,按作用域选用。

Dao 层数据编写

增 Insert

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)
 
    @Insert
    suspend fun insertBothUsers(user1: User, user2: User)
 
    @Insert
    suspend fun insertUsersAndFriends(user: User, friends: List<User>)
 
    // 需要拿到自增主键时
    @Insert
    suspend fun insertAndReturnId(user: User): Long
}

OnConflictStrategy 常用值:ABORT(默认,冲突抛异常)、REPLACE(覆盖)、IGNORE(跳过)。

删 Delete

@Dao
interface UserDao {
    @Delete
    suspend fun deleteUsers(vararg users: User)
 
    // 按条件批量删除用 @Query
    @Query("DELETE FROM users WHERE department_id = :deptId")
    suspend fun deleteByDepartment(deptId: Long)
}

@Delete 按主键匹配;复杂条件删除必须写 @Query

改 Update

@Dao
interface UserDao {
    @Update
    suspend fun updateUsers(vararg users: User)
}

@Update 同样按主键匹配,只更新非主键字段。

查 Read Query

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    suspend fun loadAllUsers(): List<User>
 
    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun loadUserById(id: Long): User?
 
    // 返回 Flow:表数据变化时自动推送新列表(配合 SSOT 很顺手)
    @Query("SELECT * FROM users ORDER BY user_name ASC")
    fun observeAllUsers(): Flow<List<User>>
}

特殊返回值类型

返回类型场景
List<T> / T?一次性查询
Flow<T>监听表变化,Room 自动在后台线程发射
PagingSource<Int, T>与 Paging 3 分页
LiveData<T>旧项目;新项目优先 Flow

使用 Paging 库将查询分页:

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE user_name LIKE '%' || :query || '%'")
    fun pagingSource(query: String): PagingSource<Int, User>
}

关系查询:@Embedded 与 @Relation

一张表搞不定时,用 POJO 做查询结果映射,而不是再建一张表:

data class UserWithDepartment(
    @Embedded val user: User,
    @Relation(
        parentColumn = "department_id",
        entityColumn = "id"
    )
    val department: Department
)
 
@Dao
interface UserDao {
    @Transaction
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserWithDepartment(userId: Long): UserWithDepartment
}

@Transaction 保证嵌套查询在同一数据库连接里完成,避免读到中间态。

Database 层

@Database(entities = [User::class, Department::class], version = 2, exportSchema = true)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun departmentDao(): DepartmentDao
}

数据库迁移

版本号变了却没有提供迁移路径,Room 默认直接 Crash。正确做法是显式声明 Migration

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT NOT NULL DEFAULT ''")
    }
}
 
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app-db")
    .addMigrations(MIGRATION_1_2)
    .build()

开发阶段可以 .fallbackToDestructiveMigration() 快速清库重建,上线前必须删掉,否则用户升级会丢数据。

事务

多个写操作需要原子性时:

@Dao
interface UserDao {
    @Transaction
    suspend fun replaceDepartmentUsers(deptId: Long, users: List<User>) {
        deleteByDepartment(deptId)
        insertUsers(*users.toTypedArray())
    }
}

或在 Database 层用 KTX 扩展:

db.withTransaction {
    userDao.deleteByDepartment(deptId)
    userDao.insertUsers(*users.toTypedArray())
}

线程规则

Room 默认禁止在主线程访问数据库(查询会抛 IllegalStateException)。现代写法用 suspendFlow,Room 自动调度到后台线程:

// ViewModel 中
viewModelScope.launch {
    val users = userDao.loadAllUsers()   // suspend,自动切 IO 线程
}
 
// 观察数据变化
userDao.observeAllUsers()
    .flowOn(Dispatchers.IO)
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

若确实需要在主线程同步读(不推荐),构建时加 .allowMainThreadQueries(),仅限极端场景如 Widget 的 RemoteViews 同步读取。

Usage:从构建到 Repository

// 单例持有,避免多实例锁死数据库
@Singleton
class AppDatabaseProvider @Inject constructor(
    @ApplicationContext context: Context
) {
    val db: AppDatabase = Room.databaseBuilder(
        context,
        AppDatabase::class.java,
        "app-database"
    )
        .addMigrations(MIGRATION_1_2)
        .build()
}
 
// Repository 作为 SSOT 的本地数据源
class UserRepository @Inject constructor(
    private val userDao: UserDao
) {
    val users: Flow<List<User>> = userDao.observeAllUsers()
 
    suspend fun refreshFromNetwork(remoteUsers: List<User>) {
        userDao.insertUsers(*remoteUsers.toTypedArray())
    }
}
val userDao = db.userDao()
val users: List<User> = userDao.loadAllUsers()

与 SSOT / MVVM 的配合方式

典型数据流:

  1. 网络层拉取最新数据 → Repository 写入 Room(insert / REPLACE
  2. Dao 的 Flow 查询作为本地 SSOT,表一变 ViewModel 自动收到
  3. ViewModelstateIncollect 转成 UI State
  4. Compose / XML 只渲染 State,不直接碰 Dao

这样本地数据库就是「权威数据源」,网络只是刷新手段;离线时 Flow 照样能发出缓存数据,不会出现多份副本不同步的问题。