(原创)Jetpack系列(三):Room

前言

今天继续讲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就先讲到这里了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值