一、Room组件概述
Room是Android JetPack架构组件之一,是一个持久处理的库。Room提供了在SQLite数据库上提供抽象层,使之实现数据访问。
(1)实体类(Entity):映射并封装了数据库对应的数据表中对应的结构化数据。实体定义了数据库中的数据表。实体类中的数据域与表的列一一对应。
(2)数据访问对象(Data Access Object,DAO):在DAO中定义了访问数据库的常见的操作(例如插入、删除、修改和检索等),以达到实现创建映射数据表的实体类对象,以及对该实体类对象实例的属性值进行设置和获取的目的。
(3)数据库(Room Database):表示对数据库基本信息的描述,包括数据库的版本、名称、包含的实体类和提供的DAO对象实例。Room组件中的所有的数据库必须扩展为RoomDatabase抽象类,从而实现对实际SQLite数据库的封装。
二、Room组件的配置
(1)配置libs.versions.toml文件
在顶层项目的libs.versions.toml文件增加如下内容:
[versions]
room = "2.6.1"
ksp = "1.9.20-1.0.14"
[libraries]
androidx-room-runtime = {group = "androidx.room",name="room-runtime",version.ref="room"}
androidx-room-compiler = {group = "androidx.room",name = "room-compiler", version.ref = "room"}
androidx-room-ktx = {group="androidx.room",name = "room-ktx",version.ref = "room"}
androidx-room-rxjava3 = {group = "androidx.room",name = "room-rxjava3", version.ref = "room"}
[plugins]
google-devtools-ksp = {id="com.google.devtools.ksp",version.ref = "ksp"}
(2)配置模块build.gradle.kts
需要分别执行四个步骤:
增加KSP插件
增加kotlin-ksp实现标注(注解)处理
plugins{
......
alias(libs.plugins.google.devtools.ksp)
}
修改配置编译Java和Kotlin版本为17以上
android {
.......
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
增加KSP标注处理的配置
```kotlin
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection","true")
}
增加相关依赖
dependencies {
implementation(libs.androidx.room.runtime)
//使用Kotlin标注处理工具ksp
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.rxjava3)
//可选项,使用Kotlin扩展和协程
implementation(libs.androidx.room.ktx)
//可选项,使用RxJava3支持Room
implementation(libs.androidx.room.rxjava3)
...
}
注意:上述四个步骤,必须先配置plugins和编译选项,即前两个步骤,Sync后。配置ksp的相关配置和依赖才有效。
三、Room组件实现数据库的处理
新建一个项目,实现对多位学生的信息写入数据库并执行检索和CRUD操作。
3.1 创建实体类
映射并封装了数据库对应的数据表中对应的结构化数据。实体定义了数据库中的数据表。实体类中的数据域与表的列一一对应。
@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操作。
@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数据库的封装。
@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定义应用类
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,类似如下代码
<?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中定义对数据库的测试代码。
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,代码如下:
@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的方法,执行具体迁移的操作。
@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 修改测试代码
在上述修改的前提基础上,因数据库的变更,测试代码也进行修改,代码如下:
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