使用 Room DAO 访问数据

当您使用 Room 持久性库来存储应用的数据时,您可以通过定义数据访问对象或 DAO 来与存储的数据进行交互。每个 DAO 都包含提供对应用数据库的抽象访问的方法。在编译时,Room 会自动生成您定义的 DAO 的实现。

通过使用 DAO 而不是查询生成器或直接查询来访问应用的数据库,您可以保留关注点分离,这是一个重要的架构原则。当您测试应用时,DAO 还可以让您更轻松地模拟数据库访问。

DAO 的结构

您可以将每个 DAO 定义为接口或抽象类。对于基本用例,您通常使用接口。在这两种情况下,您都必须始终使用@Dao注释您的 DAO。DAO 没有属性,但它们确实定义了一个或多个用于与应用数据库中的数据交互的方法。

以下代码是一个简单的 DAO 示例,它定义了在 Room 数据库中插入、删除和选择User对象的方法

Kotlin

@Dao
interface UserDao {
    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)

    @Query("SELECT * FROM user")
    fun getAll(): List<User>
}

Java

@Dao
public interface UserDao {
    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);

    @Query("SELECT * FROM user")
    List<User> getAll();
}

有两种类型的 DAO 方法定义数据库交互

  • 便利方法,允许您在不编写任何 SQL 代码的情况下插入、更新和删除数据库中的行。
  • 查询方法,允许您编写自己的 SQL 查询来与数据库交互。

以下部分演示了如何使用两种类型的 DAO 方法来定义应用程序所需的数据库交互。

便捷方法

Room 提供了便捷的注解来定义执行简单插入、更新和删除的方法,而无需编写 SQL 语句。

如果您需要定义更复杂的插入、更新或删除操作,或者需要查询数据库中的数据,请改用 查询方法

插入

@Insert 注解允许您定义将参数插入数据库中相应表的方法。以下代码显示了有效 @Insert 方法的示例,这些方法将一个或多个 User 对象插入数据库

Kotlin

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

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

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

Java

@Dao
public interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

@Insert 方法的每个参数必须是 Room 数据实体类(用 @Entity 注解)的实例,或者数据实体类实例的集合,每个实例都指向一个数据库。调用 @Insert 方法时,Room 会将传递的每个实体实例插入到相应的数据库表中。

如果 @Insert 方法接收单个参数,则它可以返回一个 long 值,该值是插入项目的新的 rowId。如果参数是数组或集合,则改为返回 long 值的数组或集合,每个值作为插入项目之一的 rowId。要了解有关返回 rowId 值的更多信息,请参阅 @Insert 注解的参考文档和 SQLite 行 ID 表文档

更新

@Update 注解允许您定义更新数据库表中特定行的方法。与 @Insert 方法类似,@Update 方法接受数据实体实例作为参数。以下代码显示了一个 @Update 方法的示例,该方法尝试更新数据库中一个或多个 User 对象

Kotlin

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

Java

@Dao
public interface UserDao {
    @Update
    public void updateUsers(User... users);
}

Room 使用 主键 将传递的实体实例与数据库中的行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

@Update 方法可以选择返回一个 int 值,指示成功更新的行数。

删除

@Delete 注解允许您定义从数据库表中删除特定行的方法。与 @Insert 方法类似,@Delete 方法接受数据实体实例作为参数。以下代码显示了一个 @Delete 方法的示例,该方法尝试从数据库中删除一个或多个 User 对象

Kotlin

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

Java

@Dao
public interface UserDao {
    @Delete
    public void deleteUsers(User... users);
}

Room 使用 主键 将传递的实体实例与数据库中的行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。

@Delete 方法可以选择返回一个 int 值,指示成功删除的行数。

查询方法

@Query 注解允许您编写 SQL 语句并将它们作为 DAO 方法公开。使用这些查询方法从应用程序的数据库中查询数据,或者在您需要执行更复杂的插入、更新和删除操作时使用。

Room 在编译时验证 SQL 查询。这意味着,如果查询存在问题,则会发生编译错误,而不是运行时错误。

简单查询

以下代码定义了一个方法,该方法使用简单的 SELECT 查询返回数据库中的所有 User 对象

Kotlin

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

Java

@Query("SELECT * FROM user")
public User[] loadAllUsers();

以下部分演示了如何针对典型用例修改此示例。

返回表列的子集

大多数情况下,您只需要返回查询的表中列的子集。例如,您的 UI 可能会仅显示用户的姓氏和名字,而不是该用户的每个详细信息。为了节省资源并简化查询的执行,只需查询您需要的字段。

