[譯] 關於 Room 的 7 點專業提示

原文:medium.com/androiddeve…
做者:Florina Muntenescuhtml

前言

Room 在 SQLite 上提供了一個抽象層,方便開發者更加容易的存儲數據。若是您以前未曾接觸過 Room,請先閱讀下面的入門文章: 7-steps-to-roomjava

在本文中,我將向你們分享一些關於使用 Room 的專業提示:android

  • 經過 RoomDatabase#Callback 爲 Room 設置默認數據
  • 使用 Dao 的繼承功能
  • 在具備最少樣本代碼的事務中執行查詢
  • 只查詢你須要的數據
  • 使用 外鍵 約束實體類之間的關係
  • 經過 @Relation 簡化一對多的查詢
  • 避免 可觀察查詢 的錯誤通知

1. 爲 Room 設置默認數據

當新建或者打開數據庫以後,您是否須要爲其設置默認數據?使用 RoomDataBase#Callback 便可。構建 RoomDataBase 時調用 addCallback 方法,並重寫 onCreate 或者 onOpengit

在建立表以後,首次建立數據庫將調用 onCreate。打開數據庫時調用 onOpen。因爲只有在這些方法返回後,才能訪問 Dao,經過建立一個新的線程,獲取數據庫的引用,繼而獲得 Dao,並插入數據。github

Room.databaseBuilder(context.applicationContext,
        DataDatabase::class.java, "Sample.db")
        // prepopulate the database after onCreate was called
        .addCallback(object : Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                // moving to a new thread
                ioThread {
                    getInstance(context).dataDao()
                                        .insert(PREPOPULATE_DATA)
                }
            }
        })
        .build()
複製代碼

點擊查看完整 示例數據庫

注意: 使用 ioThread 時,若是您的應用程序在第一次啓動時崩潰,在數據庫建立和插入之間,將永遠不會插入數據。app

2. 使用 Dao 的繼承功能

您的數據庫中是否有多張表,而且發現本身正在複製相同的 insertupdatedelete 方法。Dao 支持繼承功能,建立一個 BaseDao<T> 類,並聲明通用的 @Insert@Update@Delete 方法。讓每一個 Dao 繼承自 BaseDao 並添加每一個 Dao 特定的方法。ide

interface BaseDao<T> {
    @Insert
    fun insert(vararg obj: T)
}
@Dao
abstract class DataDao : BaseDao<Data>() {
    @Query("SELECT * FROM Data")
    abstract fun getData(): List<Data>
}
複製代碼

點擊查看完整 示例post

Dao 必須是接口或者抽象類,由於 Room 在編譯期間生成他們的實現類,包括 BaseDao 中的方法。ui

3. 在具備最少樣板代碼的事務中執行查詢

使用 @Transaction 註解,能夠確保你在該方法中執行的全部數據庫操做,都將在一個事務中運行。

在方法體中拋出異常時,事務將失敗。

@Dao
abstract class UserDao {
    
    @Transaction
    open fun updateData(users: List<User>) {
        deleteAllUsers()
        insertAll(users)
    }
    @Insert
    abstract fun insertAll(users: List<User>)
    @Query("DELETE FROM Users")
    abstract fun deleteAllUsers()
}
複製代碼

在如下狀況,您可能但願對具備查詢語句的 @Query 方法使用 @Transaction 註解。

  • 當查詢結果至關大時,經過在一個事務中查詢數據庫,能夠確保若是查詢結果不適合單個 cursor window,則由數據庫 cursor window wraps致使的數據庫更改,不會被破壞。
  • 當查詢結果是一個包含 @Relation 字段的 POJO時。因爲這些字段是單獨的查詢,所以在單個事務中執行,將保證查詢結果的一致性。

具備多個參數的 @Delete@Update@Insert 方法將自動在事務中執行。

4. 只查詢須要的數據

當您查詢數據庫時,您是否使用查詢結果中返回的全部字段?處理應用程序使用的內存,並僅加載最終使用的字段子集。這還能夠經過下降 IO 成原本提升查詢速度。Room 將爲您執行列和對象以前的映射。

考慮這個複雜的 User 對象:

@Entity(tableName = "users")
data class User(@PrimaryKey
                val id: String,
                val userName: String,
                val firstName: String, 
                val lastName: String,
                val email: String,
                val dateOfBirth: Date, 
                val registrationDate: Date)
複製代碼

在一些屏幕上,咱們並不須要顯示全部的信息。所以,咱們能夠建立一個僅包含所需數據的 UserMinimal 對象。

data class UserMinimal(val userId: String,
                       val firstName: String, 
                       val lastName: String)
複製代碼

Dao 類中,咱們定義查詢語句,並從 users 表中選擇正確的列。

@Dao
interface UserDao {
    @Query(「SELECT userId, firstName, lastName FROM Users)
    fun getUsersMinimal(): List<UserMinimal>
}
複製代碼

5. 使用 外鍵 約束實體類之間的關係

儘管 Room 不直接支持 關係,但它容許您在實體類之間定義外鍵約束。

Room 擁有 @ForeignKey 註解,它是 @Entity 註解的一部分,容許使用 SQLite 的外鍵功能。它會跨表強制執行約束,以確保在修改數據庫時關係有效。在實體類中,定義 要引用的父實體父實體的列 以及 當前實體中的列

