简介
官方给出的描述:
- Room持久性库在SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
所以,Room
还是跟我们过去使用的OrmLite
、greendao
数据库一样都是基于SQLite
,当然Room
会更强大。
Room的优点
既然说Room
更强大,那它究竟有哪些优势呢?Room
数据库框架主要有以下几点优势:
- 使用编译时注解,能够对
@Query
和@Entity
里面的SQL
语句等进行验证; - 与SQL语句的使用更加贴近,能够降低学习成本,提交开发效率,减少模板代码;
- @Embedded 能够减少表的创建;
- 对
LiveData
、Kotlin Coroutines
的支持。
Room的基本使用
依赖库
//注解处理器插件
plugins {
id 'kotlin-kapt'
}
android {
//官方建议:在`android`块中添加以下代码块,以从软件包中排除原子函数模块并防止出现警告
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
kotlinOptions {
jvmTarget = "1.8"
}
}
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
//Room不能在主线程即UI线程上操作,可以使用lifecycleScope在协程作用域中操作
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
创建Entity的数据库表
@Entity(tableName = "word_table")
data class WordEntity(
@ColumnInfo(name = "word")
val word:String?,
@PrimaryKey(autoGenerate = true)
val id: Long = 0
)
@Entity
注解是标识实体类,并通过tableName
指定该实体类指向的表名,@PrimaryKey
注解标识主键,这个跟greenDao
这些数据库基本是一样的,autoGenerate = true
则表示主键自增,@ColumnInfo
注解标识列参数的信息,name =
是指定数据库字段名称,不指定默认是自定义的值。
通过接口注解定义数据的增删查改方法
@Dao
interface WordDao {
//插入多个数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(words: MutableList<WordEntity>)
//
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(words: WordEntity)
//获取所有数据
@Query("SELECT * FROM word_table")
fun queryAll(): MutableList<WordEntity>
//根据id获取一个数据
@Query("SELECT * FROM word_table WHERE id = :id")
fun getWordById(id: Int): WordEntity?
//删除表中所有数据
@Query("DELETE FROM word_table")
suspend fun deleteAll()
//通过id修改数据
@Query("UPDATE word_table SET word=:word WHERE id=:id")
suspend fun updateData(id: Long, word: String)
//根据Id删除数据
@Query("DELETE FROM word_table WHERE id=:id")
suspend fun deleteById(id: Long)
//根据属性值删除数据
@Query("DELETE FROM word_table WHERE word=:word")
suspend fun deleteByName(word: String)
}
WordDao
接口在编译时,当它被数据库引用时,Room
会生成此类的实现,这也是为什么可以通过接口来调用。因为通过Room
操作数据库不可以在主线程,所以把增删查的方法都写成了挂起函数,通过协程来调用,另外也可以通过LiveData
使用,因为LiveData
会在后面学习,所以这里就先不提了。
创建数据库
@Database(entities = [WordEntity::class], version = 1, exportSchema = false)
abstract class WordDB : RoomDatabase() {
abstract fun getWordDao(): WordDao
companion object {
@Volatile
private var instantce: WordDB? = null
private const val DB_NAME = "jetpack_room.db"
fun getInstance(context: Context): WordDB? {
if (instantce == null) {
synchronized(WordDB::class.java) {
if (instantce == null) {
instantce = createInstance(context)
}
}
}
return instantce
}
private fun createInstance(context: Context): WordDB {
return Room.databaseBuilder(context.applicationContext, WordDB::class.java, DB_NAME)
.allowMainThreadQueries()
.build()
}
}
}
创建数据库的类是RoomDatabase
,先创建一个类继承它,并@Database
来标注这个自定义类,@Database
注解里面还有几个参数需要理解一下:
entities
:指定添加进来的数据库表,这里数组形式添加,如果项目用到多个表可以用逗号隔开添加进来;version
:当前数据库的版本号,当需要版本升级会用到;exportSchema
:表示导出为文件模式,默认为true,这里要设置为false,不然会报警告。
Room操作数据增删查改
先来看最终实现的效果:
这里有EditText
输入框,Save
就是插入数,Delete All
则是删除全部数据,下面的显示的该表中所有数据,通过RecycleView
实现,并通过item的点击实现单个删除。
下面是Room操作流程:
在Application中调用一下,让数据库初始化创建:
class RoomApp : Application() {
override fun onCreate() {
super.onCreate()
WordDB.getInstance(this)
}
}
在activity_main.xml实现下布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/et"
android:layout_width="match_parent"
android:layout_height="48dp"
android:textColor="@color/black"
android:textSize="14sp"
android:layout_margin="16dp"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="HardcodedText,LabelFor"/>
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/teal_200"
android:layout_margin="16dp"
android:text="Save"
android:textSize="14sp"
android:textColor="@color/white"
android:textAllCaps="false"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/btn_delete"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/teal_200"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Delete All"
android:textSize="14sp"
android:textColor="@color/white"
android:textAllCaps="false"
tools:ignore="HardcodedText" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"/>
</LinearLayout>
RecycleView
的适配器:
class WordAdapter(context: Context, list: MutableList<WordEntity>): RecyclerView.Adapter<WordAdapter.ViewHolder>() {
private var mContext: Context = context
private var mList: MutableList<WordEntity> = list
private lateinit var wordDeleteListener:WordDeleteListener
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item,parent,false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mList[position]
holder.tvWord.text = item.word
holder.llItem.setOnClickListener {
wordDeleteListener.delete(item.id)
}
}
override fun getItemCount(): Int {
return mList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvWord: TextView = itemView.findViewById(R.id.tv_word)
var llItem: LinearLayout = itemView.findViewById(R.id.ll_item)
}
fun setWordDeleteListener(wordDeleteListener: WordDeleteListener){
this.wordDeleteListener = wordDeleteListener
}
interface WordDeleteListener{
fun delete(id: Long)
}
}
WordAdapter
的item布局文件recyclerview_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/teal_700"
android:gravity="center"
android:layout_marginTop="8dp"
android:id="@+id/ll_item">
<TextView
android:id="@+id/tv_word"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/white"/>
</LinearLayout>
MainActivity
的实现:
class MainActivity : AppCompatActivity() {
private val et by bindView<EditText>(R.id.et)
private val btnSave by bindView<Button>(R.id.btn_save)
private val btnDelete by bindView<Button>(R.id.btn_delete)
private val recyclerView by bindView<RecyclerView>(R.id.recyclerview)
private lateinit var wordDao: WordDao
private lateinit var wordAdapter: WordAdapter
private var mList: MutableList<WordEntity> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
wordDao = WordDB.getInstance(this)?.getWordDao() ?: return
wordAdapter = WordAdapter(this,mList)
recyclerView?.adapter = wordAdapter
recyclerView?.layoutManager = LinearLayoutManager(this)
lifecycleScope.launch {
mList.addAll(wordDao.queryAll())
wordAdapter.notifyDataSetChanged()
}
btnSave?.setOnClickListener {
lifecycleScope.launch {
et?.text.toString().let {
wordDao.insert(WordEntity(it))
updateData()
}
}
}
btnDelete?.setOnClickListener {
lifecycleScope.launch {
wordDao.deleteAll()
updateData()
}
}
wordAdapter.setWordDeleteListener(object :WordAdapter.WordDeleteListener{
override fun delete(id: Long) {
lifecycleScope.launch {
wordDao.deleteById(id)
updateData()
}
}
})
}
fun updateData(){
mList.clear()
mList.addAll(wordDao.queryAll())
wordAdapter.notifyDataSetChanged()
}
}
bindView
的实现方法:
fun <V : View> Activity.bindView(id: Int): Lazy<V?> = lazy {
viewFindId(id)?.saveAs<V>()
}
val viewFindId: Activity.(Int) -> View?
get() = { findViewById(it) }
fun <V : View> Fragment.bindView(id: Int): Lazy<V?> = lazy {
frgViewFindId(id)?.saveAs<V>()
}
val frgViewFindId: Fragment.(Int) -> View?
get() = { view?.findViewById(it) }
fun <T> Any.saveAs(): T? {
return this as? T
}
数据库升级
实际开发中,我们总是会遇到因为新的业务需求要在已有数据表中添加新的参数,这个时候就要对数据升级了。数据库升级主要是以下几点:
- 在Room构建数据库是通过addMigrations(Migration migrations...)方法进行版本升级,方法内饰一个可边长参数,可以实现处理多个版本升级迁移;
- Migration(int startVersion, int endVersion)方法是指定从什么版本升级到哪一版本,每次迁移都可以在定义的两个版本之间移动;
- 在重写的
migrate(database: SupportSQLiteDatabase)
方法中执行更新的sql语句,相应的要在Entity增加字段或者增加一个新的Entity类。
同一张表中增加字段:
在WordEntity
中增加一个字段content
:
@Entity(tableName = "word_table")
data class WordEntity(
@ColumnInfo(name = "word")
val word: String?,
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "content")
val content: String?
)
WordDB
中的更改:
version改为2:
@Database(entities = [WordEntity::class], version = 2, exportSchema = false)
创建一个Migration
:
private val MIGRATION_1_2 = object : Migration(1,2){
override fun migrate(database: SupportSQLiteDatabase) {
//FRUIT 表 新增一列
database.execSQL("ALTER TABLE word_table add COLUMN content text")
}
}
添加到创建库的方法里:
private fun createInstance(context: Context): WordDB {
return Room.databaseBuilder(context.applicationContext, WordDB::class.java, DB_NAME)
.addMigrations(MIGRATION_1_2)
.allowMainThreadQueries()
.build()
}
我们RecycleView列表下面增加一个字段显示,当save的时候也多save一个字段,效果如下:
升级增加新的表:
新建一个Entity
:
@Entity(tableName = "order_table")
data class OrderInfoEntity(
@ColumnInfo(name = "order_id")
val order_id: String?,
@ColumnInfo(name = "order_info")
val order_info: String?,
@PrimaryKey(autoGenerate = true)
val id: Long = 0
)
OrderInfoDao
这里就不展示出来了,对照之前的WordEntity
就行。WordDB
改动如下:
@Database(entities = [WordEntity::class, OrderInfoEntity::class], version = 3, exportSchema = false)
abstract class WordDB : RoomDatabase() {
abstract fun getWordDao(): WordDao
abstract fun getOrderInfoDao(): OrderInfoDao
companion object {
@Volatile
private var instantce: WordDB? = null
private const val DB_NAME = "jetpack_room.db"
fun getInstance(context: Context): WordDB? {
if (instantce == null) {
synchronized(WordDB::class.java) {
if (instantce == null) {
instantce = createInstance(context)
}
}
}
return instantce
}
// private val MIGRATION_1_2 = object : Migration(1, 2) {
// override fun migrate(database: SupportSQLiteDatabase) {
// //FRUIT 表 新增一列
// database.execSQL("ALTER TABLE word_table add COLUMN content text")
// }
// }
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
// 新增一个表
database.execSQL("CREATE TABLE IF NOT EXISTS order_table(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, order_id TEXT, order_info TEXT)")
}
}
private fun createInstance(context: Context): WordDB {
return Room.databaseBuilder(context.applicationContext, WordDB::class.java, DB_NAME)
.addMigrations(MIGRATION_2_3)
.allowMainThreadQueries()
.build()
}
}
}
主要改动是:
entities
增加OrderInfoEntity::class
;version
升级1;- 重写
migrate()
方法,添加sql语句。
小结
Room
组件在操作数据库时,在idea上可以看到SQLite增删查改的语句细节,并且idea可以直接验证这些语句是否正确,这的确会降低出错的几率。个人觉得就入门使用来说会比greenDao
复杂一点,尤其就是当升级表或者增加表字段时,不得不提高一下你写sql语句的水平,但在你熟悉它的基本使用后,无疑它是非常有使用优势的,而且又是官方推崇。这篇文章介绍的主要是Room
的基本使用,文中内容足够我们应付一般开发任务,如果有更多时间,可以去了解一它的源码实现,这或者有利于我们在实际开发中进一步优化和定制功能。