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返回值
@InsertINSERTLong(新ID)或 Unit
@UpdateUPDATEInt(影响行数)
@DeleteDELETEInt(影响行数)
@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 各组件的关系,我画了一张图:

Room 组件关系图 App / ViewModel DAO (Data Access Object) AppDatabase Entity (表定义) SQLite 关键点 • 单例 Database • DAO 用 suspend • Entity 写 @PrimaryKey

从图里能看出来,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,离线的数据流就能跑起来了。