一、前言
使用 Room 库存储应用数据时,通过定义数据访问对象(DAOs)与存储的数据进行交互。每一个 DAO 包含用来访问应用数据库的抽象方法,在编译时, Room 会自动生成并实现在 DAO 中定义的访问方法。
用数据访问对象(而不是使用查询构建起或者直接查询)访问数据库,可以拆分数据库架构中的不同组件。另外,数据访问对象也使得应用在测试阶段可以轻松模拟试数据库访问。
二、Room 数据访问对象详解
2.1 定义数据访问对象类
数据访问对象类(DAO)可以是接口,也可以是抽象方法(一般情况下,使用接口定义DAO)。定义数据访问对象类时,必须使用 @Dao
注解标示,数据访问对象类(DAO)没有属性,内部定义一个或者多个与应用的数据库进行交互的方法。如下示例所示:
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE name LIKE :name")
fun findByName(name: String): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
注意事项:DAO 类可以是接口,也可以是抽象方法,并且 DAO 类不能有成员属性。
2.2 数据访问对象的方法分类
数据访问对象类中定义的与数据库交互的方法可以分为两种类型:
- 便利类型方法(Convenience methods):便利类型方法可以在不需要编写任何 SQL 语句的情况下,对数据库进行插入、更新和删除行数据。
- 查询类型方法(Query methods):查询类型方法允许开发者编写自定义的 SQL 查询语句与数据库进行交互。
2.2.1 DAO 便利类型方法
Room 提供便利型的注解来定义便利方法,这些方法不需要编写任何的 SQL 语句就可以执行简单的插入、更新和删除行数据,与之对应的注解分别是 @Insert
、@Update
和 @Delete
。
说明:如果需要定义更复杂的插入、更新和删除,或者需要从数据库中查询数据,请使用 DAO 查询型方法
2.2.1.1 插入
使用 @Insert
注解标注定义的便利方法,可以将方法参数传入的对象数据写入到数据库对应的表中。 传入 @Insert
方法的参数必须是用 @Entity
注解标注的 Room 数据实体类,或者它的集合,当调用 @Insert
方法时,Room 就会将传入的每个数据实体类对象数据插入到对应的表中。如下示例所示:
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg users: User)
}
如果 @Insert
方法传入参数是单个数据实体类,调用 @Insert
方法时返回新的行id,如果 @Insert
方法传入的参数是数据实体类集合,调用 @Insert
方法时返回插入的行id数组。
注意事项:
1. 传入@Insert
方法的参数必须是用@Entity
注解标注的 Room 数据实体类,或者它的集合;
2.@Insert
方法可通过onConflict
参数指定插入是发生冲突的解决方案(默认值:OnConflictStrategy.ABORT(终止操作,抛出异常))
2.2.1.2 更新
使用 @Update
注解标注定义的便利方法,可以更新数据库表中的指定行的数据。跟 @Insert
类似, 传入 @Update
方法的参数必须是用 @Entity
注解标注的 Room 数据实体类,或者它的集合。如下示例代码所示:
@Dao
interface UserDao {
@Update
fun updateUser(user: User)
@Update
fun updateAll(vararg users: User)
}
@Update
方法执行完成后可以返回一个表示成功更新行数的整形数值。
注意事项:
1. Room 根据主键将传入的数据实体类实例匹配数据库表中的行,如果传入的数据实体类实例在数据库表中找不到匹配的主键值,Room 对该数据不做任何操作。
2.@Update
方法可通过onConflict
参数指定插入是发生冲突的解决方案(默认值:OnConflictStrategy.ABORT(终止操作,抛出异常))
2.2.1.3 删除
使用 @Delete
注解标注定义的便利方法,可以删除数据库表中的指定行的数据。跟 @Insert
类似, 传入 @Delete
方法的参数必须是用 @Entity
注解标注的 Room 数据实体类,或者它的集合。如下示例代码所示:
@Dao
interface UserDao {
@Delete
fun delete(user: User)
@Delete
fun deleteAll(vararg users: User)
}
@Delete
方法执行完成后可以返回一个表示成功更新行数的整形数值。
注意事项:
1. Room 根据主键将传入的数据实体类实例匹配数据库表中的行,如果传入的数据实体类实例在数据库表中找不到匹配的主键值,Room 对该数据不做任何操作。
2.2.2 DAO 查询型方法
Room 提供 @Query
注解定义查询型方法,查询型方法可以在注解中通过 value
参数自定义 SQL 语句,该方法调用时执行 SQL 语句定义的查询操作,查询型方法可用于查询数据库中的数据,或者执行更复杂的插入、更新和删除操作。查询型方法能够实现什么样的操作,完全看 SQL 语句。
注意事项:Room 会在编译时校验 SQL 查询语句,如果 SQL 查询语句存在问题,在编译期间就会出现错误(编译失败),而不是等到运行时才报错。
2.2.2.1 简单查询操作
一个简单的查询就是查询数据库中所有列的数据。
- 示例:
@Query("SELECT * FROM users")
fun getAll(): List<User>
2.2.2.2 查询包含数据表部分列的数据
在某些情况下,我们只需要查询包含数据库表中某些列的值,而不是返回所有列的值。
- 示例:
// 这里的数据类不是数据实体类,无需给 Room 解析对应数据表,所以无需添加 @Entity 注解。
// 数据类的属性名如果跟数据库表中列名不一致,需要使用 @ColumnInfo 注解指定对应列名的名称。
data class NameAge(@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "age") val age: Int )
// 在 DAO 中定义查询方法,返回的结果类型必须是根据 SQL 语句中指定列定义的实体类
@Query("SELECT name, age FROM users")
fun findAllWithNameAge(): List<NameAge>
解析:上面的例子中返回只包含数据库表中
name
和age
这两个字段值的结果。
注意事项:
1. 必须根据需要查询的列定义一个拥有相同属性的实体类,这个实体类不是数据实体类,无需添加 @Entity 注解。
2. 数据类的属性名如果跟数据库表中列名不一致,需要使用 @ColumnInfo 注解指定对应列名的名称。
2.2.2.3 传入简单参数的查询
如果根据不定的条件查询数据,那么可以将此条件作为 DAO 方法的参数传入,在 @Query
注解中通过 “冒号 + 参数名称” 的形式引用 DAO 方法中的参数。带条件的插叙可以使用 SQL 的 WHERE
语法实现。
- 示例:
@Query("SELECT * FROM users WHERE name LIKE :name")
fun findByName(name: String): List<User>
注意事项:在
@Query
注解中通过 “冒号 + 参数名称” 的形式引用 DAO 方法中的参数
2.2.2.3 传入集合参数的查询
如果根据不定的条件查询数据,并且查询条件是一个集合类型的条件,同样可以将此集合条件作为 DAO 方法的参数传入,在 @Query
注解中通过 “冒号 + 参数名称” 的形式引用 DAO 方法中的参数。带条件的插叙可以使用 SQL 的 WHERE
语法实现,集合条件使用 SQL de IN 、
语法
- 示例:
@Query("SELECT * FROM users WHERE name IN (:names)")
fun findByNames(vararg names: String): List<User>
注意事项:在
@Query
注解中通过 “冒号 + 参数名称” 的形式引用 DAO 方法中的参数,in 语句语法需要使用括号将集合包裹起来。
2.2.2.4 跨表查询
有时候我们需要跨表查询,跨表查询使用 SQL 的 JOIN
语法,JOIN
语法的格式是 “JOIN 表名 ON 表间连接条件”。
@Entity(tableName = "users")
data class User(@PrimaryKey val uid: Int, @ColumnInfo(name = "name") val name: String, val age: Int, val tid: Int)
@Entity
data class Teacher(@PrimaryKey val tid: Int, @ColumnInfo val name: String) {
@ColumnInfo var subject: String? = null
}
@Dao
interface UserDao {
@Query("SELECT * FROM users INNER JOIN teacher ON users.tid = teacher.tid WHERE teacher.name like :teacherName")
fun findUserByTeacher(teacherName: String): List<User>
}
注意事项:跨表查询是有条件的,那就是这些表之间是有相互关联的(可以通过某个列的值关联起来)。
三、编后语
在数据访问对象类中定义的查询语句,特别是复杂查询,需要有扎实的 SQL 基础,读者如果 SQL 基础欠缺,可以自行学些下 SQL 相关的内容。