思考 UserPet 類。Pet 有一個 owner 字段,它是一個引用爲外鍵的 user id

@Entity(tableName = "pets", foreignKeys = arrayOf( ForeignKey(entity = User::class, parentColumns = arrayOf("userId"),
                       childColumns = arrayOf("owner"))))
data class Pet(@PrimaryKey val petId: String,
              val name: String,
              val owner: String)
複製代碼

(可選)您能夠定義在數據庫中刪除或者更新父實體時要採起的操做。您能夠選擇如下之一: NO_ACTIONRESTRICTSET_NULLSET_DEFAULT, 或者 CASCADE,這與 SQLite 具備相同的行爲。

注意:Room 中,SET_DEFAULT 用做 SET_NULL。由於 Room 尚不容許爲列設置默認值。

6. 經過 @Relation 簡化一對多的查詢

在以前的 User - Pet 示例中,設定存在 一對多 的關係:一個用戶能夠擁有多隻寵物。假設咱們想得到擁有寵物的用戶列表:List<UserAndAllPets>

data class UserAndAllPets (val user: User,
                           val pets: List<Pet> = ArrayList())
複製代碼

要手動執行此操做,咱們須要實現 2 個查詢:獲取全部用戶的列表 和 根據用戶 ID 獲取寵物列表

@Query(「SELECT * FROM Users」)
public List<User> getUsers();

@Query(「SELECT * FROM Pets where owner = :userId」)
public List<Pet> getPetsForUser(String userId);
複製代碼

而後咱們將遍歷用戶列表並查詢 Pets 表。

爲了簡化上述操做,Room 提供 @Relation 註解能夠自動獲取相關實體。@Relation 只能用於 List 或者 Set 對象。修改後的實體類以下所示:

class UserAndAllPets {
   @Embedded
   var user: User? = null
   @Relation(parentColumn = 「userId」, entityColumn = 「owner」)
   var pets: List<Pet> = ArrayList()
}
複製代碼

Dao 中,咱們只需聲明一個查詢。 Room 將查詢 UsersPets 表並處理對象映射。

@Transaction
@Query(「SELECT * FROM Users」)
List<UserAndAllPets> getUsers();
複製代碼

7. 避免可觀察查詢的錯誤通知

假設您但願經過用戶 id 獲取用戶,並將查詢結果做爲一個可觀察的對象返回:

@Query(「SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User>
// or
@Query(「SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): Flowable<User>
複製代碼

每當用戶更新,你將會接收到一個新的 User 對象。可是,當 Users 表發生與您感興趣的用戶,無關的其餘操做(刪除,更新或插入)時,您也將得到相同的對象,從而致使錯誤通知。更重要的是,若是涉及到多表查詢,那麼只要其中的一個表發生變化,您將會得到新的對象。

這是幕後發生的事情:

  1. 每當表中發生 DELETEUPDATEINSERT 時,SQLite 將觸發 觸發器
  2. Room 建立一個 InvalidationTracker,它使用 Observers 跟蹤觀察到的表中發生了什麼變化。
  3. LiveDataFlowable 查詢都依賴於 InvalidationTracker.Observer#onInvalidated 通知。收到此通知後,將觸發從新查詢。

Room 只知道表已經被修改,但不知道爲何和修改了什麼。所以,在從新查詢後,查詢到的結果將由 LiveDataFlowable 發射。因爲 Room 在內存中不保存任何數據,而且不能假設對象具備 equals(),所以沒法判斷這是不是相同的數據。

你須要確保 Dao 可以過濾發射的數據,而且只對不一樣的對象作出響應。

若是使用 Flowable 實現可觀察的查詢,請使用 Flowable#distinctUntilChanged

@Dao
abstract class UserDao : BaseDao<User>() {
/** * Get a user by id. * @return the user from the table with a specific id. */
@Query(「SELECT * FROM Users WHERE userid = :id」)
protected abstract fun getUserById(id: String): Flowable<User>
fun getDistinctUserById(id: String): 
   Flowable<User> = getUserById(id)
                          .distinctUntilChanged()
}
複製代碼

若是你的查詢結果,返回的是一個 LiveData 對象,則可使用 MediatorLiveData。它只容許從數據源發射不一樣的對象。

fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object : Observer<T> {
        private var initialized = false
        private var lastObj: T? = null
        override fun onChanged(obj: T?) {
            if (!initialized) {
                initialized = true
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            } else if ((obj == null && lastObj != null) 
                       || obj != lastObj) {
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            }
        }
    })
    return distinctLiveData
}
複製代碼

Daos 中,定義一個 public 字段修飾,返回不一樣的 LiveData 對象的方法, 以及 protected 字段修飾的查詢數據庫的方法。

@Dao
abstract class UserDao : BaseDao<User>() {

@Query(「SELECT * FROM Users WHERE userid = :id」)
protected abstract fun getUserById(id: String): LiveData<User>

fun getDistinctUserById(id: String): 
         LiveData<User> = getUserById(id).getDistinct()
}
複製代碼

點擊查看完整 示例

注意: 若是返回要顯示的列表,能夠考慮使用 Paging Library 並返回一個 LivePagedListBuilder。由於該庫將自動計算 Item 之間的差別,並更新 UI

若是你是 Room 新手,請查閱咱們以前的文章:

使用 Room 的 7個步驟

Room 🔗 RxJava

瞭解 Room 的遷移

相關文章
相關標籤/搜索