一、概念

ORM框架(Object Relational Mapping 对象关系映射),将面向对象的编程语言和面向关系的数据库之间建立一种映射关系。Database 管理多个 Entity,通过 DAO 操作它们。
| Entity 表 | 数据实体(数据模型、数据结构)对应一张表,表中的列根据实体类中的字段生成。实体的每个实例都表示相应表中的一行数据。 |
| Dao 操作 | 数据访问对象(SQL操作)。必须是接口,根据业务封装对数据库操作的抽象方法会在编译时自动生成实现,调用时就不用跟底层语法打交道了。 |
| Database 库 | 数据库持有者。必须是抽象类,定义了版本号、包含哪些表,暴露Dao层访问的实例。 |
二、添加依赖
[versions]
kotlinKsp = "2.3.6"
room = "2.8.4"
[libraries]
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlinKsp" }
Project.build.gradle
plugins {
alias(libs.plugins.kotlin.ksp) apply false
}
Module.build.gradle
plugins {
alias(libs.plugins.kotlin.ksp)
}
dependencies {
implementation(libs.room.runtime)
implementation(libs.room.ktx) //协程、扩展函数 支持
implementation(libs.room.paging) //paging3 支持
ksp(libs.room.compiler)
}
三、创建
3.1 Entity 定义表结构
实体包含表中的每一列的字段,包括构成主键的一个或多个列。
| 注解 | 可选配置 | 说明 |
| @Entity | tableName | 默认情况下会将类名用作表名,使用该配置来更改。(SQL中表和列名称不区分大小写) |
| indices | 将数据库中的某些列编入索引,以加快查询速度。对类配置 @Entity( indices = [Index(value = ["last_name","address"])] ) | |
| primaryKeys | 需要通过多个列的组合进行唯一标识(定义复合主键)可对类配置@Entity(primaryKeys = ["fristName","lastName"])。 | |
| ignoredColumns | 如果实体继承了父类,对类配置@Entity(ignoredColumns = ["age"])来忽略父类中的字段。 | |
| @PrimaryKey | autoGenerate = true | 主键用于唯一标识每一行,对参数使用@primaryKey注解来设置,需要自增可配置autoGenerate = true。 |
| @Ignore | 默认情况下会为每个参数创建一个列,对参数使用@Ignore注解来忽略(不生成列、不入库)。 | |
| @ColumnInfo | name | 默认情况下会将参数名用作表的列名,一个参数对应一个列名,使用改配置来更改。(SQL中表和列名称不区分大小写) |
| typeAffinity | 该属性在表中的类型。 |
3.1.1 定义实体类
对类使用 @Entity 注解来设置该类为Room识别的实体类。
@Entity(tableName = "UserEntity") //不设置默认使用类名作为表名
data class User()
3.1.2 定义主键
实体必须指定主键,主键用于唯一标识每一行,对参数使用@primaryKey注解来设置,需要自增(自动分配id)可配置autoGenerate = true。
在创建实体对象时,主键字段需要传入 null 或 0,Room 会在插入数据库时自动为其分配唯一值,建议是默认值设为 0,设为 null 调用时会有非空处理。
@Entity(primaryKeys = ["fristName","lastName"]) //定义复合主键,通过多个列的组合进行唯一标识
data class User(
//设为主键自增长
@PrimaryKey(autoGenerate = true) val id: Long = 0L
)
3.1.3 定义列名
默认情况下会将参数名用作表的列名,一个参数对应一个列名。需要取别名才用 @ColumnInfo 注解指定 name。
@Entity
data class User(
//不指定列名默认为参数名
@ColumnInfo(name = "frist_name") val fristName: String?
)
3.1.4 忽略参数
默认情况下会为每个参数创建一个列,对参数使用@Ignore注解来忽略。如果实体继承了父类,对类配置@Entity(ignoredColumns = ["age"])来忽略父类中的字段。
data class User(
@Ignore val name: String
)
open class Father {
var age: Int? = null
}
@Entity(ignoredColumns = ["age"]) //忽略继承的参数
data class User(
@PrimaryKey(autoGenerate = true) val id: Int
) : Father()
3.1.5 支持全文搜索
需要通过全文搜索 (FTS) 快速访问数据库信息,对类使用@FTS3或@FTS4,需要Room2.1.0以上版本。仅当应用有严格的磁盘空间要求,或需要与旧版 SQLite 兼容时,才使用 @Fts3。
@Fts4
@Entity
data class User(
//为基于 FTS 表的实体指定主键是可选的,但若包含主键,则必须使用此类型和列名
@PrimaryKey @ColumnInfo(name = "rowid") val id: Int
)
3.1.6 将特定列编入索引
可以将数据库中的某些列编入索引,以加快查询速度。对类配置@Entity(indices = [Index(calue = ["last_name","address"])])。
@Entity(indices = [Index(value = ["name","address"])])
data class User(
val name: String?,
val address: String?
)
3.2 创建 Dao 定义外部访问的接口
| 注解 | 说明 | 函数参数使用Entity实例 | 函数参数使用非Entity实例 |
| @Insert | 插入 | 可以直接使用该注解 | 只能使用@Query+SQL语句 |
| @Delete | 删除 | ||
| @Update | 更新 | ||
| @Query | 查询 | @Query+SQL语句 | |
3.2.1 @Insert @Delete @Update
传入的形参需要是Entity的实例。删除和更新是通过主键进行匹配,如果没有相同的行不会进行任何更改。默认不允许在主线程执行数据库操作(会抛出IllegalStateException)因此使用 suspend 修饰方法配合协程使用。
- @Insert:如果方法接收到单个参数,则可返回一个 long 值,这是插入项的新 rowId,-1 表示失败。如果参数是数组或集合,则可改为返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId。
- @Update:可以选择性地返回 int 值,指示成功更新的行数,0 表示失败。
- @Delete:可以选择性地返回 int 值,指示成功删除的行数,0 表示失败。
| onConflict | REPLACE:主键冲突就整行替换。用来实现"存在则更新" |
| IGNORE:冲突则忽略(保留旧行)。 | |
| ABORT(默认):冲突直接抛异常。 |
@Dao
interface UserDao {
//插入
@Insert
suspend fun insertUsers(vararg users: User)
@Insert
suspend fun insertBothUsers(user1: User, user2: User)
@Insert
suspend fun insertUsersAndFriends(user: User, friends: List<User>)
//删除
@Delete
suspend fun deleteUsers(vararg users: User)
//更新(尝试更新数据库中的一个或多个 User 对象)
@Update
suspend fun updateUsers(vararg users: User)
}
3.2.2 @Query+SQL语句
编写SQL语句从而进行查询数据,或更复杂的插入、更新和删除。Room会在编译时自动验证查询语句,如果查询有问题会出现编译错误。
@Dao
interface UserDao {
//使用非实体类参数,只能用@Query加上SQL语句
@Query("delete from Person where age = :age")
fun deleteByPersonName(age: Int): Int //根据传入的年龄删除表中对应的数据
}
3.2.3 获取列的子集
有时候需要一次性获取某几列的数据,可以从查询返回简单对象,前提是可以将这些数据映射到返回的对象中。
//定义数据类,设置对应的字段
data class FullName(
@ColumnInfo(name = "frist_name") val fristName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
//ROOM知道查询会返回frist_name,last_name这两个列的值,会被映射到FullName类的字段中
//如果查询返回的列未映射到返回对象的字段中,会显示警告
@Query("SELECT frist_name,last_name FROM Student")
fun getFullName(): List<FullName> //查询语函数返回该简单对象
3.2.4 将参数传递给查询
通常需要接受传参以便进行过滤。
//单个参数
@Query("SELECT * FROM user WHERE age > :minAge")
fun getAllUsersOlderThan(minAge: Int): Array<User>
//多个参数
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun getAllUsersBetween(minAge: Int, maxAge: Int): Array<User>
//多次引用同一参数
@Query("SELECT * FROM user WHERE frist_name LIKE :str OR last_name LIKE :str")
fun getUsersWithName(str: String): List<User>
//一组参数
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun getUsersFromRegions(regions: List<String>): List<User>
3.3 创建 Database 配置数据库
Database必须声明为抽象类。存在于 data/data/包名/databases/。
| 注解 | 可选配置 | 说明 |
| @Database | verson | 数据库版本。 |
| entities | 包含的实体类。 | |
| exportSchema | 是否支持导出成 JSON 文件存档,默认false,改为 true 后会在 app/schemas/ 下按版本导出表结构,便于后续数据迁移和版本管理。 |
//指定数据库版本,与Entity进行关联
@Database(version = 1, entities = [User::class], exportSchema = true)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao //与Dao进行关联(ROOM编译器生成实现,KSP干的活)
companion object { //单例因为全局只存在一份数据库
@Volatile // 保证多线程间的可见性,防止指令重排导致拿到半初始化对象
private var INSTANCE: UserDatabase? = null
// 第一次判空在锁外,避免已初始化后还去竞争锁
fun getInstance(context: Context): AudioDatabase = INSTANCE ?: synchronized(this) {
context = context.applicationContext, //上下文使用Application避免内存泄漏
klass = UserDatabase::class.java, //本类名称
name = "UserDB" //数据库名称
).build()
.also { instance = it }
}
}
把 exportSchema = true 并在 build.gradle 指定导出目录。导出后,每个版本的 JSON schema 落盘,迁移时可对照旧结构写 SQL,还能做破坏性变更检测(CI 里校验迁移是否丢列)。
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments["room.schemaLocation"] = "$projectDir/schemas"
}
}
}
3.4 使用
class Repository {
val db by lazy { PersonDatabase.getInstance(MyApplication.context) }
val dao by lazy { db.personDao() }
suspend fun getData() {
val id = dao.insertPerson(Person(null, "", 12))
println("查ID:${db.personDao().queryPerson(id)}")
val list = dao.getAllPerson()
list.forEach { println("查所有:$it") }
}
}
四、数据库升级
任何对数据库中表的改变都需要升级数据库版本。当旧 APP 更新后,操作时发现本地数据库版本不同(文件结构不对),如果没有提供迁移策略也没有设置允许破坏性重建,会抛异常 IllegalStateException 出于数据安全考虑宁愿崩溃,让你给出明确方案。
需要实现 Migration 类编写 SQL 迁移语句,在创建数据库时添加迁移规则。若版本跨度大(1→3),需要依次实现 MIGRATION_1_2 和 MIGRATION_2_3,Room会自动按顺序执行。
迁移发生在第一次访问数据库的时候(例如调用Dao方法),Room 会在其内部管理的后台执行器上运行迁移逻辑,如果需要确保迁移在进入主界面之前完成(例如配合启动页),正确的做法是主动触发一次数据库访问,并等待其完成,而不是改造 getInstance()。
| 添加新表 | 新功能需要存储新的数据类型。 |
| 修改旧表 | 增加新列(最常见,如上文的添加手机号)。 删除某一列(Room 不支持直接删列,需要更复杂的迁移策略)。 修改列的类型或约束(如将文本改为非空)。 |
| 创建索引或触发器 | 为了提升查询性能。 |
4.1 破坏性重建(丢弃旧数据)
适用于数据库用作缓存/测试的场景,禁止用于生产环境,迁移失败就删库重建,用户数据全丢。
| fallbackToDestructiveMigration() | public actual fun fallbackToDestructiveMigration( dropAllTables: Boolean //是否删除所有表(建议为true避免留下过时数据) ): Builder<T> 升级时,在找不到迁移策略时,允许破坏性重建。 |
| fallbackToDestructiveMigrationOnDowngrade() | public open fun fallbackToDestructiveMigrationOnDowngrade(): Builder<T> 降级时,在找不到迁移策略时,允许破坏性重建。 |
4.2 迁移策略(保留旧数据)
2.5.1 新增一张表
@Entity
data class Student(
@PrimaryKey(autoGenerate = true) val id: Long
)
@Dao
interface StudentDao {
@Delete
fun deleteStudent(student: Student) //将传入的Student对象从表中删除
}
@Database(version = 2, entities = [User::class, Student::class]) //版本号升级到了2,后添加的Entity也要关联
abstract class SemerWeatherDatabase : RoomDatabase() {
abstract fun UserDao(): UserDao
abstract fun studentDao(): StudentDao //后添加的Dao也要关联
companion object {
@Volatile
private var INSTANCE: SemerWeatherDatabase? = null
val MIGRATION_1_2 = object : Migration(1, 2) { //1升2的时候就要执行这个逻辑
override fun migrate(database: SupportSQLiteDatabase) {
//建表语句需要和后添加的Entity中声明的结构完全一致,否则报错
database.execSQL("create table Student (id long primary key autoincrement not null)")
}
}
fun getInstance(context: Context): AudioDatabase = INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
SemerWeatherDatabase::class.java,
"SemerWeatherDB"
).addMigrations(MIGRATION_1_2) //添加迁移规则
.build()
.also { INSTANCE = it }
}
}
2.5.2 原表新增列
新列必须给默认值,减少崩溃。数据后期慢慢补齐。
@Entity
data class Student(
@PrimaryKey(autoGenerate = true) val id: Long,
val name: String
)
@Database(version = 3, entities = [User::class, Student::class]) //版本号升级到了3
abstract class SemerWeatherDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun studentDao(): StudentDao
companion object {
private var instance: SemerWeatherDatabase? = null
//新增
val MIGRATION_2_3 = object : Migration(2, 3) { //2升3的时候就要执行这个逻辑
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("alter table Student add column name text not null default 'unknown'")
}
}
@Synchronized
fun getInstance(context: Context): SemerWeatherDatabase = instance
?: Room.databaseBuilder(
context.applicationContext,
SemerWeatherDatabase::class.java,
"SemerWeatherDB"
).addMigrations(MIGRATION_1_2, MIGRATION_2_3) //添加迁移规则
.build()
}
}
2.5.3 向前修,不回滚
假设 v2.0 发布后发现迁移有 bug,不能"撤回"迁移,因为已升级用户的库已是 v2,正确做法是发 v3 修正。
// 场景:v2 迁移时某列数据算错了
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE users SET age = 0 WHERE age < 0") // 修正错误数据
// 或补上 v2 漏掉的结构变更
}
}
2.5.4 修改列类型
建新表 → 搬数据 → 删旧表 → 改名。
CREATE TABLE users_new (...新结构...);
INSERT INTO users_new SELECT ... FROM users; -- 搬数据
DROP TABLE users;
ALTER TABLE users_new RENAME TO users;



被折叠的 条评论
为什么被折叠?



