乐趣区

关于android:Room又要写业务代码了看看我吧给你飞一般的感觉

前言:

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

🌟Room 官网文档:https://developer.android.goo…

🌟SQL 语法教程:https://www.runoob.com/sqlite…

🚀本文代码地址:https://github.com/taxze6/Jet…

为什么要应用 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…,不过 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)
    }
}

后果:

到这里咱们曾经讲完了 Room 的最根本的应用,如果只是一些非常简单的业务,你看到这里曾经能够去写代码了,然而还有一些进阶的操作需解说一下,持续往下看吧!

数据库的降级

Room 在 2021 年 4 月 21 日公布的版本 2.4.0-alpha01 中开始反对主动迁徙,不过很多敌人反馈还是有很多问题,倡议手动迁徙,当然如果你应用的是更低的版本只能手动迁徙啦。

具体信息请参考:https://developer.android.goo…

具体如何降级数据库呢?上面咱们一步一步来实现吧!

①批改数据库版本

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.goo…

⑨类型转换器 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
@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 缓存封装

…..

退出移动版