Room官方文档(翻译)4.使用Room DAOs访问数据

1,152 阅读8分钟

为了使用 Room 访问 app 数据,请使用 DAOs(data access objects).这些 Dao 对象集合构成了 Room 的主要组件,每个 DAO 包含访问数据库的抽象方法。 使用 DAO 访问数据库替代直接查询,可以做到数据库架构的组件分离。此外,DAOs 可以在你测试 app 时更方便的 mock 数据。

Note: 配置依赖

DAO既可以是接口也可以是抽象类。如果它是一个抽象类,可以有选择地提供一个构造函数,这个构造使用RommDatabase作为唯一的参数。Room在编译时为每一个DAO创建实现。

Note:如果你没有在builder中调用allowMainThreadQueries()函数,那么就不能在主线程使用Room访问数据库,因为这可能导致主线程被长时间阻塞。异步查询——返回LiveDataFlowable的查询——不受此规则约束。因为它们可以在后台线程进行异步查询。

一些简单好用的方法

有几个简单好用的查询可以使用DAO类来表示,本文档包括几个常见示例。

Insert

如果你创建的DAO类方法使用@Insert注解,Room将生成一个用于在单事务中将所有参数都插入数据库的实现。

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1: User, user2: User)

    @Insert
    fun insertUsersAndFriends(user: User, friends: List<User>)
}

如果被@Insert注解的方法只接收一个参数,那么它将返回一个long值,代表新插入项的rowId。如果接收的是数组或集合,那么返回就是long[]或List。

更多详细信息,请参看@Insert注解的参考文档。以及SQLite文档SQLite documentation for rowid tables.

Update

@Update注解可以简便的修改数据库中作为参数给出的一组实体。它使用了与每个实体主键相匹配的查询。

@Dao
interface MyDao {
    @Update
    fun updateUsers(vararg users: User)
}

虽然通常情况下没有必要,你可以让这个方法返回一个int值,表明数据库中有多少条记录被更新了。

Delete

@Delete注解 可以从数据库中删除一组以参数形式给出的实体。它使用主键查询要删除的实体。

@Dao
interface MyDao {
    @Delete
    fun deleteUsers(vararg users: User)
}

查询信息

@Query是DAO类中主要被使用的注解。它可以让你在数据库中执行读写操作。每个被@Query注解的方法都会在编译期得到检查,因此,如果查询有问题,就会出现编译错误,而不是运行时失败。

Room同样会验证查询的返回值,这样,如果返回对象中的字段名称与查询响应中列名不匹配时,会以以下两种形式给出提示。

  • 如果只有部分字段名匹配,发出警告
  • 如果没有字段名匹配,给出错误

简单查询

@Dao
interface MyDao {
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>
}

这是一个加载所有 User 的简单查询。在编译期,Room知道它是查询 User 表的所有列。如果查询包含语法错误,或者数据库中不存在用户表,Room在你的app编译期间给出直观的错误提示。

将参数传递到查询中

大多数时候,你需要传递参数到查询中以执行过滤操作,例如只显示超过某个年龄的用户。为完成这个任务,在Room注解中使用方法参数。

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>
}

在编译时处理此查询时,Room将:minAge和方法参数中minAge绑定。Room 使用参数名执行匹配,如果不匹配,会编译时抛出错误。

你可以使用传递多个参数或者在查询中多次引用它们。

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    fun findUserWithName(search: String): List<User>
}

返回列的子集

大多数时候,你只需获取实体中很少的几个字段。例如,你的UI中可能只需显示姓和名,而不是用户的完整信息。只获取你需要的几列,节省宝贵的资源,并且查询完成得更快。

Room 允许从查询中返回任何基于Java的对象,只要结果列集可以映射到这个对象。例如,你可以创建下面的对象来查询用户的姓和名

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

现在你可以在查询中使用:

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(): List<NameTuple>
}

Room 理解查询返回first_name和last_name列的值并且这些值被映射到NameTuple的成员变量中。因为,Room能够生成正确的代码。如果查询返回太多列,或者NameTuple不存在的列,Room将给出警告。

Note:这些POJOs同样可以使用@Embedded注解

传递参数集合

有些查询可能需要插入可变数量的参数,只有运行时才知道确切数量的参数。例如,你可能希望从一个区域子集检索处所有用户的信息。当参数是一个集合时Room就知道在运行时根据参数数量将其自动展开

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}

