天天看點

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​

    ​語句的使用更加貼近,能夠降低學習成本
  • 對​

    ​RxJava​

    ​​、​

    ​LiveData​

    ​​ 、​

    ​Kotlin​

    ​協程等都支援

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​

​​負責提供通路​

​DB​

​​的​

​API​

​​,我們每一張表都需要一個​

​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的最基本的使用,如果隻是一些非常簡單的業務,你看到這裡已經可以去寫代碼了,但是還有一些進階的操作需講解一下,繼續往下看吧!

資料庫的更新

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)      

②更優雅的修改資料

在上面的修改資料操作中,我們是需要填入每個字段的值的,但是,大部分情況,我們是不會全部知道的,比如我們不知道​

​User​

​​的​

​age​

​​,那麼我們的​

​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​

​的例子來完成這篇文章的學習吧

話不多說,先上效果圖:

Room:又要寫業務代碼了?看看我吧,給你飛一般的感覺!

①建立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:orientatinotallow="vertical"
    tools:cnotallow=".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 versinotallow="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)

進階系列: