Jetpack Room

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

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

Room的具体优势:

  • 有可以最大限度减少重复和容易出错的样板代码的注解
  • 简化了数据库迁移路径
  • 针对编译期 SQL的语法检查
  • API设计友好,更容易上手,理解
  • SQL语句的使用更加贴近,能够降低学习成本
  • RxJavaLiveDataKotlin协程等都支持

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

  • Dao: 数据库访问对象,用于访问和管理数据(增删改查)。在使用时需要 @DAO注解
  • Database: 它作为数据库持有者,用 @Database注解和 Room Database扩展的类

最近更新时间(文章发布时的最新版本)稳定版Alpha 版2022 年 6 月 1 日

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(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?

)

其中,每个表的字段都要加上 @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
interface UserDao {

    @Insert
    fun addUser(vararg userEntity: UserEntity)
​

    @Delete
    fun deleteUser(vararg userEntity: UserEntity)
​

    @Update
    fun updateUser(vararg userEntity: UserEntity)
​

    @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 {

    String value();
}

☀注意:Room会在编译期基于Dao自动生成具体的实现类,UserDao_Impl(实现增删改查的方法)。
🔥Dao所有的方法调研都在当前线程进行,需要避免在UI线程中直接访问!

@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_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)
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e35hYmSg-1657849942540)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/50e10c57e1fa41a6960b3c5d87fd9713~tplv-k3u1fbpfcp-watermark.image?)]
到这里我们已经讲完了Room的最基本的使用,如果只是一些非常简单的业务,你看到这里已经可以去写代码了,但是还有一些进阶的操作需讲解一下,继续往下看吧!

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

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

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
interface ScoreDao {
    @Insert
    fun insertUserScore(vararg scoreEntity: ScoreEntity)
​
    @Query("select * from score")
    fun queryUserScoreData():List<ScoreEntity>
}
@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2)
abstract class UserDatabase : RoomDatabase() {
    abstract fun getUserDao(): UserDao

    abstract fun getScoreDao():ScoreDao
​
    companion object {

        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)

在上面的修改数据操作中,我们是需要填入每个字段的值的,但是,大部分情况,我们是不会全部知道的,比如我们不知道 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

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg userEntity: UserEntity)
}

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

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

  • OnConflictStrategy.REPLACE : 替换旧值,继续当前事务
  • OnConflictStrategy.NONE : 忽略冲突,继续当前事务
  • OnConflictStrategy.ABORT : 回滚

每次都查表的全部信息这也不是事啊,我们要用到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注解,将一个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?

)

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

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

话不多说,先上效果图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGq7L7fj-1657849942542)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/106dc07d0e1c43b0a3b54e04d4e80678~tplv-k3u1fbpfcp-watermark.image?)]

@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
interface UserDao {
    //&#x6DFB;&#x52A0;&#x7528;&#x6237;
    @Insert
    fun addUser(vararg userEntity: UserEntity)
&#x200B;
    //&#x67E5;&#x627E;&#x7528;&#x6237;
    //&#x8FD4;&#x56DE;user&#x8868;&#x4E2D;&#x6240;&#x6709;&#x7684;&#x6570;&#x636E;,&#x4F7F;&#x7528;LiveData
    @Query("select * from user")
    fun getUserData(): LiveData<list<userentity>>
}
</list<userentity>

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

class UserViewModel(userDao: UserDao):ViewModel(){
    var userLivedata = userDao.getUserData()
}

我们在 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
    }
}
</t></t>

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">
&#x200B;
    <edittext android:id="@+id/user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="&#x8BF7;&#x8F93;&#x5165;UserName">
&#x200B;
    <edittext android:id="@+id/user_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="&#x8BF7;&#x8F93;&#x5165;UserAge">
&#x200B;
    <button android:id="@+id/btn_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="&#x6DFB;&#x52A0;&#x4E00;&#x6761;user&#x6570;&#x636E;">
&#x200B;
    <listview android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent">
    </listview>
</button></edittext></edittext></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">
</textview>
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
        //&#x5B9E;&#x4F8B;&#x5316;UserViewModel&#xFF0C;&#x5E76;&#x76D1;&#x542C;LiveData&#x7684;&#x53D8;&#x5316;&#x3002;
        viewModel =
            ViewModelProvider(this, UserViewModelFactory(userDao)).get(UserViewModel::class.java)
        viewModel.userLivedata.observe(this, Observer {
            userList.clear()
            userList.addAll(it)
            arrayAdapter.notifyDataSetChanged()
        })
        addButton.setOnClickListener {
            addClick()
        }
    }
&#x200B;
    //&#x521D;&#x59CB;&#x5316;&#x63A7;&#x4EF6;
    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)
    }
&#x200B;
    fun addClick() {
        if (editUserName.text.toString() == "" || editUserAge.text.toString() == "") {
            Toast.makeText(this, "&#x59D3;&#x540D;&#x6216;&#x5E74;&#x9F84;&#x4E0D;&#x80FD;&#x4E3A;&#x7A7A;", Toast.LENGTH_SHORT).show()
            return
        }
        val user = UserEntity(
            name = editUserName.text.toString(),
            age = editUserAge.text.toString().toInt()
        )
        thread {
            userDao.addUser(user)
        }
    }
}
</userentity></userentity>

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

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

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

基础系列:

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

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

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

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

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

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

进阶系列:

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

Room 缓存封装

Original: https://blog.csdn.net/txaz6/article/details/125798109
Author: 编程的平行世界
Title: Jetpack Room

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/817289/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球