可观察的查询

当执行查询时,你总是希望app的UI能够在数据发生变化时自动更新。要实现这一点,将LiveData作为查询方法的返回值。Room生成所有必要的代码,用于数据库中数据变化时,更新LiveData

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}

Note:从1.0版开始,Room使用查询中访问的表的列表来决定是否更新LiveData实例。

使用RxJava进行响应式查询

Room对RxJava2类型的返回值提供以下支持:

  • @Query方法:Publisher,Flowable,Observable
  • @Insert,@Update,@Delete方法:Complete,Single和Maybe

要使用此功能,请在app的build.gradle中添加依赖

dependencies {
    def room_version = "2.1.0"
    implementation 'androidx.room:room-rxjava2:$room_version'
}

查看该库的当前版本,versions

@Dao
interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    fun loadUserById(id: Int): Flowable<User>

    // Emits the number of users added to the database.
    @Insert
    fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

    // Makes sure that the operation finishes successfully.
    @Insert
    fun insertLargeNumberOfUsers(varargs users: User): Completable

    /* Emits the number of users removed from the database. Always emits at
       least one user. */
    @Delete
    fun deleteAllUsers(users: List<User>): Single<Int>
}

直接访问游标

如果你的app逻辑中要求直接返回行,你可以在查询中返回Cursor对象

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    fun loadRawUsersOlderThan(minAge: Int): Cursor
}

警告:强烈建议不要使用Cursor API,因为它不能保证哪些行存在或行内不包含哪些值。仅当你确实需要使用cursor并且无法轻松重构代码时,才使用此功能

查询多表

有些查询可能需要方法为多张表才能计算出结果。Room允许你编写任意的查询,所以你可以关联表查询。此外,如果响应时可观察数据类型,例如Flowable或者LiveData,Room会检查查询中的表引用是否合法。 以下代码段显示了如何执行表联接以合并包含正在借书的用户的表和包含有关当前借书的数据的表之间的信息:

@Dao
interface MyDao {
    @Query(
        "SELECT * FROM book " +
        "INNER JOIN loan ON loan.book_id = book.id " +
        "INNER JOIN user ON user.id = loan.user_id " +
        "WHERE user.name LIKE :userName"
    )
    fun findBooksBorrowedByNameSync(userName: String): List<Book>
}

你也可以在查询中返回POJOs,例如,你可以写一个查询用于加载用户和他们的宠物名字

@Dao
interface MyDao {
    @Query(
        "SELECT user.name AS userName, pet.name AS petName " +
        "FROM user, pet " +
        "WHERE user.id = pet.user_id"
    )
    fun loadUserAndPetNames(): LiveData<List<UserPet>>

    // You can also define this class in a separate file.
    data class UserPet(val userName: String?, val petName: String?)
}

用Kotlin协程编写异步方法

你可以在DAO方法中添加suspend关键字,已使用Kotlin的协程功能达到异步执行。它能保证无法在主线程执行。

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user")
    suspend fun loadAllUsers(): Array<User>
}

Note:在Room中使用Kotlin协程需要Room 2.1.0,Kotlin 1.3.0,Coroutlines 1.0.0及以上。

本指南也适用于DAO中使用了@Transaction注解的方法。您可以使用此功能从其他DAO方法中构建挂起数据库方法。 这些方法然后在单个数据库事务中运行。

@Dao
abstract class UsersDao {
    @Transaction
    open suspend fun setLoggedInUser(loggedInUser: User) {
        deleteUser(loggedInUser)
        insertUser(loggedInUser)
    }

    @Query("DELETE FROM users")
    abstract fun deleteUser(user: User)

    @Insert
    abstract suspend fun insertUser(user: User)
}

Note:避免在单个数据库事务中执行额外的应用程序端工作,因为Room将事务视为独占事务,并且一次只能执行一个事务。 这意味着包含比必要更多的操作的事务很容易锁定数据库并影响性能。


Room官方文档(翻译)0.概览

Room官方文档(翻译)1.使用Room实体定义数据

Room官方文档(翻译)2.定义对象间的关系

Room官方文档(翻译)3.在数据库中创建视图

Room官方文档(翻译)4.使用Room DAOs访问数据

Room官方文档(翻译)5.迁移数据库

Room官方文档(翻译)6.测试数据库

Room官方文档(翻译)7.使用Room引用复杂数据