Room-数据持久化存储(进阶)

@

前言

上一篇文章我们讲述room的基本用法.
本篇将讲述: 复杂数据处理, 配合Flow实现全局UI刷新, DB版本升级方式.

Room的其他功能

以下功能了解即可

  • 定义对象间的关系: 有一对多, 多对多什么的. 但博主暂时没有业务需要在 App 中构建过于庞大且复杂的数据库. 而且构建这种关系的对象时, 必须要额外创建一个复合对象来容纳他们.
  • 定义视图: 一般多表关联的复杂查询才需要视图
  • 预填充数据库: 有 应用资源预填充, 文件系统预填充, 数据库的迁移 等

需要了解的, 请看查看官方文档

提示:以下是本篇文章正文内容,下面案例可供参考

一、复杂数据处理?

有的时候我们会对象套对象, 对象套集合, 例如:

@Entity
class RoomTwoEntity {
    @PrimaryKey
    var id: String = ""
    @ColumnInfo
    var nickname: String? = null

	@ColumnInfo
    var imgs: MutableList<Image>? = null    //集合实体

    @ColumnInfo
    var room: RoomEntity? = null	//其他实体
}

class Image(val res: Int)

上面 @ColumnInfo 直接写是不行的. room也不知道它算什么类型.
我们为这些实体单独创建表, 再管理关联关系的话, 相当麻烦
@TypeConverter 会让这件事非常简单.

1. @TypeConverter

类型转换器, 它的本质就是:

插入库表时, 将实体类型转换为 基本类型. 查询时再讲基本类型转换为实体类型; 例如: 对象转换为JSON, Date转换为Long等

第一步: 编写转换器

class MyConverters {
    @TypeConverter
    //Image集合 转换为 Json字符串
    fun imgsToStr(imgList: MutableList<Image>? = null): String? {
        imgList ?: return null
        return Gson().toJson(imgList)
    }

    @TypeConverter
    //Json字符串 转换为 Image集合
    fun strToImgs(imgStr: String?): MutableList<Image>? {
        imgStr ?: return null
        return imgStr.toBeanList()
    }

    @TypeConverter
    //实体 转换为 Json字符串
    fun roomToStr(room: RoomEntity?): String? {
        room ?: return null
        return Gson().toJson(room)
    }

    @TypeConverter
    //Json字符串 转换为 实体
    fun strToRoom(str: String?): RoomEntity? {
        str ?: return null
        return Gson().fromJson(str, RoomEntity::class.java)
    }
}

转换器会根据匹配到的类型, 自动转换

第二步: 使用转换器

