初探Jetpack(四) -- ROOM 數據庫

初探Jetpack(一) – ViewModel
初探Jetpack(二) – Lifecycles
初探Jetpack(三) – LiveData
Demo工程

Android 雖然自身攜帶SQLite,但是操作比較麻煩,而且如果再大型項目,會變得比較混亂且難以維護,除非你設計了一套非常好的架構和封裝。
當然,如果要操作簡單的話,郭老師的 Litepal 算不錯的,不過我們今天學習 google 在 Jetpack 中帶的組件 — ROOM ,今天一起來學習。

一 、ROOM 的基本使用

首先,ROOM 由 Entity ,Dao 和 Database 三個部分組成:

  • Entity:用於定義iFeng準給實際數據的實體類,每個實體類都會在數據庫中對應一張表,並且表中的列是根據實體類的字段自動生成的。
  • Dao :Dao 是數據訪問對象的意思,通常會在這裏對數據庫的各項操作進行封裝,比如 增刪查改,這樣,訪問數據時,就不用去管底層數據庫了,只需要跟Dao打交道即可
  • Database:用於定義數據庫中的關鍵信息,包括版本號,包含哪些實體類以及 提供 Dao 的訪問層

如果你需要使用 ROOM ,需要在你的moudle build.gradle 添加插件

apply plugin: 'kotlin-kapt'

關聯依賴:

//room
implementation 'androidx.room:room-runtime:2.1.0'
kapt 'androidx.room:room-compiler:2.1.0'

kpt 爲註解的意思,相當於 java 的 annotationProcessor。

1.2 創建實體類

接着,我們創建一個實體類,User:

@Entity
data class UserData (
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") var lastName: String?
)

可以看到,我們用了 @Entity 註解,將它申明成一個實體類,然後添加了一個 id 字段,並使用 @PrimaryKey 註解將它設置成主鍵,然後參數使用 @ColumnInfo 註解,表示表的列。

1.3 設置 Dao

在數據庫中,最常見的就是 增刪查改了,但業務是千變萬化的,而 Dao 要做的事情就是覆蓋這些業務,這樣我們的邏輯就只要和 Dao 打交道,而不用去理會底層數據庫。

新建一個 UserDao ,注意必須是接口,然後編寫以下代碼:

@Dao
interface UserDao {

    @Query("SELECT * FROM userdata")
    fun getAll(): List<UserData>

    @Update
    fun updateUser(user: UserData)

    @Query("SELECT * FROM userdata WHERE first_name LIKE :first AND " +
            "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): UserData

    @Insert
    fun insertAll(vararg users: UserData)

    @Delete
    fun delete(user: UserData)

    @Query("delete from UserData where last_name = :lastName")
    fun deleteByLastName(lastName:String) :Int
}

可以看到,UserDao 用 @Dao 註解,ROOM 纔會識別成 Dao,並提供了 @Insert、@Update、 @Delete, @Query 四種對應的註解。

其中 @Insert 插入數據後,會返回自動生辰的主鍵 id,而特別要注意則是 @Query 註解,ROOM 無法知道我們要查詢哪些數據,因此必須編寫 SQL 語句纔行。
如果我們不是用實體類參數去 增刪改 數據,那麼也要編寫SQL 語句纔行,這個時候,不能使用 @Inset @Delete @update 註解,而都是使用 @Query 註解纔行,比如上面的 deleteByLastName 方法。

雖然要編寫 SQL 語句這點不太友好,但Room是編譯時動態檢查 SQL 語句的,也就是說,如果你的SQL 沒有寫對,編譯時就會報錯。

1.3 編寫 Database

Database 需要定義版本號啊,包含了哪些實體類,以及提供 Dao 層的訪問實例即可。新建一個 AppDatabase.kt ,代碼如下:


@Database(version = 1,entities = [UserData::class])
abstract class AppDataBase : RoomDatabase(){
    abstract fun userDao() : UserDao

    companion object{
        private var instance : AppDataBase ? = null;
        @Synchronized
        fun getDatabase(context:Context) : AppDataBase{
            instance?.let {
                return it
            }

            val db = Room.databaseBuilder(
                context.applicationContext,
                AppDataBase::class.java, "AppDataBase"
            ).build()
            return db.apply {
                instance = this;
            }
        }
    }
}

可以看到,AppDataBase 類的頭部使用了 @Database 的註解,並填寫了版本號,以及實體類,如果有多個實體類,用逗號隔開即可。

需要注意的是,AppDataBase 需要申明爲抽象類,並申明 Dao 的抽象方法,比如 userDao()。

接着,由於Dao 理論上應該就是一個單例模式,所以這裏用 companion objec 修飾,當 getDatabase 中,如果已經存在,則直接返回,如果不存在,則通過 Room.databaseBuilder 去創建build,它接收三個參數:

  1. context:這裏最好用 applicationContext 防止內存泄漏
  2. class 類型,這裏傳遞 AppDataBase::class.java
  3. 數據庫名
    然後通過 apply 拿到實例,並賦值給 instance 即可。

ok,Room 的配置已經完成了,接着,我們在 xml 中添加 4 個按鈕:

    <Button
        android:id="@+id/addDataBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

    <Button
        android:id="@+id/updateBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data"/>

    <Button
        android:id="@+id/deleteBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="delete data"/>

    <Button
        android:id="@+id/queryBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="query data"/>

在這裏插入圖片描述
在 activity 中添加代碼:

        val userDao = AppDataBase.getDatabase(this).userDao();
        val user1 = UserData(1,"z","sr")
        val user2 = UserData(2,"w","y")

        //添加數據
        addDataBtn.setOnClickListener{
            thread {
                userDao.insertAll(user1,user2)
            }
        }
        //更新數據
        updateBtn.setOnClickListener{
            thread {
                 user2.lastName = "san"
                 userDao.updateUser(user2)
            }
        }

        //刪除
        deleteBtn.setOnClickListener{
            thread {
               // userDao.delete(user1)
                userDao.deleteByLastName("sr")
            }
        }

        //查詢
        queryBtn.setOnClickListener{
            thread {
                for (user in userDao.getAll()){
                    Log.d(TAG, "zsr onCreate: "+user.toString())
                }
            }
        }

ROOM 要求查詢數據庫在 線程中,所以這裏用了 thread{},如果你覺得需要在主線程中去更新,可以在配置中設置:

val db = Room.databaseBuilder(
            context.applicationContext,
            AppDataBase::class.java, "AppDataBase"
        ).allowMainThreadQueries().build()

點擊增加,然後按查詢:
在這裏插入圖片描述
點擊更新,然後查詢:
在這裏插入圖片描述
點擊刪除,然後查詢:
在這裏插入圖片描述

二、數據庫升級

ROOM 的數據庫升級比較麻煩,如果在測試階段,可以使用 fallbackToDestructiveMigration() 強制升級

val db = Room.databaseBuilder(
    context.applicationContext,
    AppDataBase::class.java, "AppDataBase"
).fallbackToDestructiveMigration().build()

2.1 、新增一張表

但,僅限於測試,如果我要增加一張表呢?比如增加一個 Book:

@Entity
data class Book(var name:String,var pages:Int) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0;
}

並添加一個 BookDao 接口:

@Dao
interface BookDao  {
    @Insert
    fun insertBook(book: Book)
    
    @Query("select * from Book")
    fun loadAllBooks() : List<Book>
}

然後修改 AppDataBase:

@Database(version = 2, entities = [UserData::class, Book::class])
abstract class AppDataBase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun bookDao(): BookDao
    companion object {

	val Migration1_2 = object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("create table Book (id integer primary key autoincrement not null," +
                            "name text not null ,pages integer not null)")
                }
            }

        private var instance: AppDataBase? = null;

        @Synchronized
        fun getDatabase(context: Context): AppDataBase {
            instance?.let {
                return it
            }
            
            val db = Room.databaseBuilder(
                context.applicationContext,
                AppDataBase::class.java, "AppDataBase"
            ).addMigrations(Migration1_2).build()
            return db.apply {
                instance = this;
            }
        }
    }
}

可以看到,我們在 @Database 註解中,修改版本爲2,並添加 BookDao 類。

接着,實現一個 Migration 匿名類,重寫 migrate 方法,並在裏面編寫 SQL 語句,添加一個 Book 表;接着,在 Room.databaseBuilder() 那裏,通過 addMigrations(Migration1_2) 添加進去。
這樣,當 SQL 版本從1變到2 的時候,就會執行 Migration1_2 裏面的方法。

接着,調用一下:

        val bookDao = AppDataBase.getDatabase(this).bookDao()

        //添加數據
        addDataBtn.setOnClickListener{
            thread {
                 bookDao.insertBook(Book("android",100))
            }
        }
    

        //查詢
        queryBtn.setOnClickListener{
            thread {
                for (book in bookDao.loadAllBooks()){
                    Log.d(TAG, "zsr onCreate: "+book.toString())
                }
            }
        }

點擊插入,並查詢:
在這裏插入圖片描述

2.2 、新增字段

但不是每次升級都升級一張表,假如要加贈一個字段呢?比如新增 Book 的作者名,author,修改 Book 類:

@Entity
data class Book(var name:String,var pages:Int,var author:String) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0;
}

由於 Book 變動了,所以,AppDatabase 也需要改變:

@Database(version = 3, entities = [UserData::class, Book::class])
abstract class AppDataBase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun bookDao(): BookDao

    companion object {
	... 
    val Migration2_3 = object : Migration(2, 3) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("alter table Book add column author text not null default 'unknown'")
                }
            }
        private var instance: AppDataBase? = null;

        @Synchronized
        fun getDatabase(context: Context): AppDataBase {
            instance?.let {
                return it
            }

        

            val db = Room.databaseBuilder(
                context.applicationContext,
                AppDataBase::class.java, "AppDataBase"
            ).addMigrations(Migration1_2,Migration2_3).build()
            return db.apply {
                instance = this;
            }
        }
    }
}

可以看到,版本改稱3,且增加了一個 Migration2_3 ,SQL 語句使用 alert 插入一個列。

這樣,我們就學習完成了。

參考:
第一行代碼,第三版
官網 ROOM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章