写点什么

Room:又要写业务代码了?看看我吧,给你飞一般的感觉!

  • 2022 年 7 月 15 日
  • 本文字数:9371 字

    阅读完需:约 31 分钟

Room:又要写业务代码了?看看我吧,给你飞一般的感觉!

前言:

🏀在我们日常开发中,经常要和数据打交道,所以存储数据是很重要的事。Android 从最开始使用 SQLite 作为数据库存储数据,再到许多的开源的数据库,例如 QRMLite,DBFlow,郭霖大佬开发的 Litepal 等等,都是为了方便 SQLite 的使用而出现的,因为 SQLite 的使用繁琐且容易出错。Google 当然也意识到了 SQLite 的一些问题,于是在 Jetpack 组件中推出了 Room,本质上 Room 也是在 SQLite 上提供了一层封装。因为它官方组件的身份,和良好的开发体验,现在逐渐成为了最主流的数据库 ORM 框架。


🌟Room 官方文档:https://developer.android.google.cn/jetpack/androidx/releases/room


🌟SQL 语法教程:https://www.runoob.com/sqlite/sqlite-tutorial.html


🚀本文代码地址:https://github.com/taxze6/Jetpack_learn/tree/main/Jetpack_basic_learn/room

为什么要使用 Room?Room 具有什么优势?

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够享有更强健的数据库访问机制。


Room 的具体优势:


  • 有可以最大限度减少重复和容易出错的样板代码的注解

  • 简化了数据库迁移路径

  • 针对编译期SQL的语法检查

  • API 设计友好,更容易上手,理解

  • SQL语句的使用更加贴近,能够降低学习成本

  • RxJavaLiveDataKotlin协程等都支持

Room 具有三个主要模块

  • Entity: Entity用来表示数据库中的一个表。需要使用@Entity(tableName = "XXX")注解,其中的参数为表名。

  • Dao: 数据库访问对象,用于访问和管理数据(增删改查)。在使用时需要@DAO注解

  • Database: 它作为数据库持有者,用@Database注解和Room Database扩展的类

如何使用 Room 呢?

①添加依赖


