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

13kmsteady發表於2019-03-31

原文:medium.com/androiddeve…
作者:Florina Muntenescu

前言

Room 在 SQLite 上提供了一個抽象層,方便開發者更加容易的儲存資料。如果您之前不曾接觸過 Room,請先閱讀下面的入門文章: 7-steps-to-room

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

  • 通過 RoomDatabase#Callback 為 Room 設定預設資料
  • 使用 Dao 的繼承功能
  • 在具有最少樣本程式碼的事務中執行查詢
  • 只查詢你需要的資料
  • 使用 外來鍵 約束實體類之間的關係
  • 通過 @Relation 簡化一對多的查詢
  • 避免 可觀察查詢 的錯誤通知

1. 為 Room 設定預設資料

當新建或者開啟資料庫之後,您是否需要為其設定預設資料?使用 RoomDataBase#Callback 即可。構建 RoomDataBase 時呼叫 addCallback 方法,並重寫 onCreate 或者 onOpen

在建立表之後,首次建立資料庫將呼叫 onCreate。開啟資料庫時呼叫 onOpen。由於只有在這些方法返回後,才能訪問 Dao,通過建立一個新的執行緒,獲取資料庫的引用,繼而得到 Dao,並插入資料。

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 時,如果您的應用程式在第一次啟動時崩潰,在資料庫建立和插入之間,將永遠不會插入資料。

2. 使用 Dao 的繼承功能

您的資料庫中是否有多張表,並且發現自己正在複製相同的 insertupdatedelete 方法。Dao 支援繼承功能,建立一個 BaseDao<T> 類,並宣告通用的 @Insert@Update@Delete 方法。讓每個 Dao 繼承自 BaseDao 並新增每個 Dao 特定的方法。

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

點選檢視完整 示例

Dao 必須是介面或者抽象類,因為 Room 在編譯期間生成他們的實現類,包括 BaseDao 中的方法。

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 的遷移

相關文章