@Entity
@TypeConverters(MyConverters::class)
class RoomTwoEntity {

如图所示, 直接在 Entity类上 加上 @TypeConverters 注释即可

OK, 这样就完事了. 可以写测试代码了

注解还可以加在 DataBase上

试想一下, 好多类都有 Image 集合对象, 我每个 Entity 都加注释, 岂不是很麻烦

@Database(entities = [RoomEntity::class, RoomTwoEntity::class], version = 10)
@TypeConverters(MyConverters::class)
abstract class RoomTestDatabase : RoomDatabase() {

这样就可以省去 每个Entity都加注解的烦恼.

对了, 还有个 toBeanList, 它是一个扩展函数, 将json字符串转换为实体集合

inline fun <reified T> String.toBeanList(): MutableList<T> = 
ApiManager.INSTANCE.mGson.fromJson(this, ParameterizedTypeImpl(T::class.java))

//ParameterizedType 
class ParameterizedTypeImpl(private val clz: Class<*>) : ParameterizedType {
    override fun getRawType(): Type = List::class.java
    override fun getOwnerType(): Type? = null
    override fun getActualTypeArguments(): Array<Type> = arrayOf(clz)
}

再给个 Date 转 Long, 其实都一样啦:

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

最后:
db别忘了升级

二、配合Flow全局刷新 UI

比如当前登录用户-我自己. 我修改了我的头像,昵称啥的. 而显示这些资料的地方有好几处, 比如 "我", "个人主页", "资料设置" 等. 它们都要跟着改掉.

此时我们可以将user信息存入数据库中, 然后用Room+Flow

@Query("select * from UserInfo where id = 0")
fun getSelf(): Flow<UserInfo?>

在需要显示的页面中, 订阅它:

lifecycleScope.launch {
    RoomTestDatabase
        .getInstance(applicationContext)
        .roomTwoDao()
        .getSelf()
        .asLiveData()
        .observe(this@SelfActivity){
            mDataBind.user = it
    }
}

此时, 当我们修改了个人资料. 只需要把修改内容同步到数据库即可.

lifecycleScope.launch(Dispatchers.IO) {
    val user = UserInfo().apply {
        imgRes = R.drawable.bg
        name = mDataBind.etName.text.toString()
    }
    RoomTestDatabase.getInstance(applicationContext).roomTwoDao().setSelf(user)
}

然后, 不管之前有几个页面, 在它们重新显示时,都会更新为最新数据.

那么, 这是怎么实现的呢?

只要表中的任何数据发生变化,返回的 Flow 对象就会再次触发查询并重新发出整个结果集。

注意: 这个触发机制是以表为单位. 也就说, 只要当前表 经历了增删改操作, 这个表关联的 所有Flow全部失效,重新查询. 不管我当前数据是不是被修改的那个

distinctUntilChanged:

官方说:
通过将 distinctUntilChanged() 运算符应用于返回的 Flow 对象,可以确保仅在实际查询结果发生更改时通知界面:

但这个效果, 博主没有具体测试. 还没弄明白. 具体写法如下:
然后直接使用 getSelfDistinctUntilChanged() 即可.

@Query("select * from UserInfo where id = 0")
    fun getSelf(): Flow<UserInfo?>
    fun getSelfDistinctUntilChanged() =
        getSelf().distinctUntilChanged()

三、数据库升级

数据库升级需要管理版本号. 它们对这个数字非常敏感, 所以一定不要忘记这个版本号.

@Database(entities = [RoomEntity::class, RoomTwoEntity::class, UserInfo::class], version = 3)

1.清空重建

.fallbackToDestructiveMigration()

简单粗暴, 直接清除原来的表及数据, 按照注解重新建表.
适合开发阶段, 业务及实体经常变

instance = Room.databaseBuilder(
	    context.applicationContext,
	    RoomTestDatabase::class.java,
	    "Test.db" //数据库名称
	)
	    .fallbackToDestructiveMigration() //数据稳定前, 重建.
	    .build()

2.按版本号升级

.addMigrations()

companion object {
        private var instance: RoomTestDatabase? = null
        fun getInstance(context: Context): RoomTestDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context.applicationContext,
                    RoomTestDatabase::class.java,
                    "Test.db" //数据库名称
                )
                    .fallbackToDestructiveMigration() //数据稳定前, 重建.
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3) //版本升级
                    .build()
            }
            return instance!!
        }
    }

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `RoomTwoEntity` (`id` TEXT NOT NULL, `nickname` TEXT, `sex` INTEGER NOT NULL, PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

val MIGRATION_1_3 = object : Migration(1, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
    	database.execSQL("CREATE TABLE `RoomTwoEntity` (`id` TEXT NOT NULL, `nickname` TEXT, `sex` INTEGER NOT NULL, PRIMARY KEY(`id`))")
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Migration()

  • 这个对象应该很好理解吧. 两个参数分别表示前后版本号. 当然它是可以垮版本升级的, 比如 我 MIGRATION_1_3 的写法.
  • database.execSQL: 版本要升级 数据库要做调整. 它就是用来执行这些变化.

垮版本迁移:

例如: 版本2时,我们创建了某表, 版本3时,我们又不用它了. 此时 1 -> 3 我们按顺序升级的话, 需要执行建表, 再执行删表. 是不是白折腾了.
此时我们给出 1 -> 3 的空 Migration() 即可. room会自动判断并执行更快捷的 Migration()

3.建表注意事项:

下面 name 可以是 null, name1 NOT NULL; 此时建表语句的 NOT NULL一定要写上.

var name: String? = null
var name1: String = ""

下面 name 有默认值, 升级建表或加字段时 也要带上.

@ColumnInfo(defaultValue = "123")
var name: String? = null 

4.升级总结

  • 无论增表, 删表, 增改字段等. 都必须要做版本升级 (升号码, 并给Migration)
  • fallbackToDestructiveMigration() 与 addMigrations() 并不冲突, 在room没有找到相匹配的 Migration 时, 会抛出异常. 如果加上 fallbackToDestructiveMigration() 则会将库表清空重建. 也就是说room会优先匹配 Migration. 带上它,会防止部分异常,但也会存在丢失旧数据的风险
  • 单表升级时, 比如增加字段, 只需要执行 database.execSQL("ALTER TABLE ...")
  • 新建表时, 需要执行建表语句, 主键, NOT NULL, DEFAULT等都不要写错. 当然,写错的话 需要仔细比对报错信息, 找出自己升级的语句中的错误, 改掉即可
  • 删表时, 去掉实体@Entity时, 可以给空的升级 Migration(), 也可以执行 DROP TABLE

空 Migration() 如下:

val MIGRATION_1_2 = object : Migration(6, 7) {
    override fun migrate(database: SupportSQLiteDatabase) {
//        database.execSQL("DROP TABLE RoomTwoEntity")
    }
}

总结

实际项目中可以合理的规划 DataBase. 例如不常修改的,配置什么的单独一个DB.
常改的,业务相关的 也可以按业务模块 分BD.


posted @ 2021-06-28 15:18  孟老板  阅读(948)  评论(0编辑  收藏  举报