Android Room 数据访问对象(DAO)详解
一、前言
使用 Room 库存储应用数据时,通过定义数据访问对象(DAOs)与存储的数据进行交互。每一个 DAO 包含用来访问应用数据库的抽象方法,在编译时, Room 会自动生成并实现在 DAO 中定义的访问方法(如果不了解 Room 库,请先阅读:Android Room 库基础入门)。
用数据访问对象(而不是使用查询构建起或者直接查询)访问数据库,可以拆分数据库架构中的不同组件。另外,数据访问对象也使得应用在测试阶段可以轻松模拟试数据库访问。
二、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>@Insertfun insertAll(vararg users: User)@Deletefun 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 {@Updatefun updateUser(user: User)@Updatefun 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 {@Deletefun delete(user: User)@Deletefun 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>
}
注意事项:跨表查询是有条件的,那就是这些表之间是有相互关联的(可以通过某个列的值关联起来)。
2.3 编写异步的 DAO 查询
为了防止数据查询操作阻断 UI,造成 UI 卡顿,Room 不允许在 UI 主线程中访问数据库。这就意味着开发者必须将 DAO 中的查询定义成异步的。Room 库包含与多阿哥不同的框架进行集成,提供异步查询支持。
数据库查询可分为三类
- 一次性写查询:往数据库中插入、更新或者删除数据;
- 一次性读查询:单次从数据库读取数据,并且返回查询执行时数据库快照对应的结果;
- 观察者读查询:每次底层数据库表发生变更时,从数据库中读取新数据,并用新数据体现这些变更。
2.3.1 编程语言和框架选项
Room 为与特定编程语言的功能和库的相互操作提供集成支持。下表是基于查询类型和框架返回的适用的类型。
查询类型 | Kotlin 语言功能 | RxJava框架 | Guava语言 | Jetpack Lifecycle |
---|---|---|---|---|
一次性写查询 | Coroutines(suspend) | Single, Maybe, Completable | ListenableFuture | 无 |
一次性读查询 | Coroutines(suspend) | Single, Maybe | ListenableFuture | 无 |
观察者读查询 | Flow | Flowable, Publisher, Observable | 无 | LiveData |
2.3.1.1 Kotlin 的 Flow 和协程
Kotlin 提供语言功能允许你无需使用第三方框架实现异步的写查询操作。
- 在 Room 2.2 及更高版本,你可以使用 Kotlin 的 Flow 功能编写可观察查询。
- 在 Room 2.1 及更高版本,你可以使用 Kotlin 的协程的
suspend
关键字让你的 DAO 实现异步查询。
注意事项:
1. 在 Room 中使用 Kotlin 的 Flow 或者协程,必须在项目程序模块的build.gradle
脚本中添加room-ktx
工件模块。// optional - Kotlin Extensions and Coroutines support for Roomimplementation("androidx.room:room-ktx:2.3.0")
2. 使用 Kotlin 在 Android 上开发是 JVM 环境,在不使用 Flow 和协程的情况下,依然可以使用
Thread
实现异步,但是不建议这么做,在 Kotlin 中,使用协程更加轻量级。
2.3.1.2 Java 与 RxJava
如果你的应用使用 Java 开发语言,你可以使用 RxJava 框架来编写异步 DAO 方法并返回专门的类型。Room 支持以下 RxJava2 返回类型(Room 2.3 及更高版本支持RxJava3.)。
- 对于一次性查询,Room 2.1 以及更高的版本支持
Completable
、Single<T>
和Maybe<T>
返回值类型。 - 对于观察者查询,Room 支持
Publisher<T>
、Flowable<T>
、 andObservable<T>
返回值类型。
注意事项:在 Room 中使用 RxJava,必须在项目程序模块的
build.gradle
脚本中添加room-rxjava2
或者room-rxjava3
工件模块。// optional - RxJava2 support for Roomimplementation "androidx.room:room-rxjava2:2.3.0")// optional - RxJava3 support for Roomimplementation "androidx.room:room-rxjava3:2.3.0")
2.3.1.3 Java 与 LiveData 和 Guava
如果你的应用程序使用 Java 编程语言,并且你不想使用 RxJava 框架,则可以使用以下替代方案来编写异步查询:
- 使用 Jetpack 的
LiveData
包装类来编写异步观察者查询。 - 使用 Guava 的
ListenableFuture<T>
包装类来编写异步一次性查询。
注意事项:在 Room 中使用 Guava,必须在项目程序模块的
build.gradle
脚本中添加room-guava
工件模块。// optional - Guava support for Room, including Optional and ListenableFutureimplementation "androidx.room:room-guava:2.3.0")
2.3.2 编写异步一次性查询
笔者注:笔者使用 Kotlin 语言,这里就使用 Kotlin 的 Flow 和协程进行演示,读者有兴趣的话可以自己使用RxJava 和 Guava 尝试。
一次性查询是只运行一次,并且在运行时获取数据当前时刻的快照的数据。以下是一次性查询的示例,使用协程实现异步查询,只需要在 DAO 方法中使用 suspend
关键字即可。DAO 方法添加 suspend
关键字之后,就不能在主线程中调用(编译时就已经会报错),需要在协程中调用。
@Dao
interface UserDao {@Query("SELECT * FROM users")suspend fun getAll(): List<User>@Query("SELECT * FROM users WHERE name LIKE :name")suspend fun findByName(name: String): List<User>@Insertsuspend fun insertAll(vararg users: User)
}// 使用 DAO 中的异步查询方法
runBlocking {val result = userDao.getAll();result.forEach {println("User { userId = ${it.userId}, name = ${it.name}, age = ${it.age}}")}
}
2.3.3 编写异步观察者查询
笔者注:笔者使用 Kotlin 语言,这里就使用 Kotlin 的 Flow 和协程进行演示,读者有兴趣的话可以自己使用RxJava 和 Guava 尝试。
观察者查询是这样一种查询操作,当数据库中被查询方法引用的表发生任何变动,都会重新查询返回最新的数据。这种查询操作通常用于数据库数据插入、更新、删除之后,需要及时显示出来(例如:医院的叫号系统)。在 DAO 查询方法中,使用 Flow
作为返回值,调用 Flow
的 collect()
方法即可查询结果,该方法参数为一个 FlowCollector
接口实现,首次查询返回当前数据库快照数据,查询结果数据会在 FlowCollector
接口实现的 emit
回调方法中返回,如下示例所示:
@Dao
interface UserDao {@Query("SELECT * FROM users")fun getAll(): Flow<List<User>>
}// 观察者查询
GlobalScope.launch {val flow = userDao.getAll();flow.collect(object : FlowCollector<List<User>> {override suspend fun emit(value: List<User>) {value.forEach {println("User { userId = ${it.userId}, name = ${it.name}, age = ${it.age}}")}}})
}
注意事项:在Kotlin 中,Room 观察者查询
Flow.collect()
必须在协程中调用,且必须是非阻断协程中调用。
三、编后语
在数据访问对象类中定义的查询语句,特别是复杂查询,需要有扎实的 SQL 基础,读者如果 SQL 基础欠缺,可以自行学些下 SQL 相关的内容。
Android Room 数据访问对象(DAO)详解相关推荐
- Android实战:CoolWeather酷欧天气(加强版数据接口)代码详解(上)
-----------------------------------该文章代码已停更,可参考浩比天气(更新于2019/6/25)----------------------------------- ...
- Android 进阶——Framework 核心之Android Storage Access Framework(SAF)存储访问框架机制详解(一)
文章大纲 引言 一.Android Storage Access Framework 二.Storage Access Framework 的主要角色成员 1.Document Provider 文件 ...
- Android 进阶——Framework 核心之Android Storage Access Framework(SAF)存储访问框架机制详解(二)
文章大纲 引言 一.DirectFragment 1.当选中DirectoryFragment中RecyclerView的Item时 2.选中DirectoryFragment中RecyclerVie ...
- php dao类设计,DAO数据访问对象设计 - GoFrame官网 - 类似PHP-Laravel, Java-SpringBoot的Go企业级开发框架...
关于DAO数据访问对象设计其实是关于GoFrame框架工程化实践中比较重要一块设计. DAO设计结合GoFrame的ORM组件性能和易用性都很强,可以极大提高开发和维护效率.看完本章节内容之后,小伙伴 ...
- DAO数据访问对象(Data Access Object)
持久化是将程序中数据库在瞬时状态和持久状态间转换的机制JDBC是一种持久化的机制,将程序直接保存成文本文件也是持久化机制的一种实现 JDBC的封装: 原因: JDBC将程序中的数据持久化保存到MySQ ...
- mysql如何限制数据修改密码_MySQL_MySQL数据库中修改密码及访问限制设置详解,MySQL是一个真正的多用户、多 - phpStudy...
MySQL数据库中修改密码及访问限制设置详解 MySQL是一个真正的多用户.多线程SQL数据库服务器.MySQL是以一个客户机/服务器结构的实现,它由一个服务器守护程序mysqld和很多不同的客户程序 ...
- Android四大组件Service之AIDL详解
Android四大组件Service之AIDL详解 前言 简介 基础知识 AIDL 服务端 定义AIDL文件规则 创建 .aidl 文件 清单注册 通过 IPC 传递对象 调用 IPC 方法 Andr ...
- 基于sqlite的android数据库编程,Android编程之SQLite数据库操作方法详解
Android编程之SQLite数据库操作方法详解 发布时间:2020-09-07 12:33:04 来源:脚本之家 阅读:85 作者:低调小一 本文实例讲述了Android SQLite数据库操作方 ...
- 深入理解Android(一):Gradle详解
http://www.infoq.com/cn/articles/android-in-depth-gradle/ http://www.infoq.com/cn/articles/android-i ...
- Android之TelephonyManager类的方法详解
林计钦-JAVA java技术群:127834248 博客园 首页 社区 新随笔 联系 订阅 管理 随笔-105 评论-24 文章-0 trackbacks-0 Andr ...
最新文章
- Mybatis入门:3(动态sql)
- 将多个文件的属性添加到数据库
- Spring RestTemplate中文乱码解决方案
- vuex状态管理简单入门
- SwipeRefreshLayout
- Java 程序 ——感想
- 收音机磁棒天线4根接法_五六十年代不需要电的收音机,你见过吗?
- 122 Python程序中的进程池和线程池
- prototype.js学习(3)函数绑定
- linux终端清除命令,如何清除 Linux 中的终端?使用 clear 命令清除 Linux 终端
- qunee for html5 绘图
- 云锁和悬镜服务器哪个好,安全狗、悬镜、云锁、云帮手建议用哪个比较好?
- Java网络编程(BIO和NIO)
- 网管必修课 中小企业安全路由器配置
- 微信小程序识别二维码
- Cannot get a text value from a numeric cell
- 修改服务器tcp会话数,windows2012服务器TCP连接数
- 移动硬盘安装ubuntu16.04
- html+css(锚点的使用)
- java-集合-set(不重复集合)知识分解——庖丁解牛版