前言
今天继续讲Jetpack
这次要讲的是Room库
因为涉及的东西比较多
所以写的比较长
话不多说,立马开始!
Room是什么
Room是Google官方开发的对象关系映射(ORM)库框架,
说白了,就是方便你操作数据库的
它采用注解的方式,省去了一些重复的代码
让你访问数据库更加稳健,提升数据库性能。
之前也有封装过一些简单的数据库工具类
网上其他的开源库也不少
但是既然Google推出了这个东西
建议以后开发还是优先使用这个库来完成业务
整个Room的使用,我画了下面一个简单的图作为参考
当然,这个图里是Room+Viewmodel+LiveData的方式使用,下面也会有介绍
Table:数据表,可以有多个
Dao:操作数据表的类,可以一个Dao操作多个表,或者一个Dao操作对应的表
DataBase:初始化数据库的类(包括数据库版本,升级等等)
Repository:一个中间类,用来给Viewmodel传递数据
Viewmodel:这个不用说了,用来返回数据的
View层:通过Viewmodel获得数据即可
这样看下来就会发现
View层不需要关注具体执行数据库的过程
代码都在Repository里封装好了
不过,在使用Room+Viewmodel+LiveData之前
我们先学习下Room的基础用法
Room基础用法
第一步肯定就是导包了,这里用的是2.2.5版本:
//Room
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
第二步是创建我们的数据表
在Room里是创建一个类并且加上@Entity注解,代码如下:
/**
* 主构造方法里,最好写一个必须要生成的字段,不然表是空的,room是不支持的
*/
@Entity
data class Student(val name: String = "无名氏") {
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER)
var age: Int = 0
// //V2.0
// @ColumnInfo(name = "sex", typeAffinity = ColumnInfo.INTEGER)
// var sex: Int = 1
//V3.0
@ColumnInfo(name="sex",typeAffinity = ColumnInfo.TEXT)
var sex: String = "1"
/**
* 主键自增长,不需要传值,让其自动生成就好
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
var id: Int = 0
/**
* 无参构造
*/
@Ignore
constructor() : this("无名氏") {
}
/**
* 这个构造主要是为了根据id删除和修改数据库使用的
* 一般插入操作不使用这个构造
*/
@Ignore
constructor(id: Int, name: String, age: Int) : this(name) {
//次级构造函数
this.id = id
this.age = age
}
@Ignore
constructor(name: String, age: Int) : this(name) {
//次级构造函数
this.age = age
}
// constructor( name: String, age: Int,sex: Int) : this(name) {
// //次级构造函数
// this.age = age
// this.sex = sex
// }
override fun toString(): String {
return "Student(id=$id)(name=$name)(age=$age)(sex=$sex)"
}
}
这里的几个注解解释一下:
@Ignore:kotlin 更新值 1.2.6 版本时,room 表的实体类必须要有个无参构造
同时有多个构造的情况下,需要把主构造以外的其他构造用@Ignore注解来标记
否则报错:
There are multiple good constructors and Room will pick the no-arg constructor. You can use the @Ignore annotation to eliminate unwanted constructors.
其实意思就是:有多个构造函数,Room将选择无参数构造函数。您可以使用@Ignore注释来消除不需要的构造函数。
@ColumnInfo:代表一个字段
@PrimaryKey:代表是主键
typeAffinity :字段类型
autoGenerate :是否自增长,true代表自增长
这里需要注意几点:
1、主构造方法里,最好写一个必须要生成的字段,不然表是空的,room是不支持的
2、主键自增长,不需要传值,让其自动生成就好
第三步
创建操作数据表的Dao文件
文件头加上@Dao注解
这个类就是用来写一些增删改查操作的
@Dao
interface StudentDao {
@Insert
abstract fun insertStudent(s: Student)
@Delete
abstract fun deletStudent(s: Student)
@Query("DELETE FROM Student")
abstract fun deletAllStudent()
@Update
abstract fun upDateStudent(s: Student)
@Query("SELECT * FROM Student")
abstract fun getAllStudent(): List<Student>
@Query("SELECT * FROM Student WHERE id = :id")
abstract fun getStudentById(id: Int): List<Student>
}
注意,这边写好了之后,Room就会自动给我们进行增删改查操作了
再来看第四步
创建一个DataBase
先贴出代码,然后一个个解释注解的意思
@Database(entities = arrayOf(Student::class), version = 3, exportSchema = true)
abstract class MyDataBase : RoomDatabase() {
abstract fun getStudentDao(): StudentDao
companion object {
private var instance: MyDataBase? = null
private var DATABASE_NAME = "my_db"
@Synchronized
fun getInstance(context: Context): MyDataBase {
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java, DATABASE_NAME
)
// .allowMainThreadQueries()//允许Room在主线程操作数据库,不推荐使用
// .addMigrations(MIGRATION_1_2)
// .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
// .fallbackToDestructiveMigration()
// .createFromAsset("")
.build()
}
return instance!!
}
}
}
首先是@Database的头注解
entities里面写的是你的数据表,有几个数据表,就写几个到里面就好
version是数据库版本号
exportSchema是记录数据库操作记录,这个后面会讲,默认false
这个类里面写了一个abstract方法用来获得StudentDao
这个方法也不需要我们实现,Room自己会实现帮助我们获得Dao对象
这里我采用了一个单例模式来创建Database对象
需要注意的一点
操作数据库属于耗时操作
Room默认要在子线程执行增删改查
所以allowMainThreadQueries这个方法是不建议使用的
最后第五步,在我们的Activity得到StudentDao
执行StudentDao里面写好的增删改查操作即可
这里贴出一个示例代码
插入两条数据,然后执行查询:
class MainActivity : AppCompatActivity() {
lateinit var studentDao: StudentDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main3)
val instance = MyDataBase.getInstance(this)
studentDao = instance.getStudentDao()
InsertStuTask(studentDao).start()
btn.setOnClickListener {
Thread {
val allStudent = studentDao.getAllStudent()
runOnUiThread {
tvlist.setText(allStudent.toString())
}
}.start()
}
}
//插入两条数据
class InsertStuTask(val studentDao: StudentDao) : Thread() {
override fun run() {
studentDao.insertStudent(Student("张三", 18))
studentDao.insertStudent(Student("李四", 19))
super.run()
}
}
}
Room配合ViewModel+LiveData
介绍完基础的用法,我们也发现了一点小问题:
1:每次增删改查都要自己去创建一个线程,在线程里面执行
2:数据库有更新的时候,无法自动去刷新数据,而是要再去点击按钮查询一遍
这时候就需要用到我们的ViewModel+LiveData了
它们刚好可以帮我们解决这两个问题
首先,LiveData配合Room,返回LiveData类型,
内部会自动开启一个线程。这时候就不需要再新建子线程操作数据库了
具体说明参考这篇博客
LiveData结合Room数据库使用以及线程问题
然后,ViewModel+LiveData可以实现数据变化的更新,让数据变化后的结果实时显示
需要补充一点的是
livedata会自动根据activity的生命周期,决定要不要去刷新数据。
比如页面A和页面B都做了数据表发生变化然后去刷新UI的操作
但是这时关闭页面B,在页面A中执行插入数据的操作
操作完成后
页面A会刷新,页面B是不会刷新的。
具体使用步骤如下:
第一步,创建Repository类
这个类主要是来作为一个中间类方便Viewmodel调用的
具体代码如下:
class StudentRepository(context: Context) {
lateinit var studentDao: StudentDao
lateinit var context: Context
init {
val myDataBase = MyDataBase.getInstance(context)
studentDao = myDataBase.getStudentDao()
this.context = context
}
fun InsertStuDent() {
InsertStuTask(studentDao).start()
}
fun InsertStuDent2(student: Student) {
InsertStuTask2(studentDao,student).start()
}
fun DeleteStuDent(student: Student) {
DeleteStuTask(studentDao, student).start()
}
fun UpDateStuDent(student: Student) {
UpDateStuTask(studentDao, student).start()
}
fun DeleteAllStuDent() {
DeleteAllStuTask(studentDao).start()
}
class InsertStuTask(val studentDao: StudentDao) : Thread() {
override fun run() {
studentDao.insertStudent(Student( "张三",18))
studentDao.insertStudent(Student( "张三2",19))
super.run()
}
}
class InsertStuTask2(val studentDao: StudentDao,val student: Student) : Thread() {
override fun run() {
studentDao.insertStudent(student)
super.run()
}
}
class UpDateStuTask(val studentDao: StudentDao, val stu: Student) : Thread() {
override fun run() {
studentDao.upDateStudent(stu)
super.run()
}
}
class DeleteStuTask(val studentDao: StudentDao, val stu: Student) : Thread() {
override fun run() {
studentDao.deletStudent(stu)
super.run()
}
}
class DeleteAllStuTask(val studentDao: StudentDao) : Thread() {
override fun run() {
studentDao.deletAllStudent()
super.run()
}
}
/**
* LiveData配合Room,返回LiveData类型,内部会自动开启一个线程。这时候就不需要再新建子线程操作数据库了
*/
fun getAllStuDent():LiveData<List<Student>> {
return studentDao.getAllStudent()
}
}
可以看到,StudentDao和Database是在这里进行初始化了
另外StudentDao里面查询数据的方法
返回值也需要改成Livedata类型的了
示例如下:
@Query("SELECT * FROM Student")
abstract fun getAllStudent(): LiveData<List<Student>>
这里需要注意的是
查询可以返回livedata,所以不需要创建子线程
其他的还是需要创建的
不过创建也很简单,在Repository一并创建实现即可
第二步便是我们的Viewmodel类了
class StudentViewModel(application: Application) :
AndroidViewModel(application) {
var studentRepository: StudentRepository? = null
//
init {
this.studentRepository = StudentRepository(application)
}
fun InsertStuDent() {
studentRepository?.InsertStuDent()
}
fun InsertStuDent2(student: Student) {
studentRepository?.InsertStuDent2(student)
}
fun DeleteStuDent(student: Student) {
studentRepository?.DeleteStuDent(student)
}
fun UpDateStuDent(student: Student) {
studentRepository?.UpDateStuDent(student)
}
fun DeleteAllStuDent() {
studentRepository?.DeleteAllStuDent()
}
fun getAllStuDentLive(): LiveData<List<Student>>? {
return studentRepository?.getAllStuDent()
}
}
也没有什么太多东西,获得studentRepository
和正常Viewmodel写法一样
最后,Activity里就简单多了
使用viewmodel进行这些操作就好了
class MainActivity : AppCompatActivity() {
lateinit var studentViewModel: StudentViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main3)
studentViewModel =
ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application)).get(
StudentViewModel::class.java
)
studentViewModel.getAllStuDentLive()?.observe(this, Observer {
tvlist.text = it.toString()
})
studentViewModel.InsertStuDent()
}
}
Room升级数据库
数据库升级也是很常见的操作
Room给我们提供了方便的升级操作
在DataBase的getInstance方法里
我们会对DataBase进行初始化操作
也就是Room.databaseBuilder()
这里可以增加一个方法
addMigrations
这个方法就是用来做数据库升级的
这个方法里面可以传入若干个Migration类
我们继承这个类,实现其中的migrate方法
在方法里做我们的升级操作
例如,我们从版本1,升级到版本2,
需要新增一个性别sex字段
这时候就可以这样写我们的升级类:
MIGRATION_老版本号_新版本号
示例代码如下:
var MIGRATION_1_2: Migration? = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Student ADD COLUMN sex INTEGER NOT NULL DEFAULT 1")
}
}
然后初始化处加上这一行,把我们写好的升级类传进去即可
.addMigrations(MIGRATION_1_2)
当然,我们的表里面自然也要加上这个字段
//V2.0
@ColumnInfo(name = "sex", typeAffinity = ColumnInfo.INTEGER)
var sex: Int = 1
这个升级的类,可以以内部类的方式放在Database类里面
方便我们查看升级执行的具体操作
关于Room数据库的升级就是这样
Room升级注意事项
这里有几点需要注意的
1、如果用户当前app的数据库版本为1
然后安装了一个数据库版本为3的新安装包
Room在升级数据库时
会先判断当前有没有直接从1到3的升级方案MIGRATION_1_3
如果有,就会执行MIGRATION_1_3方法
如果没有,则会先执行MIGRATION_1_2
然后执行MIGRATION_2_3,从而完成数据库的升级
2、异常处理
如果将数据库升级为4,但是却没有为此写相应的Migration
则会出现一个IllegalStateException异常
Database初始化处可以加入fallbackToDestructiveMigration方法
该方法会在出现异常时重建数据库,避免程序崩溃
当然,此时数据也会消失。
但是并不推荐在调试时打开,因为出问题时不好体现
3、还有类似几个方法,
可以在初始化时指定版本或者降级
.fallbackToDestructiveMigrationFrom()
.fallbackToDestructiveMigrationOnDowngrade()
schema文件
在我们Database的头部注解里,有一个exportSchema字段
@Database(entities = arrayOf(Student::class), version = 3, exportSchema = false)
这个字段是一个开关
决定是否导出数据库修改记录
Room的这个数据库记录很有用
他帮助我们记录了我们数据库的一些操作记录
也方便我们按照它的代码风格去写代码
把这个开关打开,置为true
然后在我们的build.gradle的defaultConfig里面这样配置
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]//指定schema导出位置
}
}
这里指定数据库修改记录导出到我们的项目工程下
然后我们build一下项目,执行代码,就发现工程下多了这个文件夹
这个2.json就是代表数据库从版本1升级到版本2后的记录
我把里面代码贴一下,大家可以看到
Room是如何给我们自动执行数据库代码的
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1b4a897adcc287d341431350e72ed91d",
"entities": [
{
"tableName": "Student",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`age` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "age",
"columnName": "age",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sex",
"columnName": "sex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b4a897adcc287d341431350e72ed91d')"
]
}
}
我们可以看到createSql里面是如何书写创建数据表的代码的
这里也需要提一个坑
就是Room里面
INTEGER字段类型创建时一定要记得后面加上NOT NULL
因为Room认定INTEGER不能为空
可以看到他自己创建数据表的代码,也是这样写的
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`age` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
所以我们建议在调试时
打开这个exportSchema的开关
方便我们自己对比代码和定位问题
销毁和重建
如果想将Student表中sex字段由Integer改为text类型
最好的是采用销毁重建策略
大致步骤如下
1创建一张符合表结构要求的临时表temp_Student
2将数据从旧表Student复制到临时表temp_Student
3删除旧表Student
4将临时表temp_Student重命名为Student
示例代码如下:
var MIGRATION_2_3: Migration? = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
//1创建一张符合表结构要求的临时表temp_Student,这个sql语句可以在schema日志中复制过来
database.execSQL("CREATE TABLE temp_Student (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name TEXT NOT NULL,age INTEGER NOT NULL default 1, sex TEXT NOT NULL)")
//2将数据从旧表Student复制到临时表temp_Student,id自增长,所以不需要复制
database.execSQL("insert into temp_Student (age,sex,name) select age,sex,name from Student")
//3删除旧表Student
database.execSQL("drop table Student")
//4将临时表temp_Student重命名为Student
database.execSQL("alter table temp_Student rename to Student")
}
}
预填充数据
在Database初始化时加入.createFromFile()
或者 .createFromAsset()方法
可以预填充数据
一个是从文件中读取预存的数据库数据
一个是从Asset中读取预存的数据库数据
最后,关于Room就先讲到这里了