前言
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
新手,請查閱我們之前的文章: