使用 Room DAO 访问数据

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

通过使用 DAO(而不是查询构建器或直接 查询)来访问应用的数据库,您可以使 关注点保持分离,这是一项关键的架构 原则。DAO 还可让您在测试应用时模拟数据库访问。

DAO 剖析

您可以将每个 DAO 定义为一个接口或一个抽象类。对于基本用例,您通常应使用接口。无论是哪种情况,您都必须始终 使用 @Dao 为您的 DAO 添加注解。DAO 不具有属性,但它们定义了一个或多个函数,可用于与应用数据库中的数据进行交互。

以下代码是一个 DAO 示例,该 DAO 定义了用于在 Room 数据库中插入、删除和选择 User 对象的函数:

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

    @Delete
    suspend fun delete(user: User)

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

有两种类型的 DAO 函数用于定义数据库交互:

  • 可让您在不编写任何 SQL 代码的情况下插入、更新和删除数据库中的行的便捷函数。
  • 可让您编写自己的 SQL 查询以与数据库进行交互的查询函数。

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

便捷函数

Room 提供了方便的注解,用于定义无需编写 SQL 语句即可执行插入、更新和删除操作的函数。

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

插入

借助 @Insert 注解,您可以定义将 参数插入到数据库中相应表的函数。以下代码展示了有效的 @Insert 函数示例,这些函数会将一个或多个 User 对象插入到数据库中:

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

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

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

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

如果 @Insert 函数收到单个参数,则可以返回 Long 值,该值是插入项的新 rowId。如果参数是数组或集合,则应返回 Long 值的数组或集合,其中每个值都是插入项之一的 rowId。 如需详细了解如何返回 rowId 值,请参阅 @Insert 注解的参考文档以及 rowid 表的 SQLite 文档

更新

借助 @Update 注解,您可以定义用于更新数据库表中特定 行的函数。与 @Insert 函数一样,@Update 函数接受数据实体实例作为参数。以下代码展示了一个 @Update 函数示例,该函数尝试更新数据库中的一个或多个 User 对象:

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

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

@Update 函数可以选择性地返回一个 Int 值,该值表示成功更新的行数。

删除

借助 @Delete 注解,您可以 定义用于从数据库表中删除特定行的函数。与 @Insert 函数一样,@Delete 函数接受数据实体实例作为参数。以下代码展示了一个 @Delete 函数示例,该函数尝试从数据库中删除一个或多个 User 对象:

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

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

@Delete 函数可以选择性地返回一个 Int 值,该值表示成功删除的行数。

更新/插入

借助 @Upsert 注解,您可以 定义用于在没有匹配行时插入实体实例,或者 在具有相同主键的行已存在时更新实体实例的函数。

@Insert@Update 函数一样,@Upsert 函数接受数据实体实例作为参数。以下代码展示了一个 @Upsert 函数示例,该函数尝试在数据库中更新/插入一个或多个 User 对象:

@Dao
interface UserDao {
    @Upsert
    suspend fun upsertUsers(vararg users: User)
}

如果 @Upsert 函数收到单个参数,则可以返回 Long 值。如果插入了新行,则返回新插入行的 rowId。如果更新了现有行,则返回 -1。如果参数是数组或集合,则应返回 Long 值的数组或集合。

查询函数

借助 @Query 注解,您可以 编写 SQL 语句并将其公开为 DAO 函数。当您需要从应用的数据库查询数据,或者执行更复杂的插入、更新和删除操作时,可以使用这些查询函数。

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

简单查询

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

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

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

返回表中多个列的子集

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

借助 Room,您可以从任何查询返回数据对象,前提是您可以将一组结果列映射到返回的对象。例如,您可以定义以下对象来保存用户的名字和姓氏:

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

然后,您可以从查询函数返回该数据对象:

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

由于查询会返回 first_namelast_name 列的值,因此 Room 会将这些值映射到 NameTuple 类中的属性。如果查询返回的列未映射到返回对象中的属性,Room 会显示警告。

虽然上一个示例使用自定义数据类来检索列的子集,但当查询返回正好两列或三列时,Room 也支持返回 kotlin.Pairkotlin.Triple 以提供便利。使用这些类型时,列会按照它们在查询语句中定义的顺序进行映射,因此 SELECT 语句中的列顺序必须与 PairTriple 中的类型顺序一致。

