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

原文: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 简化一对多的查询
  • 避免 可观察查询 的错误通知

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345