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.
上一篇: Room-資料持久化儲存(入門)