当您使用 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_name 和 last_name 列的值,因此 Room 会将这些值映射到 NameTuple 类中的属性。如果查询返回的列未映射到返回对象中的属性,Room 会显示警告。
虽然上一个示例使用自定义数据类来检索列的子集,但当查询返回正好两列或三列时,Room 也支持返回 kotlin.Pair 和 kotlin.Triple 以提供便利。使用这些类型时,列会按照它们在查询语句中定义的顺序进行映射,因此 SELECT 语句中的列顺序必须与 Pair 或 Triple 中的类型顺序一致。
将简单参数传递给查询
大多数情况下,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)
返回多重映射
对于联接操作,您还可以通过编写返回 多重映射的查询函数来查询多个表中的列,而无需 定义其他数据类。
请参考查询多个表中的示例。您可以直接从查询函数返回 User 和 Book 的映射,而不是返回包含 User 和 Book 实例配对的自定义数据类实例列表:
@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 返回值类型转换器:
- 在 build 配置中添加
androidx.room3:room3-paging工件。 - 使用
@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。您可以使用
连接以进行只读操作,或使用
useReaderConnection
对RoomDatabase实例进行写入操作,并使用
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 实例上使用 immediateTransaction、
deferredTransaction 或 exclusiveTransaction 帮助程序函数:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
或者,如果您只需要在
事务中执行高级别 DAO 操作,请在 RoomDatabase 实例上使用 withReadTransaction 或 withWriteTransaction
帮助程序扩展函数:
// 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) }