阅读 719

[译] 关于 Room 的 7 点专业提示

原文: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 的迁移

关注下面的标签,发现更多相似文章
评论