原文:https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1
作者:Florina Muntenescu
Room 在 SQLite 上提供了一个抽象层,方便开发者更加容易的存储数据。如果您之前不曾接触过 Room
,请先阅读下面的入门文章:
7-steps-to-room
在本文中,我将向大家分享一些关于使用 Room 的专业提示:
RoomDatabase#Callback
为 Room 设置默认数据Dao
的继承功能@Relation
简化一对多的查询当新建或者打开数据库之后,您是否需要为其设置默认数据?使用 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
时,如果您的应用程序在第一次启动时崩溃,在数据库创建和插入之间,将永远不会插入数据。
您的数据库中是否有多张表,并且发现自己正在复制相同的 insert
,update
,delete
方法。Dao
支持继承功能,创建一个 BaseDao
类,并声明通用的 @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
中的方法。
使用 @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
方法将自动在事务中执行。
当您查询数据库时,您是否使用查询结果中返回的所有字段?处理应用程序使用的内存,并仅加载最终使用的字段子集。这还可以通过降低 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>
}
尽管 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
尚不允许为列设置默认值。
在之前的 User
- Pet
示例中,设定存在 一对多 的关系:一个用户可以拥有多只宠物。假设我们想获得拥有宠物的用户列表:List
。
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();
假设您希望通过用户 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
新手,请查阅我们之前的文章:
使用 Room 的 7个步骤
Room ? RxJava
了解 Room 的迁移