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.