1. 程式人生 > 其它 >Room-資料持久化儲存(進階)

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-資料持久化儲存(入門)