Jetpack Room是Android官方提供的一个持久化库,旨在简化Android应用程序中的数据库操作。它提供了一个抽象层,使开发人员能够以面向对象的方式处理数据库操作,而无需编写复杂的SQL查询语句。通过使用Jetpack Room,开发人员可以更快速、更高效地构建稳健的数据库驱动应用程序。
Room 最新版本为2.5.2
Android Studio版本为 Android Studio Flamingo | 2022.2.1 Patch 2
配置Room
在使用Room之前,我们需要添加它的依赖进入到工程,首先在app模块的build.gradle
中添加依赖项
dependencies {
...
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
// kotlin需要kapt,java则是annotationProcessor
kapt "androidx.room:room-compiler:$room_version"
}
因为Room依赖需要kapt
插件,所以我们还需要添加kapt
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
同步下工程之后,我们就可以在工程中使用Room来管理数据库了。
简单操作Room
在介绍初始化之前,我们先了解下几个相关的概念
Entity
用来定义数据库中的某个表和表中的数据结构;Dao
用来管理和操作数据库中的表,包括常见的增、删、改、查等操作;Database
继承自RoomDatabase
类,它是抽象类,AS会自动为我们生成它的实现,用于管理数据库的名称、版本和升级等操作,并且可以从它获取Dao
的实现类。
了解了上面的概念之后,下面我们直接进入使用Room的环节,一起来看看Room是如何帮我们简化数据库的操作。
第一步先定义一个实体类,用于表示数据库中某个表的结构
@Entity(tableName = "user_entity")
data class UserEntity(
val name: String,
@PrimaryKey
val id: Int
)
我们定义了一个UserEntity
数据类,类的注解采用@Entity
修饰,表示它是数据库中的一张表,设置了表名为user_entity
,并且给表的主键设置为id
字段,这个主键可以帮助我们在插入相同id
时给予冲突策略(后面会详细介绍)。
第二步定义我们user_entity
的Dao
类,将增删改查方法先定义好
@Dao
interface UserDao {
// 设置主键冲突之后的策略,这里选择直接覆盖原数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(userEntity: UserEntity)
// 删除某条数据
@Delete
fun deleteUser(userEntity: UserEntity)
// 更新某条数据
@Update
fun updateUser(userEntity: UserEntity)
// 根据id查找数据
@Query("select * from user_entity where id=:id")
fun findUser(id: Int): List<UserEntity>
}
这里我们定义了一个UserDao
接口,注意是接口类哦,并且使用@Dao
注解进行修饰,内部定义了四个方法,分别为增删改查操作。注意看insert
方法的注解,其中onConflict
参数就是用来处理主键冲突的,当我们插入一个数据时,表中已经有此主键数据,这时候Room就会根据我们设置的策略来处理这条新增的数据:
OnConflictStrategy.REPLACE
如果发生冲突,直接覆盖已有数据,将表中现存的数据替换成插入的这条;OnConflictStrategy.IGNORE
如果发生冲突,直接忽略此次插入操作OnConflictStrategy.NONE
这个是默认的策略,它和ABORT
作用是一致的,都是终止此次插入操作,并且抛出SQLiteConstraintException
OnConflictStrategy.ROLLBACK
这个表示如果发生冲突,终止插入操作,并且将事务回滚到最初的状态,在最新版本已经被标记@Deprecated
推荐使用ABORT
OnConflictStrategy.ABORT
和NONE
作用一致,这里就不过多介绍OnConflictStrategy.FAIL
这个表示如果发生冲突,终止插入操作,并且抛出SQLiteConstraintException
异常,在最新版本也是被标记@Deprecated
,也是推荐使用ABORT
。
以上就是在插入操作过程中,主键冲突时,所有的策略模式,大家可以按需求采用。
定义好Dao
之后,我们就可以配置RoomDatabase
了,少了它我们还不能使用Room来操作数据库呢。
@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class RoomDb : RoomDatabase() {
abstract fun getUserDao(): UserDao
}
定义一个RoomDatabase
子类,并且它是抽象方法,采用@Database
注解修饰,注解中带了三个参数:
entities
表示所有的实体类,也是就Room要创建的表结构,它是一个数组类型,可以创建多个表;version
表示的是数据库的版本,这个在后面的数据库升级中会详细介绍,重要信息之一;exportSchema
这个参数仅仅代表是否可以在编译的时候导出数据库的配置文件,如果你需要看配置可以设置为true
,默认的配置文件会在app/build/
的schema
文件夹中。
内部有一个抽象方法,是用于获取UserDao
实例,这里AS会默认为我们生成实现类,具体生成的类在build/generated/source/kapt/com/...
,生成之后的类名是我们定义的类名加上_Impl
。
最后我们需要创建RoomDb
单例对象,这里我们采用的是Koin
库帮助我们简化操作,具体Koin
的使用前几篇文章有详细介绍,小伙伴们可以去了解下。
val module = module {
// 创建RoomDb的单例对象
single {
Room.databaseBuilder(androidContext(), RoomDb::class.java, "room_db")
.build()
}
// 创建UserDao的单例对象
single { get<RoomDb>().getUserDao() }
}
在创建数据库RoomDb
对象时,采用的是build
模式,传入了Context
、RoomDb
和数据库名称,这里还是比较简单的,在后面涉及数据库升级时,我们还是会回到此处,添加升级操作。
到这里数据库的准备工作已经完成了,接下来就可以实践一下最常用的增删改查操作了,顺便提一下,AS现在可以直接查看和调试App的数据库了,在App inspection
工具栏里面就可以体验。
class MainActivity : AppCompatActivity() {
// 获取UserDao单例对象
private val userDao by inject<UserDao>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.tv).setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
// 插入一条数据
userDao.insertUser(UserEntity(1, "taonce"))
}
}
findViewById<TextView>(R.id.textView).setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
// 将name更新为taonce_update
userDao.updateUser(UserEntity(1, "taonce_update"))
}
}
findViewById<TextView>(R.id.textView2).setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
// 查找表中id为1的数据
val entityList = userDao.findUser(1)
entityList.forEach { Log.d(TAG, "find user: $it") }
}
}
findViewById<TextView>(R.id.textView3).setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
// 删除此UserEntity数据
userDao.deleteUser(UserEntity(1, "taonce_update"))
}
}
}
}
当我们点击插入数据后,我们可以在App Inspection
中实时查看到数据的变化。
如果你想实时的观察表中数据变化,记住勾选Live updates框,未勾选的情况需要手动点击前面的刷新图标。App Inspection
还可以直接使用SQL
语句来操作表,你可以在调试过程中修改或者模拟一些数据。
左边红框就是新建SQL
语句的入口。
Entity的高阶用法
上面提到了在数据类采用@Entity
注解之后,表结构就会根据数据类的字段来生成对应表字段,如果有个数据类的字段很多,但是我们又不想全部存入数据库,或者这个数据类引入了别的数据类此时对应的表结构会是怎样呢?
忽略字段
当我们不想表中存入全部字段时,我们可以采用忽略某些字段的形式来解决这种问题。
@Entity
data class ArticleEntity(
@PrimaryKey()
var articleId: Long = 0L,
var title: String = "",
var url: String = "",
var author: String = "",
@Ignore
var authorAlisa: String = ""
)
模拟了一个文章实体类,它内部包含很多信息,但是authorAlisa
这个字段我们并不想存入到表中,这个就可以采用@Ignore
注解来忽略此字段,最后的表结构通过App Inspection
看下,它是不包含authorAlisa
字段的。
嵌套对象
当我们定义的数据类中嵌套了另外一个或者多个数据类时,如果不做任何操作Room是无法为我们创建对应的表结构,我们需要显示的通过@Embedded
注解告诉Room此字段为嵌入对象,需要将嵌入对象的字段也加入到表中。
@Entity
data class ArticleEntity(
@PrimaryKey()
var articleId: Long = 0L,
var title: String = "",
var url: String = "",
var author: String = "",
@Ignore
var authorAlisa: String = "",
// 嵌入了AuthorDetail对象
@Embedded
var authorDetail: AuthorDetail = AuthorDetail()
)
data class AuthorDetail(
val authorId: Long = 0L,
val authorName: String = "",
val joinTime: String = "",
val updateTime: String = ""
)
上面我们在ArticleEntity
数据类中嵌入了AuthorDetail
对象,Room会将被嵌入对象的字段也一并加入到表中,还是通过App Inspection
来观察下表结构。
从上面的图片就可以看出被嵌入对象的字段也一起加入到AuthorEntity
表中了。
嵌入List对象
当我们定义的实体类中含有List
字段时,并且在不忽略此字段的情况下,无法通过@Embedded
嵌入对象的方式来引入其内部字段,这时候就需要通过TypeConverter
的形式来操作List
字段了。
首先我们模拟带有List
字段的实体类,并且在类上通过@TypeConverters
注解指定类型转换
@TypeConverters(AuthorDetailConvert::class)
@Entity
data class ArticleEntity(
@PrimaryKey()
var articleId: Long = 0L,
var title: String = "",
var url: String = "",
var author: String = "",
@Ignore
var authorAlisa: String = "",
var authorDetail: List<AuthorDetail> = listOf()
)
data class AuthorDetail(
val authorId: Long = 0L,
val authorName: String = "",
val joinTime: String = "",
val updateTime: String = ""
)
然后再看看我们定义的类型转换具体实现
@ProvidedTypeConverter
class AuthorDetailConvert {
@TypeConverter
fun string2AuthorDetailList(detailList: String): List<AuthorDetail> {
return Gson().fromJson(detailList, object : TypeToken<List<AuthorDetail>>() {}.type)
}
@TypeConverter
fun authorDetailList2String(list: List<AuthorDetail>): String {
return Gson().toJson(list)
}
}
此类必须通过@ProvidedTypeConverter
注解修饰,表示它提供某种具体的类型转换,内部定义两个方法,方法也必须通过@TypeConverter
注解修饰。
authorDetailList2String
方法具体含义就是将List<AuthorDetail>
通过Gson转换成字符串的形式,这个是用来插入数据时调用的方法;string2AuthorDetailList
方法则是相反,它是将字符串通过Gson转换成我们需要的List<AuthorDetail>
对象,这个是用来从数据库中取出数据时调用的方法。
总得来说也就是我们在存数据到库中的时候,会将List<T>
对象转换成字符串存入到表中,它在表中是一个字段,然后再取数据时直接将字符串转换成对应List<T>
对象,这样在开发者的角度就不需要额外的转换逻辑。
下面我们模拟一条数据插入到表中,看看表中保存的数据呈现的是何种样式,先模拟插入操作:
val authorList = listOf<AuthorDetail>(
AuthorDetail(1, "taonce", "今天", "今天"),
AuthorDetail(2, "taonce2", "今天", "今天"),
)
val articleEntity =
ArticleEntity(1, "article", "android.com", "taonce", "taonce", authorList)
authorDao.insertAuthor(articleEntity)
此时通过App Inspection
来看下表的数据
和我们预期的是一致的,它在表中的具体形式就是一个Gson字符串。
文章结尾
本次篇幅暂时就介绍以上内容,篇幅过长阅读起来会产生疲倦感,后面的数据库升级操作和技巧会另起一篇文章详细介绍,这次就到这了~