Android笔记(二十一):Room组件实现Android应用的持久化处理

一、Room组件概述

Room是Android JetPack架构组件之一,是一个持久处理的库。Room提供了在SQLite数据库上提供抽象层,使之实现数据访问。

(1)实体类(Entity):映射并封装了数据库对应的数据表中对应的结构化数据。实体定义了数据库中的数据表。实体类中的数据域与表的列一一对应。

(2)数据访问对象(Data Access Object,DAO):在DAO中定义了访问数据库的常见的操作(例如插入、删除、修改和检索等),以达到实现创建映射数据表的实体类对象,以及对该实体类对象实例的属性值进行设置和获取的目的。

(3)数据库(Room Database):表示对数据库基本信息的描述,包括数据库的版本、名称、包含的实体类和提供的DAO对象实例。Room组件中的所有的数据库必须扩展为RoomDatabase抽象类,从而实现对实际SQLite数据库的封装。

二、Room组件的配置

在移动应用所在的模块对应的build.gradle中需要进行如下配置:

(1) 增加插件

Groovy DSL:

c 复制代码
plugins {
	   ......
    id 'kotlin-kapt'
}

Kotlin DSL:

kotlin 复制代码
plugins{
...
  id("kotlin-kapt")
}

kotlin-kapt实现标注(注解)处理

(2)增加标注处理的配置

Groovy DSL定义:

