Android Room 入坑详解
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
- 针对 SQL 查询的编译时验证。
- 可最大限度减少重复和容易出错的样板代码的方便注解。
- 简化了数据库迁移路径。
主要组件
Room 包含三个主要组件:
- 数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。
- 数据实体,用于表示应用的数据库中的表。
- 数据访问对象 (DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。
Room 的不同组件之间的关系
引入Room依赖
首先在build.gradle
导入依赖,Kotlin和RxJava可以按需导入,注释处理工具选一个即可。
def room_version = "2.4.2"
implementation"androidx.room:room-runtime:$room_version"
// 注释处理工具
annotationProcessor "androidx.room:room-compiler:$room_version"
// Kotlin注释处理工具(kapt)
kapt"androidx.room:room-compiler:$room_version"
// kotlin扩展和协同程序对Room的支持
implementation "androidx.room:room-ktx:$room_version"
// RxJava2
implementation "androidx.room:room-rxjava2:$room_version"
// RxJava3
implementation "androidx.room:room-rxjava3:$room_version"
如果使用Kotlin注释处理工具(kapt),还需要在build.gradle文件顶部添加下方定义。
apply plugin: 'kotlin-kapt'
在 android
块中添加 packagingOptions
块,以从软件包中排除原子函数模块并防止出现警告。
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
// 要使用的一些 API 需要 1.8 `jvmTarget`
kotlinOptions {
jvmTarget = "1.8"
}
}
创建实体
Room 允许通过实体创建表,以下代码定义了一个 User 数据实体。User 的每个实例都代表应用数据库中 user 表中的一行。
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap?
)
-
@Entity(tableName = "xxx")
每个@Entity
类代表一个 SQLite 表。 -
@PrimaryKey
声明主键,@PrimaryKey(autoGenerate = true)
可自动生成唯一的主键。 -
@ColumnInfo(name = "xxx")
可以指定表中列的名称,默认是字段名称。 - 存储在数据库中的每个属性均需公开,这是 Kotlin 的默认设置。
- 如果某个实体中有不想保留的字段,则可以使用
@Ignore
为这些字段添加注解
创建DAO
在 DAO(数据访问对象)中,可以指定 SQL 查询并将其与方法调用相关联,DAO 必须是一个接口或抽象类。默认情况下,所有语句都必须在单独的线程上执行。Room 支持Kotlin 协程,可使用 suspend
修饰符对查询进行注解,然后从协程或其他挂起函数对其进行调用。
以下代码定义了一个名为 UserDao 的 DAO。UserDao 提供了应用的其余部分用于与 user 表中的数据交互的方法。
@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 ORDER BY last_name DESC LIMIT 1")
fun findByName(first: String, last: String): User
// onConflict 配置主键冲突处理
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Insert
fun insertBothUsers(user1: User, user2: User)
@Insert
fun insertUsersAndFriends(user: User, friends: List<User>)
@Delete
fun delete(user: User)
}
- 如果
@Insert
方法接收单个参数,则会返回 long 值,这是插入项的新 rowId。如果参数是数组或集合,则该方法应改为返回由 long 值组成的数组或集合,并且每个值都作为其中一个插入项的 rowId -
@Update
和@Delete
方法可以选择性地返回 int 值,该值指示成功的行数。 - onConflict配置:
ABORT
:在发生冲突时回滚事务、IGNORE
:保留现有行、REPLACE
:替换现有行 - 查看更多sqlite语法
interface BaseDAO<T> {
@Insert
suspend fun insert(obj: T)
@Insert
suspend fun insert(list: List<T>)
@Update
suspend fun update(obj: T)
@Update
suspend fun update(list: List<T>)
@Delete
suspend fun delete(obj: T)
@Delete
suspend fun delete(list: List<T>)
}
除了直接在DAO中写方法,还可以创建DAO的基类,把基础方法提取出来,减少重复代码。
配置数据库
Room 数据库类必须是抽象且必须扩展 RoomDatabase。整个应用通常只需要一个 Room 数据库实例。数据库类必须满足以下条件:
- 该类必须带有
@Database
注解,该注解包含列出所有与数据库关联的数据实体的entities
数组。 - 该类必须是一个抽象类,用于扩展
RoomDatabase
。 - 对于与数据库关联的每个 DAO 类,必须在数据库类中定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
// 防止同一时间创建多个实例
@Volatile
private var INSTANCE: AppDatabase ? = null
fun getDatabase(context: Context): AppDatabase {
// 单例
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
// 可以使用单例Application来代替此参数传递
context.applicationContext,
AppDatabase::class.java,
"database_name"
).build()
INSTANCE = instance
instance
}
}
}
}
现在数据库已经可以正常使用了,可以通过以下方式操作数据库:
AppDatabase.getDatabase(context).userDao().insertUsers(user)
保存Date等复杂类型数据
Date等类型字段,Room是不知道怎么存的,需要通过转换器来保存和读取。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
在数据库类配置注解。
@Database(entities = [User::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()
接下来直接在实体声明Date类型的参数即可,当写的时候会调用dateToTimestamp,读的时候会调用fromTimestamp。