[譯] 關於 Room 的 7 點專業提示
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 的繼承功能
您的資料庫中是否有多張表,並且發現自己正在複製相同的insert
,update
,delete
方法。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
的外來鍵功能。它會跨表強制執行約束,以確保在修改資料庫時關係有效。在實體類中,定義要引用的父實體
,父實體的列
以及當前實體中的列
。
思考User
和Pet
類。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_ACTION
,RESTRICT
,SET_NULL
,SET_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
將查詢Users
和Pets
表並處理物件對映。
@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
表發生與您感興趣的使用者,無關的其他操作(刪除,更新或插入)時,您也將獲得相同的物件,從而導致錯誤通知。更重要的是,如果涉及到多表查詢,那麼只要其中的一個表發生變化,您將會獲得新的物件。
這是幕後發生的事情:
-
每當表中發生
DELETE
,UPDATE
,INSERT
時,SQLite
將觸發觸發器 。 -
Room
建立一個InvalidationTracker
,它使用Observers
跟蹤觀察到的表中發生了什麼變化。 -
LiveData
和Flowable
查詢都依賴於InvalidationTracker.Observer#onInvalidated
通知。收到此通知後,將觸發重新查詢。
Room
只知道表已經被修改,但不知道為什麼和修改了什麼。因此,在重新查詢後,查詢到的結果將由LiveData
和Flowable
發射。由於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
新手,請查閱我們之前的文章: