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

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

作者: 编程的平行世界 | 来源:发表于2022-07-15 10:00 被阅读0次

    前言:

    🏀在我们日常开发中,经常要和数据打交道,所以存储数据是很重要的事。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呢?

    ①添加依赖
    最近更新时间(文章发布时的最新版本) 稳定版 Alpha 版
    2022 年 6 月 1 日 2.4.2 2.5.0-alpha02
    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)处理增删改查
    @Dao
    interface 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()进去
    • Class<?> entity() default Object.class;
      
    • @Query则是需要传递SQL语句
    • 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)
        }
    }
    

    结果:

    [图片上传失败...(image-2655fe-1657850280931)]
    到这里我们已经讲完了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
    @Dao
    interface 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: Button
    private 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 插入
    @Dao
    interface UserDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertUsers(vararg userEntity: UserEntity)
    }
    

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

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

    • OnConflictStrategy.REPLACE : 替换旧值,继续当前事务
    • OnConflictStrategy.NONE : 忽略冲突,继续当前事务
    • OnConflictStrategy.ABORT : 回滚
    ④@Query 指定参数查询

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

    @Dao
    interface UserDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<UserEntity>
    }
    

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

    ⑤多表查询

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

    @Dao
    interface 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的例子来完成这篇文章的学习吧

    话不多说,先上效果图:

    [图片上传失败...(image-4dbbbc-1657850280931)]

    ①创建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
    @Dao
    interface 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 缓存封装

    .....

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/ytvqirtx.html