重学Android Jetpack(四)之—Room基本使用详解

5,793 阅读6分钟

简介

官方给出的描述:

  • Room持久性库在SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。

所以,Room还是跟我们过去使用的OrmLitegreendao数据库一样都是基于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操作数据增删查改

先来看最终实现的效果:

222.gif

这里有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一个字段,效果如下:

999.gif

升级增加新的表:

新建一个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的基本使用,文中内容足够我们应付一般开发任务,如果有更多时间,可以去了解一它的源码实现,这或者有利于我们在实际开发中进一步优化和定制功能。