Jetpack - Room

官方页面

一、概念

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 定义表结构

实体包含表中的每一列的字段,包括构成主键的一个或多个列。

注解可选配置说明
@EntitytableName默认情况下会将类名用作表名,使用该配置来更改。(SQL中表和列名称不区分大小写)
indices

将数据库中的某些列编入索引,以加快查询速度。对类配置

@Entity(

    indices = [Index(value = ["last_name","address"])]

)

primaryKeys需要通过多个列的组合进行唯一标识(定义复合主键)可对类配置@Entity(primaryKeys = ["fristName","lastName"])。
ignoredColumns如果实体继承了父类,对类配置@Entity(ignoredColumns = ["age"])来忽略父类中的字段。
@PrimaryKeyautoGenerate = true主键用于唯一标识每一行,对参数使用@primaryKey注解来设置,需要自增可配置autoGenerate = true。
@Ignore默认情况下会为每个参数创建一个列,对参数使用@Ignore注解来忽略(不生成列、不入库)。
@ColumnInfoname默认情况下会将参数名用作表的列名,一个参数对应一个列名,使用改配置来更改。(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 表示失败。
onConflictREPLACE:主键冲突就整行替换。用来实现"存在则更新"
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/。

注解可选配置说明
@Databaseverson数据库版本。
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;

上一篇: Android - 序列化 Serializable、Parcelable
下一篇: Kotlin - 异常 Exception
Jomurphys
博客等级 码龄9年 1768粉丝 261原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值