c 复制代码
android {
   ......
    defaultConfig {
       ......
        //增加标注处理,增加Schema保存的路径
        javaCompileOptions{
            annotationProcessorOptions{//定义标注处理器选项
                arguments +=[
                        "room.schemaLocation":"$projectDir/schemas".toString(),
                        "room.incremental":"true",
                        "room.expandProjection":"true"
                ]
            }
        }
}

Kotlin DSL定义:

kotlin 复制代码
android {
   ......
    defaultConfig {
       ......
        //增加标注处理
        javaCompileOptions{
            annotationProcessorOptions{
                //定义标注处理器选项
                arguments +=mapOf(
                    "room.schemaLocation" to "$projectDir/schemas".toString(),
                "room.incremental" to "true",
                "room.expandProjection" to "true"
                )
            }
        }
}

(3)增加相关依赖

Groovy DSL定义

c 复制代码
    def room_version = "2.5.0"
    implementation"androidx.room:room-runtime:$room_version"
    // 注释处理工具
    annotationProcessor "androidx.room:room-compiler:$room_version"
    // Kotlin注释处理工具(kapt)
    kapt"androidx.room:room-compiler:$room_version"
    // kotlin扩展和协同程序对Room的支持
    implementation "androidx.room:room-ktx:$room_version"

如果在处理数据库是需要采用rxJava3来实现异步处理,这时还需要增加RxJava3库

c 复制代码
    //增加RxJava库的依赖
    implementation "io.reactivex.rxjava3:rxjava:3.0.7"
    //增加在Android对RxJava库的支持
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
    // RxJava3
    implementation "androidx.room:room-rxjava3:$room_version"

Kotlin DSL定义依赖:

kotlin 复制代码
    val room_version = "2.5.0"
    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")
    kapt("androidx.room:room-compiler:$room_version")

    // kotlin扩展和协同程序对Room的支持
    implementation("androidx.room:room-ktx:$room_version")
    // RxJava2
    implementation("androidx.room:room-rxjava2:$room_version")
    // RxJava3
    implementation("androidx.room:room-rxjava3:$room_version")
    //增加RxJava库的依赖
    implementation("io.reactivex.rxjava3:rxjava:3.0.7")
    //增加在Android对RxJava库的支持
    implementation("io.reactivex.rxjava3:rxandroid:3.0.0")

如果在构建模块的过程中,出现了java版本不兼容的情况,可以调整:

kotlin 复制代码
android{
...
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_17
        targetCompatibility = JavaVersion.VERSION_1_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}

三、Room组件实现数据库的处理

新建一个项目,实现对多位学生的信息写入数据库并执行检索和CRUD操作。

3.1 创建实体类

映射并封装了数据库对应的数据表中对应的结构化数据。实体定义了数据库中的数据表。实体类中的数据域与表的列一一对应。

c 复制代码
@Entity(tableName = "students")
data class Student(@PrimaryKey(autoGenerate = true)
                   @ColumnInfo(name= "studentId") val id:Long,
                   @ColumnInfo(name= "studentNo") val no:String?,
                   @ColumnInfo(name= "studentName") val name:String,
                   @ColumnInfo(name= "studentScore") val score:Int,
                   @ColumnInfo(name = "studentGrade") val grade:String?
                   )
{
    @Ignore
    constructor(no:String,name:String,score:Int,grade:String):
            this(0,no,name,score,grade)
}

定义的实体类Student与数据表students对应。通过标注@Entity(tableName = "students")来指定实体类对应的数据表。并对实体类的属性定义通过标注@ColumnInfo,对应于数据表students中的各个字段,并通过@PrimaryKey标注来指定数据表的关键字。

注意:Room只能识别和使用一个构造器,如果存在多个构造器可以使用@Ignore让Room忽略这个构造器。因此在上述代码中constructor定义的辅助构造器增加了标注@Ignore。

3.2 创建数据访问对象DAO

在数据访问对象DAO是一个接口,定义了对指定数据表希望能执行的CRUD操作。

c 复制代码
@Dao
interface StudentDAO {
    /**
     * 插入记录
     */
    @Insert
    fun insertStudent(student:Student):Long

    /**
     * 删除记录
     */
    @Update
    fun updateStudent(student:Student)

    /**
     * 删除记录
     */
    @Delete
    fun deleteStudent(student:Student)

    /**
     * 检索所有的记录
     */
    @Query("select * from students")
    fun queryAllStudents():List<Student>

    /**
     * 检索指定学号的学生记录
     */
    @Query("select * from students where studentNo = :no")
    fun queryStudentByNo(no:String):Student

}

3.3 创建数据库

必须定义一个RoomDatabase的抽象子类来表示对数据库基本信息的描述,包括数据库的版本、名称、包含的实体类和提供的DAO对象实例。通过数据库类来达到对实际SQLite数据库的封装。

c 复制代码
@Database(entities = [Student::class], version = 1)
abstract class StudentDatabase : RoomDatabase() {
    abstract fun studentDao(): StudentDAO

    companion object{
        private var instance: StudentDatabase? = null
        /**
         * 单例模式创建为一个StudentDatabase对象实例
         */
        @Synchronized
        fun getInstance(context: Context): StudentDatabase {
            instance?.let{
                return it
            }
            return Room.databaseBuilder(
                context,
                StudentDatabase::class.java,
                "studentDB.db"
            ).build()
        }
    }
}

@Database标注表示抽象的类对应数据库,内部包括的数据表由标注内部的属性entities指定。如果数据库包括多个数据表,entitites可以指定多个实体类的类对象。

在上述的代码中,采用了单例模式,使得在整个移动应用中只有一个数据库的对象实例,在获取这个唯一实例时,只有一个线程可以访问,因此在getInstance方法中设置标注@Synchronized。在这个方法指定创建的数据库名是studentDB.db

3.4 定义并配置应用类

因为在应用中需要获取上下文,因此定义应用类,并在AndroidManifest进行配置,使之易于获取applicationContext上下文对象。

3.4.1定义应用类

c 复制代码
class MainApp: Application() {
    @SuppressLint("StaticFieldLeaked")
    companion object{
        lateinit var context: Context
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }
}

3.4.2 在AndroidManifest.xml配置应用类

在AndroidManifest.xml中需要在application元素中指定已定义的应用类MainApp,类似如下代码

c 复制代码
 <?xml version="1.0" encoding="utf-8"?>   
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application  android:name=".MainApp"  ... > 
</application>    
</manifest>  

3.5 测试数据库的访问

在MainActivity中定义对数据库的测试代码。

c 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
                    testDB()
                }
    }
    /**
     * 测试数据库
     */
    fun testDB() {
        Observable.create<Student> { emitter ->
            //获得Dao对象
            val dao = StudentDatabase.getInstance(MainApp.context).studentDao()
            //插入记录,测试数据库版本,将下列注释取消
            dao.insertStudent(Student("6001013", "李四", 87, "良好"))
            //检索记录
            val students = dao.queryAllStudents()
            for (student in students)
                emitter.onNext(student)
        }.subscribeOn(Schedulers.io())//指定被观察者的线程处理I/O 操作
            .observeOn(AndroidSchedulers.mainThread())//指定观察者的线程为主线程
            .subscribe {
                Log.d("Ch10_05", "${it}")
            }
      }
}