将简单参数传递给查询

大多数情况下,DAO 函数需要接受参数,以便执行过滤操作。Room 支持在查询中将函数参数用作绑定参数。

例如,以下代码定义了一个函数,该函数返回所有年龄超过特定年龄的用户:

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

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

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

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

将一组参数传递给查询

某些 DAO 函数可能要求您传入数量不定的参数,参数的数量要到运行时才知道。如果参数表示集合,则会在运行时根据值的数量自动展开。

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

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

查询多个表

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

以下代码定义了一个函数,该函数将三个表联接在一起,以返回当前借给特定用户的图书:

@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
    """
)
suspend fun findBooksBorrowedByName(userName: String): List<Book>

您还可以定义数据对象以从多个联接表返回列的子集。如需了解详情,请参阅 返回表中多个列的子集。 以下代码定义了一个 DAO,其中包含一个函数,该函数返回用户的姓名以及他们借阅的图书的名称:

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

data class UserBook(val userName: String, val bookName: String)

返回多重映射

对于联接操作,您还可以通过编写返回 多重映射的查询函数来查询多个表中的列,而无需 定义其他数据类。

请参考查询多个表中的示例。您可以直接从查询函数返回 UserBook 的映射,而不是返回包含 UserBook 实例配对的自定义数据类实例列表:

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

查询函数返回多重映射时,您可以编写使用 GROUP BY 子句的查询,以便利用 SQL 的功能进行高级计算和过滤。例如,您可以修改 loadUserAndBookNames 函数,使其仅返回借阅了三本或更多图书的用户:

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

如果您不需要映射整个对象,还可以通过在返回类型的泛型参数中使用 @MapColumn 注解来返回查询中特定列之间的映射。

@Query(
    """
    SELECT user.name AS username, book.name AS bookname FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNamesColumns(): Map<
    @MapColumn(columnName = "username") String,
    List<@MapColumn(columnName = "bookname") String>
    >

特殊返回值类型

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

使用 Paging 库将查询分页

Room 通过与 Paging 库集成来支持分页查询。如需使用 Paging 3 返回值类型,您必须在数据库或 DAO 中注册 Paging 返回值类型转换器:

  1. 在 build 配置中添加 androidx.room3:room3-paging 工件。
  2. 使用 @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) 为您的 @Database@Dao 声明添加注解。

注册后,您的 DAO 可以返回 PagingSource 对象以便与 Paging 3 搭配使用:

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

如需详细了解如何为 PagingSource 选择类型参数,请参阅 选择键和值类型

直接数据库连接访问

如果应用的逻辑需要直接、低级别的数据库连接访问,则可以使用 Room 的连接 API。您可以使用 连接以进行只读操作,或使用 useReaderConnectionRoomDatabase实例进行写入操作,并使用 useWriterConnection 执行语句:usePrepared

val result: List<Pair<Long, String>> =
    roomDatabase.useReaderConnection { connection ->
        connection.usePrepared(
            "SELECT * FROM user WHERE age > :minAge LIMIT 5"
        ) { stmt ->
            // Bind arguments if needed
            stmt.bindLong(1, minAge.toLong())
            buildList {
                // Step through the results
                while (stmt.step()) {
                    add(stmt.getLong(0) to stmt.getText(1))
                }
            }
        }
    }

如果您需要在 连接上直接执行低级别数据库事务,则可以在 useWriterConnection 块内的 Transactor 实例上使用 immediateTransactiondeferredTransactionexclusiveTransaction 帮助程序函数:

roomDatabase.useWriterConnection { transactor ->
    transactor.immediateTransaction {
        // Perform transactional database operations using transactor
    }
}

或者,如果您只需要在 事务中执行高级别 DAO 操作,请在 RoomDatabase 实例上使用 withReadTransactionwithWriteTransaction 帮助程序扩展函数:

// Perform transactional read operations (DEFERRED transaction)
val userCount = roomDatabase.withReadTransaction {
    userDao.countUsers()
}

// Perform transactional write operations (IMMEDIATE transaction)
roomDatabase.withWriteTransaction {
    userDao.insert(newUser)
    userDao.update(existingUser)
}