1 Room 简介
Room 持久性库是 Android Jetpack 的一部分,它在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
该库可帮助您在运行应用的设备上创建应用数据的缓存。此缓存充当应用的单一可信来源,使用户能够在应用中查看关键信息的一致副本,无论用户是否具有互联网连接。
2 引用
在主 module 的 build.gradle 文件添加依赖
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
3 概念
Room 设计到的概念有以下几个:
Database:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。
- 使用 @Database 注释的类应满足以下条件:
- 是扩展 RoomDatabase 的抽象类。
- 在注释中添加与数据库关联的实体列表。
- 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法,通过该类方法获取具体的 Dao,并进行数据库操作。
- 在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 的实例。
Entity:表示数据库中的表,属性会与数据库表 column 进行映射。
DAO:数据库访问对象,实现具体的增删改查。
Room 不同组件之间的关系如下图所示:
了解上面的具体概念,我们就可以进行开发了,下面是具体细节处理。
4 简单使用
1.定义实体 Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
使用主键
每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,也仍然需要为该字段添加 @PrimaryKey 注释。
如果实体具有复合主键,可以使用 @Entity 注释的 primaryKeys 属性,如以下代码段所示:
@Entity(primaryKeys = arrayOf("firstName", "lastName"))
data class User(
var firstName: String?,
var lastName: String?
)
设置表名与列名
利用 @Entity 注释的 tableName 属性设置表名
利用字段的 @ColumnInfo 注释的 name 属性设置列名
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") var firstName: String?,
@ColumnInfo(name = "last_name") var lastName: String?
)
忽略字段
默认情况下,Room 会为实体中定义的每个字段创建一个列。如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段添加注释,如以下代码段所示:
@Entity
data class User(
@PrimaryKey val id: Int,
var firstName: String?,
var lastName: String?,
@Ignore var picture: Bitmap?
)
如果实体继承了父实体的字段,则使用 @Entity 属性的 ignoredColumns 属性通常会更容易:
open class User {
var picture: Bitmap? = null
}
@Entity(ignoredColumns = arrayOf("picture"))
data class RemoteUser(
@PrimaryKey val id: Int,
var hasVpn: Boolean
) : User()
2. 定义具体的 Dao
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
@Update
fun updateUsers(vararg users: User)
}
Insert
如果 @Insert 方法只接收 1 个参数,则它可以返回 long,这是插入项的新 rowId。如果参数是数组或集合,则应返回 long[] 或 List<Long>。
Update
@Update 便捷方法会修改数据库中以参数形式给出的一组实体。它使用与每个实体的主键匹配的查询。
通常没有必要,但是可以让此方法返回一个 int 值,以指示数据库中更新的行数。
Delete
@Delete 便捷方法会从数据库中删除一组以参数形式给出的实体。它使用主键查找要删除的实体。
与 @Update 相同,可以让此方法返回一个 int 值,以指示从数据库中删除的行数。
Query
@Query 是 DAO 类中使用的主要注释。它允许对数据库执行读/写操作。每个 @Query 方法都会在编译时进行验证,因此如果查询出现问题,则会发生编译错误,而不是运行时失败。
3. 定义类继承自 RoomDataBase
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
4. 获取数据库实例,进行数据库操作
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
5 与 Kotlin Coroutines 结合使用
Room 支持各种查询方法的返回类型,包括与特定框架或 API 进行互操作的特殊返回类型。下表根据查询类型和框架展示了适用的返回类型:
| 查询类型 | 协程 | RxJava | 生命周期 |
|---|---|---|---|
| 可观察读取 | Flow<T> | Flowable<T>、Publisher<T>、Observable<T> | LiveData<T> |
| 单次读取 | suspend fun | Single<T>、Maybe<T> | 无 |
| 单次写入 | suspend fun | Single<T>、Maybe<T>、Completable<T> | 无 |
1. 使用流进行响应式查询
在 Room 2.2 及更高版本中,可以使用 Kotlin 的 Flow 功能(需要添加 room-ktx 依赖)确保应用的界面保持最新状态。只要表中的任何数据发生变化,界面都会进行自动更新,写法如下:
@Query("SELECT * FROM User")
fun getAllUsers(): Flow<List<User>>
可以通过将 distinctUntilChanged() 运算符应用于返回的 Flow 对象,确保仅在实际查询结果发生更改时通知界面,用法如下:
@Dao
abstract class UsersDao {
@Query("SELECT * FROM User WHERE username = :username")
abstract fun getUser(username: String): Flow<User>
fun getUserDistinctUntilChanged(username:String) =
getUser(username).distinctUntilChanged()
}
2. 使用 Kotlin 协程进行异步查询
可以将 suspend Kotlin 关键字添加到 DAO 方法中,以使用 Kotlin 协程功能使这些方法成为异步方法。这样可确保不会在主线程上执行这些方法。
@Dao
interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Query("SELECT * FROM user")
suspend fun loadAllUsers(): Array<User>
}
suspend 关键字同时适用于带有 @Transaction 注释的 DAO 方法,这些方法会在单个数据库事务中运行。
@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: User) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}
@Query("DELETE FROM users")
abstract fun deleteUser(user: User)
@Insert
abstract suspend fun insertUser(user: User)
}
3. 使用 LiveData 进行可观察查询
执行查询时,可以通过使用 LiveData 类型的返回值,确保应用的界面在数据发生变化时自动更新,当数据库更新时,Room 会生成更新 LiveData 所必需的所有代码。
@Dao
interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}
4. 使用 RxJava 进行响应式查询
由于最近在转型使用 Kotlin 协程,所以未对RxJava进行研究,欲知如何使用,可自行参考 Android 开发者官网。
6 踩坑
1. @PrimaryKey 与 @Ignore
当使用 Kotlin data class 声明数据库表,且在一个 Entity 实体中同时出现这两个注解时,会有产生一些奇怪的错误。
- 建议使用 var 声明属性,虽然官方文档中的例子是使用 val 关键字对属性进行声明,但是实际 build 的时候会报以下错误:
Cannot find setter for field.
- 需要给 data class 中的全部属性设置默认值,否则 build 时会报以下错误:
Entities and POJOs must have a usable public constructor.
You can have an empty constructor or a constructor whose parameters match the fields (by name and type).
- 踩过前面两个坑之后,如果 Entity 主键是自增的 Int 或 Long 型时,此时,假设设置的主键默认值为 0,在 Insert 数据时,也未对主键进行赋值,那么数据的主键会一直是默认值 0,后续的产生的问题参照 onConflict 配置(数据库中只有一条数据、插入失败等)。
解决方案:
将 @Ignore 注解的属性,放入 data class 的类体中,这样即可避免问题 2 与问题 3
@Entity data class User( @PrimaryKey(autoGenerate = true) var id: Int, var firstName: String = "", var lastName: String = "", ) { @Ignore var pictureUrl: String = "" }此外,可能大家都知道 Kotlin data class 与 Gson 配合使用时,如果 data class 中没有给所有的属性设定默认值时,会有产生 NPE 的情况。考虑到问题 3,当无法给自增主键设置默认值时,可以采用以下方案进行解决。
@Entity data class User( var firstName: String = "", var lastName: String = "", ) { @PrimaryKey(autoGenerate = true) var id: Int? = null, @Ignore var pictureUrl: String = "" }
2. 不能声明 isXxx 形式的字段名
POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。
反例:isSuccess:Boolean 属性,他的方法也是isSuccess(),RPC 框架在反向解析的时候,“以为”对应的属性是 success,导致属性获取不到,进而抛出异常。
3. suspend 关键字与 Flow 和 LiveData
需要注意的是,使用 suspend 关键字进行声明的 Dao 函数中,不可以使用 Flow 或者 LiveData 作为返回值,否则会报出编译时异常(异常信息如下),因此该类 Dao 函数,建议在 Coroutines 中的非主线程中调用。
Not sure how to convert a Cursor to this method's return type (kotlinx.coroutines.flow.Flow<T>).
Not sure how to convert a Cursor to this method's return type (androidx.lifecycle.LiveData<T>).
本文使用 mdnice 排版