plugins {    ...    id 'kotlin-kapt'}def room_version = "2.4.2"implementation "androidx.room:room-runtime:$room_version"annotationProcessor "androidx.room:room-compiler:$room_version"kapt 'androidx.room:room-compiler:$room_version'
复制代码
②创建Entity实体类,用来表示数据库中的一张表(table)
@Entity(tableName = "user")data class UserEntity(    //主键定义需要用到@PrimaryKey(autoGenerate = true)注解,autoGenerate参数决定是否自增长    @PrimaryKey(autoGenerate = true) val id:Int = 0, //默认值为0    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name:String?,    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age:Int?)
复制代码


其中,每个表的字段都要加上@ColumnInfo(name = "xxx", typeAffinity = ColumnInfo.xxx)name属性表示这张表中的字段名,typeAffinity表示改字段的数据类型。


其他常用注解:


  • @Ignore :Entity中的所有属性都会被持久化到数据库,除非使用@Ignore


    @Ignore val name: String?
复制代码


  • @ForeignKey:外键约束,不同于目前存在的大多数 ORM 库,Room 不支持 Entitiy 对象间的直接引用。Google 也做出了解释,具体原因请查看:https://developer.android.com/training/data-storage/room/referencing-data,不过Room允许通过外键来表示Entity之间的关系。ForeignKey我们文章后面再谈,先讲简单的使用。

  • @Embedded :实体类中引用其他实体类,在某些情况下,对于一张表的数据,我们用多个POJO类来表示,所以在这种情况下,我们可以使用Embedded注解嵌套对象。

③创建数据访问对象(Dao)处理增删改查
@Daointerface UserDao {    //添加用户    @Insert    fun addUser(vararg userEntity: UserEntity)    //删除用户    @Delete    fun deleteUser(vararg userEntity: UserEntity)    //更新用户    @Update    fun updateUser(vararg userEntity: UserEntity)    //查找用户    //返回user表中所有的数据    @Query("select * from user")    fun queryUser(): List<UserEntity>}
复制代码


Dao负责提供访问DBAPI,我们每一张表都需要一个Dao。在这里使用@Dao注解定义Dao类。


  • @Insert, @Delete需要传一个entity()进去


-   `@Query`则是需要传递`SQL`语句-   ```kotlin    public @interface Query {        //要运行的SQL语句        String value();    }
复制代码


☀注意:Room 会在编译期基于 Dao 自动生成具体的实现类,UserDao_Impl(实现增删改查的方法)。

🔥Dao 所有的方法调研都在当前线程进行,需要避免在 UI 线程中直接访问!

④创建 Room database
@Database(entities = [UserEntity::class], version = 1)abstract class UserDatabase : RoomDatabase() {    abstract fun userDao(): UserDao}
复制代码


通过Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()获取Database实例


val db = Room.databaseBuilder(    applicationContext,    UserDatabase::class.java, "userDb"    ).build()
复制代码


☀注意:创建Database的成本较高,所以我们最好使用单例的Database,避免反复创建实例所带来的开销。


单例模式创建 Database:


@Database(entities = [UserEntity::class], version = 1)abstract class UserDatabase : RoomDatabase() {    abstract fun getUserDao(): UserDao    companion object {        @Volatile        private var INSTANCE: UserDatabase? = null        @JvmStatic        fun getInstance(context: Context): UserDatabase {            val tmpInstance = INSTANCE            if (tmpInstance != null) {                return tmpInstance            }            //锁            synchronized(this) {                val instance =                    Room.databaseBuilder(context, UserDatabase::class.java, "userDb").build()                INSTANCE = instance                return instance            }        }    }}
复制代码
⑤在 Activity 中使用,进行一些可视化操作

activity_main:


<LinearLayout    ...    tools:context=".MainActivity"    android:orientation="vertical">    <Button        android:id="@+id/btn_add"        ...        android:text="增加一条数据"/>    <Button        android:id="@+id/btn_delete"        ...        android:text="删除一条数据"/>    <Button        android:id="@+id/btn_update"        ...        android:text="更新一条数据"/>    <Button        android:id="@+id/btn_query_all"        ...        android:text="查新所有数据"/></LinearLayout>
复制代码


MainActivity:


private const val TAG = "My_MainActivity"class MainActivity : AppCompatActivity() {    private val userDao by lazy {        UserDatabase.getInstance(this).getUserDao()    }    private lateinit var btnAdd: Button    private lateinit var btnDelete: Button    private lateinit var btnUpdate: Button    private lateinit var btnQueryAll: Button    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        init()        //添加数据        btnAdd.setOnClickListener {            //数据库的增删改查必须在子线程,当然也可以在协程中操作            Thread {                val entity = UserEntity(name = "Taxze", age = 18)                userDao.addUser(entity)            }.start()        }        //查询数据        btnQueryAll.setOnClickListener {            Thread {                val userList = userDao.queryUser()                userList.forEach {                    Log.d(TAG, "查询到的数据为:$it")                }            }.start()        }        //修改数据        btnUpdate.setOnClickListener {            Thread {                userDao.updateUser(UserEntity(2, "Taxzeeeeee", 18))            }.start()        }        //删除数据        btnDelete.setOnClickListener {            Thread {                userDao.deleteUser(UserEntity(2, null, null))            }.start()        }    }    //初始化    private fun init() {        btnAdd = findViewById(R.id.btn_add)        btnDelete = findViewById(R.id.btn_delete)        btnUpdate = findViewById(R.id.btn_update)        btnQueryAll = findViewById(R.id.btn_query_all)    }}
复制代码


结果:



到这里我们已经讲完了 Room 的最基本的使用,如果只是一些非常简单的业务,你看到这里已经可以去写代码了,但是还有一些进阶的操作需讲解一下,继续往下看吧!

数据库的升级

Room 在 2021 年 4 月 21 日发布的版本 2.4.0-alpha01 中开始支持自动迁移,不过很多朋友反应还是有很多问题,建议手动迁移,当然如果你使用的是更低的版本只能手动迁移啦。


具体信息请参考:https://developer.android.google.cn/training/data-storage/room/migrating-db-versions#manual


具体如何升级数据库呢?下面我们一步一步来实现吧!

①修改数据库版本

UserDatabase文件中修改version,将其变为 2(原来是 1)


在此时,我们需要想一想,我们要对数据库做什么升级操作呢?


我们这里为了演示就给数据库增加一张成绩表:


@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2)
复制代码


添加表:


@Entity(tableName = "score")data class ScoreEntity(    @PrimaryKey(autoGenerate = true) var id: Int = 0,    @ColumnInfo(name = "userScore")    var userScore: Int)
复制代码
②创建对应的 Dao,ScoreDao
@Daointerface ScoreDao {    @Insert    fun insertUserScore(vararg scoreEntity: ScoreEntity)    @Query("select * from score")    fun queryUserScoreData():List<ScoreEntity>}
复制代码
③在 Database 中添加迁移
@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2)abstract class UserDatabase : RoomDatabase() {    abstract fun getUserDao(): UserDao        //添加一个Dao    abstract fun getScoreDao():ScoreDao    companion object {        //变量名最好为xxx版本迁移到xxx版本        private val MIGRATION_1_2 = object : Migration(1, 2) {            override fun migrate(database: SupportSQLiteDatabase) {                database.execSQL(                    """                    create table userScore(                    id integer primary key autoincrement not null,                    userScore integer not null)                """.trimIndent()                )            }        }        @Volatile        private var INSTANCE: UserDatabase? = null        @JvmStatic        fun getInstance(context: Context): UserDatabase {            ...            synchronized(this) {                val instance =                    Room.databaseBuilder(                        context.applicationContext,                        UserDatabase::class.java,                        "userDb"                    )                        .addMigrations(MIGRATION_1_2)                        .build()                INSTANCE = instance                return instance            }        }    }}
复制代码
④使用更新后的数据

在 xml 布局中添加两个 Button:


<Button    android:id="@+id/btn_add_user_score"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:text="增加user的score数据"/><Button    android:id="@+id/btn_query_user_score"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:text="查询user的score数据"/>
复制代码


在 MainActivity 中加入:


private val userScoreDao by lazy {    UserDatabase.getInstance(this).getScoreDao()}...private lateinit var btnAddUserScore: Buttonprivate lateinit var btnQueryUserScore: Button...btnAddUserScore = findViewById(R.id.btn_add_user_score)btnQueryUserScore = findViewById(R.id.btn_query_user_score)...btnAddUserScore.setOnClickListener {            Thread{                userScoreDao.insertUserScore(ScoreEntity(userScore = 100))            }.start()        }btnQueryUserScore.setOnClickListener {            Thread{                userScoreDao.queryUserScoreData().forEach{                    Log.d(TAG,"userScore表的数据为:$it")                }            }.start()        }
复制代码


这样对数据库的一次手动迁移就完成啦!💪


如果你想继续升级,就重复之前的步骤,然后将 2→3


private val MIGRATION_2_3 = object : Migration(2, 3) {    override fun migrate(database: SupportSQLiteDatabase) {        database.execSQL(            """            .... 再一次新的操作        """.trimIndent()        )    }}....addMigrations(MIGRATION_1_2,MIGRATION_2_3)
复制代码

使用 Room 更多的骚操作!

①想知道更多的 Room 数据库迁移的操作吗,那你可以看看这篇文章:

https://www.modb.pro/db/139101

②更优雅的修改数据

在上面的修改数据操作中,我们是需要填入每个字段的值的,但是,大部分情况,我们是不会全部知道的,比如我们不知道Userage,那么我们的age字段就填个Null吗?


val entity = UserEntity(name = "Taxze", age = null)
复制代码


这显然是不合适的!


当我们只想修改用户名的时,却又不知道 age 的值的时候,我们需要怎么修改呢?


⑴创建 UpdateNameBean


class UpdateNameBean(var id:Int,var name:String)
复制代码


⑵在 Dao 中加入新的方法


@Update(entity = UserEntity::class)fun updataUser2(vararg updataNameBean:UpdateNameBean)
复制代码


⑶然后在使用时只需要传入 id,和 name 即可


userDao.updateUser2(updataNameBean(2, "Taxzeeeeee"))
复制代码


当然你也可以给用户类创建多个构造方法,并给这些构造方法添加@lgnore

③详解 @Insert 插入
@Daointerface UserDao {    @Insert(onConflict = OnConflictStrategy.REPLACE)    fun insertUsers(vararg userEntity: UserEntity)}
复制代码


其中onConflict用于设置当事务中遇到冲突时的策略。


有如下一些参数可以选择:


  • OnConflictStrategy.REPLACE : 替换旧值,继续当前事务

  • OnConflictStrategy.NONE : 忽略冲突,继续当前事务

  • OnConflictStrategy.ABORT : 回滚

④@Query 指定参数查询

每次都查表的全部信息这也不是事啊,我们要用到 where 条件来指定参数查询。


@Daointerface UserDao {    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")    fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<UserEntity>}
复制代码


大家可以自己学习一下 SQL 语法~

⑤多表查询

很多业务情况下,我们是需要同时在多张表中进行查询的。


@Daointerface UserDao {    @Query(        "SELECT * FROM user " +                "INNER JOIN score ON score.id = user.id "  +                "WHERE user.name LIKE :userName"    )    fun findUsersScoreId(userName: String): List<UserEntity>}
复制代码
⑥@Embedded 内嵌对象

我们可以使用 @Embedded 注解,将一个 Entity 作为属性内嵌到另外一个 Entity,然后我们就可以像访问 Column 一样去访问内嵌的 Entity 啦。


data class Score(    val id:Int?,    val score:String?,)@Entity(tableName = "user")data class UserEntity(    @PrimaryKey(autoGenerate = true) val id:Int = 0,    .....    @Embedded val score: Score?)
复制代码
⑦使用@Relation 注解和 foreignkeys注解来描述Entity之间更复杂的关系

可以实现一对多,多对多的关系

⑧预填充数据库

可以查看官方文档:https://developer.android.google.cn/training/data-storage/room/prepopulate

⑨类型转换器 TypeConverter

....

Room 配合 LiveData 和 ViewModel

下面我们通过一个Room+LiveData+ViewModel的例子来完成这篇文章的学习吧


话不多说,先上效果图:


①创建 UserEntity
@Entity(tableName = "user")data class UserEntity(    @PrimaryKey(autoGenerate = true) val id: Int = 0,    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name: String?,    @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age: Int?,)
复制代码
②创建对应的 Dao
@Daointerface UserDao {    //添加用户    @Insert    fun addUser(vararg userEntity: UserEntity)    //查找用户    //返回user表中所有的数据,使用LiveData    @Query("select * from user")    fun getUserData(): LiveData<List<UserEntity>>}
复制代码
③创建对应的 Database

代码在最开始的例子中已经给出了。

④创建 ViewModel
class UserViewModel(userDao: UserDao):ViewModel(){    var userLivedata = userDao.getUserData()}
复制代码
⑤创建 UserViewModelFactory

我们在UserViewModel类中传递了UserDao参数,所以我们需要有这么个类实现ViewModelProvider.Factory接口,以便于将UserDao在实例化时传入。


class UserViewModelFactory(private val userDao: UserDao) : ViewModelProvider.Factory {    override fun <T : ViewModel?> create(modelClass: Class<T>): T {        return UserViewModel(userDao) as T    }}
复制代码
⑥编辑 xml

activity_main:


<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/user_name"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:hint="请输入UserName" />    <EditText        android:id="@+id/user_age"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:hint="请输入UserAge" />    <Button        android:id="@+id/btn_add"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="添加一条user数据" />    <ListView        android:id="@+id/recycler_view"        android:layout_width="match_parent"        android:layout_height="match_parent">    </ListView></LinearLayout>
复制代码


创建一个simple_list_item.xml,用于展示每一条用户数据


<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/userText"    android:layout_width="wrap_content"    android:layout_height="wrap_content" />
复制代码
⑦在 MainActivity 中调用
class MainActivity : AppCompatActivity() {    private var userList: MutableList<UserEntity> = arrayListOf()    private lateinit var arrayAdapter: ArrayAdapter<UserEntity>    private val userDao by lazy {        UserDatabase.getInstance(this).getUserDao()    }    lateinit var viewModel: UserViewModel    private lateinit var listView: ListView    private lateinit var editUserName: EditText    private lateinit var editUserAge: EditText    private lateinit var addButton: Button    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        init()        arrayAdapter = ArrayAdapter(this, R.layout.simple_list_item, userList)        listView.adapter = arrayAdapter        //实例化UserViewModel,并监听LiveData的变化。        viewModel =            ViewModelProvider(this, UserViewModelFactory(userDao)).get(UserViewModel::class.java)        viewModel.userLivedata.observe(this, Observer {            userList.clear()            userList.addAll(it)            arrayAdapter.notifyDataSetChanged()        })        addButton.setOnClickListener {            addClick()        }    }    //初始化控件    private fun init() {        editUserName = findViewById(R.id.user_name)        editUserAge = findViewById(R.id.user_age)        addButton = findViewById(R.id.btn_add)        listView = findViewById(R.id.recycler_view)    }    fun addClick() {        if (editUserName.text.toString() == "" || editUserAge.text.toString() == "") {            Toast.makeText(this, "姓名或年龄不能为空", Toast.LENGTH_SHORT).show()            return        }        val user = UserEntity(            name = editUserName.text.toString(),            age = editUserAge.text.toString().toInt()        )        thread {            userDao.addUser(user)        }    }}
复制代码


这样一个简单的 Room 配合 LiveData 和 ViewModel 实现页面自动更新的 Demo 就完成啦🌹具体代码可以查看Git仓库😉

尾述

看完这篇文章,相信你已经发现 Room 虽然看上去还是有些繁琐,但是相比较于 SQLite 还是简单不少了,Room 还能帮你检测 SQL 是否正确哈哈。这篇文章已经很详细的讲了 Jetpack Room 的大部分用法,不过在看完文章后,你仍需多多实践,相信你很快就可以掌握 Room 啦😺 因为我本人能力也有限,文章有不对的地方欢迎指出,有问题欢迎在评论区留言讨论~

关于我

Hello,我是 Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,也欢迎关注我的博客


如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?😝


基础系列:


2022 · 让我带你Jetpack架构组件从入门到精通 — Lifecycle


学会使用LiveData和ViewModel,我相信会让你在写业务时变得轻松🌞


当你真的学会DataBinding后,你会发现“这玩意真香”!


Navigation — 这么好用的跳转管理框架你确定不来看看?


Room:又要写业务代码了?看看我吧,给你飞一般的感觉!(本文🌟)


以下部分还在码字,赶紧点个收藏吧🔥


2022 · 让我带你 Jetpack 架构组件从入门到精通 — Paging3


2022 · 让我带你 Jetpack 架构组件从入门到精通 — WorkManager


2022 · 让我带你 Jetpack 架构组件从入门到精通 — ViewPager2


2022 · 让我带你 Jetpack 架构组件从入门到精通 — 登录注册页面实战(MVVM)


进阶系列:


协程 + Retrofit 网络请求状态封装


Room 缓存封装


.....


发布于: 刚刚阅读数: 5
用户头像

他日若遂凌云志 敢笑黄巢不丈夫 2020.10.15 加入

曾许少年凌云志,誓做人间第一流. 一起加入Flutter技术交流群532403442 有好多好多滴学习资料喔~ 小T目前主攻Android与Flutter, 通常会搞搞人工智能、SpringBoot 、Mybatiys等.

评论

发布
暂无评论
Room:又要写业务代码了?看看我吧,给你飞一般的感觉!_数据库_编程的平行世界_InfoQ写作社区