四、Room组件实现数据库的迁移

移动应用的需求的变化,也会导致数据库不断地升级。在数据库升级时,会希望保留原有的数据。因此,Room提供了数据库迁移的方式来解决数据库的升级。

Room库提供了Migration 类 实现数据库增量迁移。每个 Migration 子类提供了Migration.migrate() 函数实现新旧版本数据库之间的迁移路径。当移动应用需要升级数据库时,Room 库会利用一个或多个 Migration 子类运行 migrate() 函数,在运行时将数据库迁移到最新版本。

在上述的模块的基础上,要求修改数据库中数据表students的结构,增加一个新的字studentAddress,这时需要修改上述代码来完成具体的功能。

4.1 修改实体类

修改实体类Student,增加一个属性address,并映射数据表students的字段studentAddress,代码如下:

c 复制代码
@Entity(tableName = "students")
data class Student(@PrimaryKey(autoGenerate = true)
                   @ColumnInfo(name="studentId") val id:Long,
                   @ColumnInfo(name="studentNo") val no:String?,
                   @ColumnInfo(name="studentName") val name:String,
                   @ColumnInfo(name="studentScore") val score:Int,
                   @ColumnInfo(name = "studentGrade") val grade:String?,
                   @ColumnInfo(name="studentAddress") val address:String?){

    @Ignore
    constructor(no:String,name:String,score:Int,grade:String,address:String):
            this(0,no,name,score,grade,address)
}

4.2 修改数据库

因为数据表变化,这时需要修改数据库,变更数据库的版本为2。定义Migration对象,指定数据库迁移是从版本1迁移到版本2,并覆盖migrate的方法,执行具体迁移的操作。

c 复制代码
@Database(entities = [Student::class], version = 2)
abstract class StudentDatabase : RoomDatabase() {
    abstract fun studentDao(): StudentDAO
    companion object{
        private var instance: StudentDatabase? = null
        //数据库从版本1迁移到版本2
        val MIGRATION_1_2 = object : Migration(1, 2) {
            //迁移方法定义
            override fun migrate(database: SupportSQLiteDatabase) {
           //修改数据表students,增加一个新的字段address,数据类型为TEXT字符串
           database.execSQL("ALTER TABLE students ADD COLUMN studentAddress TEXT")
            }
        }

        /**
         * 单例模式创建为一个StudentDatabase对象实例
         */
        @Synchronized
        fun getInstance(context:Context):StudentDatabase{
            instance?.let{
                return it
            }
            return Room.databaseBuilder(
                context,
                StudentDatabase::class.java,
                "studentDB.db")
                .addMigrations(MIGRATION_1_2).build().apply{
                    instance = this
                }
        }
    }
}

在上述代码的getInstance返回数据库对象时,通过调用addMigrations进行处理迁移的操作。

4.3 修改测试代码

在上述修改的前提基础上,因数据库的变更,测试代码也进行修改,代码如下:

c 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
                      testDB()
                }
    }
 
    /**
     * 数据库版本2的测试函数
     */
    fun testDB(){
        Observable.create<Student>{ emitter ->
            //获得Dao对象
            val dao = StudentDatabase.getInstance(MainApp.context).studentDao()
            //插入记录
            dao.insertStudent(Student("6001015","王五",87,"良好","江西省南昌红谷大道999号"))
            //检索记录
            val students = dao.queryAllStudents()
            for(student in students)
                emitter.onNext(student)
        }.subscribeOn(Schedulers.io())//指定被观察者的线程处理I/O 操作
         .observeOn(AndroidSchedulers.mainThread())//指定观察者的线程为主线程
         .subscribe{ it: Student ->
                Log.d("TAG","${it}")
            }
     }
}

参考文献

陈轶《Android移动应用开发(微课版)》[M] 北京:清华大学出版社 2022 P407-P419

相关推荐
tatasix27 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。40 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了41 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度43 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮1 小时前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9992 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️2 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98762 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发
苏-言2 小时前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
Ljw...3 小时前
索引(MySQL)
数据库·mysql·索引