iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
0
Mobile Development

Android 十全大補系列 第 15

[Android 十全大補] Room

前幾天我們學到了如何使用 api 獲取資料,取得了資料以後有時候我們會想要把它存起來讓之後使用可以更快速,這個時候我們就必須學習如何存資料了。

儲存資料有好幾種方法,其中一種就是藉由 Room 把資料存在 Database 囉,不過在使用 database 之前可以比較看看是否有其他更適合的方式。

  • File:
    可以使用標準的 Java File api 把資料儲存在手機裡,通常照片、影片等 file base 的資料都很適合直接存成 File。
  • SharedPreferences:
    可以儲存簡單的 key-value 形態的資料,支援諸如 int、string 等基本型別,如果只是要存非常小的 config 比如說使用者的偏好設定等就很適合喔。

這也是為什麼叫做 SharedPreferences 的原因之一喔。

  • Database:
    通常固定格式多欄位、多筆數的複雜資料適合存在 database 裡,可以使用簡單的語法進行 CRUD 等操作。

CRUD -> C: Create、R: Read、U: Update、D: Delete

更多資訊請參考:
https://developer.android.com/guide/topics/data/data-storage

確定 database 就是你所要的後,就讓我們繼續看下去!

ORM

ORM stands for Object-relational mapping,我們都知道 SQLite 在存取資料的時候是沒有物件的概念的,但 java/kotlin 的世界則資料都是一個一個物件,ORM 就是幫我們自動做轉換的動作,有了這一層轉換我們可以很輕鬆把一個物件存到 database 或是從 database 取出一組物件列表等。

Install

起手式一樣是要加上 room 的 dependencies。

dependencies {
    implementation "androidx.room:room-runtime:2.2.0-rc01"
    kapt "androidx.room:room-compiler:2.2.0-rc01"
}

還記得 kapt 嗎?kapt stands for kotlin annotation processing,如果對 annotation processing 有興趣的話可以參考我們之前的介紹:[Android 十全大補] Annotation Processing

Room 有以下的三層架構我們必須一個一個談:

  1. Entity:
    我們資料在 java/kotlin 這層以物件形式存在的。
  2. Dao:
    Dao stands for Data access object,顧名思義就是可以用這個物件來存取我們的 data,所以這個 Dao 裡會定義一系列的 function 讓我們使用。
  3. Database:
    database 真正實體,定義了在系統的名字以及他所包含的所有 table 以及版本。

我們現在就一層一層來介紹:

Entity

@Entity(tableName = "users")
data class User(
    @PrimaryKey
    @ColumnInfo(name = "userid")
    val id: String,
    val userName: String
)

一個簡單的 Entity 大概會長得像這樣子,我們必須透過 @Entity 來宣告我們所要綁定的 table 資料格式,然後透過 object 的 attribute 來宣告我們的所以資料欄位,Room 會把所有 attribute 跟 table 裡的同名欄位做自動關聯,如果無法使用一樣的名字,我們就要用 @ColumnInfo 這個 annotation 來告訴 room 我們所要連結的欄位名稱,最後 @PrimaryKey 則是宣告這是我們主要區分資料的欄位。

Dao

@Dao
interface UserDao {
    @Insert
    fun insert(user: User)

    @Update
    fun update(user: User)

    @Delete
    fun delete(user: User)

    @Query("SELECT * FROM users WHERE userName LIKE :name")
    fun findUserByName(name: String): List<User>
}

如上所述,我們的 dao 就是定義一系列 CRUD 用來存取 database 的地方,Room 同樣透過一系列的 annotation 來連結 java/kotlin 層的呼叫跟 SQLite 層的實際 command。
每個 dao 都必須加上 @Dao 標記自己屬於 dao 物件,CRUD 各有對應的 annotation:

  • C: @Insert
  • R: @Query
  • U: @Update
  • D: @Delete

Read 這個行為比較特別因為通常查詢不需回傳全部資料,所以 @Query 必須放 SQL 語句來表示我們的查詢條件,Room 很厲害的地方就是他會自動連結到我們的 Entity,所以如果我們有哪個欄位名字打錯的話 IDE 也會提醒我們。
眼尖的大家應該也會發現我們使用 : 來關聯我們 function 的參數,這邊也同樣有偵錯的功能。

Database

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {

    companion object {
        private var instance: UserDatabase? = null
        private var DB_NAME = "db_name"

        fun getInstance(context: Context): UserDatabase {
            return instance ?: Room.databaseBuilder(context, UserDatabase::class.java, DB_NAME)
                .fallbackToDestructiveMigration()
                .build().also { instance = it }
        }
    }

    abstract fun userDao(): UserDao
}

這邊是個簡單的 database 範例,@Databaseentities 參數來指定所有需要的 table,還有個 version 參數來指定目前的版本,這是為了之後如果要修改 table 欄位的時候,我們必須有個版本號來做轉換。

getInstance 的存在則是因為 database 的建立非常的耗資源,我們傾向用一個變數來重複使用已經建立過的 database 物件。

既然有 getInstance 的出現,相信大家也應該猜的到我們應該要怎麼使用了吧。

val dao = UserDatabase.getInstance(this).userDao()

dao.insert(user)
dao.update(user)
dao.delete(user)

將來我們學習了 dependency injection 之後,還可以進一步優化這整個流程喔!

Migration

有發現我們在 Room.databaseBuilder 的建立使用了 fallbackToDestructiveMigration 嗎?

這個 function 的意思是我們不在乎 migration,任何有更新的欄位時我們就直接重建整張 table。但有沒有什麼比較正規的方法來實踐 migration 呢?

首先什麼是 migration 呢?

ORM 需要 java/kotlin 跟 database 二邊的格式一致,但我們 java/kotlin 層常會隨著業務需求需要改變資料結構,這時候就會需要通知 database 一起改變格式,更新 database 的欄位就叫做 migration。

當然 Room 也有支援 migration:

val MIGRATION_1_2: Migration = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN address STRING")
    }
}

首先定義一個 migration 的 command,然後再 database init 的時候指定,同時把 fallbackToDestructiveMigration 拿掉。

Room.databaseBuilder(context, UserDatabase::class.java, DB_NAME)
    .addMigrations(MIGRATION_1_2)
    .build()

database 的 migration 是個比較麻煩的操作,大家可以預見如果我們有操多版本的 migrate 應該會有很多無法拿掉的程式碼存在,這也是為什麼筆者會希望大家先確認是否真的需要一個 database 來儲存我們的資料。

總之,以上就是今天的內容囉,感謝大家。

Room 也可以跟 RxJava、LiveData 等搭配使用喔,寫程式就像疊積木ㄧ樣,越多積木就可以拚出越有意思的模型,大家一起加油吧!

Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786


上一篇
[Android 十全大補] Annotation Processing
下一篇
[Android 十全大補] RxJava
系列文
Android 十全大補30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言