只要您可以将结果列集映射到返回的对象,Room 就可以让您从任何查询中返回一个简单对象。例如,您可以定义以下对象来保存用户的姓氏和名字

Kotlin

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

Java

public class NameTuple {
    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    @NonNull
    public String lastName;
}

然后,您可以从查询方法中返回该简单对象

Kotlin

@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>

Java

@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();

Room 了解查询返回 first_namelast_name 列的值,并且这些值可以映射到 NameTuple 类中的字段。如果查询返回的列没有映射到返回对象中的字段,Room 会显示警告。

将简单参数传递给查询

大多数情况下,您的 DAO 方法需要接受参数,以便它们可以执行过滤操作。Room 支持使用方法参数作为查询中的绑定参数。

例如,以下代码定义了一个方法,该方法返回所有年龄超过某个年龄的用户

Kotlin

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

Java

@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);

您还可以传递多个参数或在查询中多次引用同一个参数,如下面的代码所示

Kotlin

@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>

Java

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

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

将参数集合传递给查询

某些 DAO 方法可能需要您传入数量可变的参数,这些参数在运行时之前是未知的。Room 了解参数何时表示集合,并在运行时根据提供的参数数量自动扩展它。

例如,以下代码定义了一个方法,该方法返回来自部分区域的所有用户信息

Kotlin

@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>

Java

@Query("SELECT * FROM user WHERE region IN (:regions)")
public List<User> loadUsersFromRegions(List<String> regions);

查询多个表

某些查询可能需要访问多个表才能计算结果。您可以在 SQL 查询中使用 JOIN 子句来引用多个表。

以下代码定义了一个方法,该方法将三个表连接在一起以返回当前借给特定用户的书籍

Kotlin

@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>

Java

@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")
public List<Book> findBooksBorrowedByNameSync(String userName);

您还可以定义简单对象以返回来自多个连接表的列的子集,如 返回表列的子集 部分所述。以下代码定义了一个 DAO,其中包含一个方法,该方法返回用户姓名和他们借阅的书籍的名称

Kotlin

interface UserBookDao {
    @Query(
        "SELECT user.name AS userName, book.name AS bookName " +
        "FROM user, book " +
        "WHERE user.id = book.user_id"
    )
    fun loadUserAndBookNames(): LiveData<List<UserBook>>

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

Java

@Dao
public interface UserBookDao {
   @Query("SELECT user.name AS userName, book.name AS bookName " +
          "FROM user, book " +
          "WHERE user.id = book.user_id")
   public LiveData<List<UserBook>> loadUserAndBookNames();

   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserBook {
       public String userName;
       public String bookName;
   }
}

返回多值映射

在 Room 2.4 及更高版本中,您还可以通过编写返回 多值映射 的查询方法,从多个表中查询列,而无需定义其他数据类。

考虑 查询多个表 部分中的示例。与其返回自定义数据类的实例列表,这些实例保存 UserBook 实例的配对,不如直接从查询方法中返回 UserBook 的映射

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<User, List<Book>> loadUserAndBookNames();

当您的查询方法返回多值映射时,您可以编写使用 GROUP BY 子句的查询,从而利用 SQL 的高级计算和过滤功能。例如,您可以修改 loadUserAndBookNames() 方法,使其仅返回借阅了三本或更多书籍的用户

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
public Map<User, List<Book>> loadUserAndBookNames();

如果您不需要映射整个对象,还可以通过在查询方法的 @MapInfo 注解中设置 keyColumnvalueColumn 属性,在查询中返回特定列之间的映射

Kotlin

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<String, List<String>>

Java

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<String, List<String>> loadUserAndBookNames();

特殊返回类型

Room 为与其他 API 库集成提供了一些特殊返回类型。

使用 Paging 库进行分页查询

Room 通过与 Paging 库 集成支持分页查询。在 Room 2.3.0-alpha01 及更高版本中,DAO 可以返回 PagingSource 对象以与 Paging 3 一起使用。

Kotlin

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  fun pagingSource(query: String): PagingSource<Int, User>
}

Java

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);
}

有关为 PagingSource 选择类型参数的更多信息,请参阅 选择键和值类型

直接光标访问

如果应用程序的逻辑需要直接访问返回的行,则可以编写 DAO 方法以返回 Cursor 对象,如下面的示例所示

Kotlin

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

Java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

其他资源

要了解有关使用 Room DAO 访问数据的更多信息,请参阅以下其他资源

示例

Codelabs