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>

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

解析:上面的例子中返回只包含数据库表中 nameage 这两个字段值的结果。

注意事项:
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, CompletableListenableFuture
一次性读查询Coroutines(suspend)Single, MaybeListenableFuture
观察者读查询FlowFlowable, Publisher, ObservableLiveData
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 Room
 implementation("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 以及更高的版本支持 CompletableSingle<T>Maybe<T> 返回值类型。
  • 对于观察者查询,Room 支持 Publisher<T>Flowable<T>、 and Observable<T> 返回值类型。

注意事项:在 Room 中使用 RxJava,必须在项目程序模块的 build.gradle 脚本中添加 room-rxjava2 或者 room-rxjava3 工件模块。

// optional - RxJava2 support for Room
 implementation "androidx.room:room-rxjava2:2.3.0")
 
// optional - RxJava3 support for Room
 implementation "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 ListenableFuture
 implementation "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>

    @Insert
    suspend 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 作为返回值,调用 Flowcollect() 方法即可查询结果,该方法参数为一个 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 相关的内容。

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android RoomAndroid平台上的一个持久化库,用于处理本地数据库的操作。当在应用程序中使用Room时,可能会遇到需要进行数据迁移的情况,例如更改数据库模式或添加新的表。 在Room中进行数据迁移可以通过数据库升级来实现。下面是一些常见的步骤: 1. 在Room的Database类中,使用`@Database`注解来定义你的数据库,并指定`version`属性,表示当前数据库的版本号。 ```kotlin @Database(entities = arrayOf(User::class), version = 2) abstract class AppDatabase : RoomDatabase() { // ... } ``` 2. 创建一个新的数据库版本,并在新版本中进行必要的更改。例如,你可以添加新的实体类或更改现有实体类的结构。 ```kotlin @Database(entities = arrayOf(User::class, Order::class), version = 3) abstract class AppDatabase : RoomDatabase() { // ... } ``` 3. 创建一个用于执行数据库升级的`Migration`对象。`Migration`对象需要实现`Migration`接口,并在`migrate()`方法中定义旧版本到新版本的迁移逻辑。 ```kotlin val MIGRATION_2_3: Migration = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { // 执行数据库迁移操作 } } ``` 4. 在创建Room的`Database`对象时,使用`.addMigrations()`方法将迁移对象添加到构建器中。 ```kotlin val db = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_2_3) .build() ``` 这样,在应用程序升级时,Room将会自动检测到数据库版本的变化,并执行相应的迁移操作。 需要注意的是,当进行数据库迁移
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值