前幾天我們學到了如何使用 api 獲取資料,取得了資料以後有時候我們會想要把它存起來讓之後使用可以更快速,這個時候我們就必須學習如何存資料了。
儲存資料有好幾種方法,其中一種就是藉由 Room 把資料存在 Database 囉,不過在使用 database 之前可以比較看看是否有其他更適合的方式。
這也是為什麼叫做 SharedPreferences 的原因之一喔。
CRUD -> C: Create、R: Read、U: Update、D: Delete
更多資訊請參考:
https://developer.android.com/guide/topics/data/data-storage
確定 database 就是你所要的後,就讓我們繼續看下去!
ORM stands for Object-relational mapping,我們都知道 SQLite 在存取資料的時候是沒有物件的概念的,但 java/kotlin 的世界則資料都是一個一個物件,ORM 就是幫我們自動做轉換的動作,有了這一層轉換我們可以很輕鬆把一個物件存到 database 或是從 database 取出一組物件列表等。
起手式一樣是要加上 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 有以下的三層架構我們必須一個一個談:
我們現在就一層一層來介紹:
@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
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:
@Insert
@Query
@Update
@Delete
Read 這個行為比較特別因為通常查詢不需回傳全部資料,所以 @Query
必須放 SQL 語句來表示我們的查詢條件,Room 很厲害的地方就是他會自動連結到我們的 Entity,所以如果我們有哪個欄位名字打錯的話 IDE 也會提醒我們。
眼尖的大家應該也會發現我們使用 :
來關聯我們 function 的參數,這邊也同樣有偵錯的功能。
@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 範例,@Database
用 entities
參數來指定所有需要的 table,還有個 version
參數來指定目前的版本,這是為了之後如果要修改 table 欄位的時候,我們必須有個版本號來做轉換。
getInstance
的存在則是因為 database 的建立非常的耗資源,我們傾向用一個變數來重複使用已經建立過的 database 物件。
既然有 getInstance 的出現,相信大家也應該猜的到我們應該要怎麼使用了吧。
val dao = UserDatabase.getInstance(this).userDao()
dao.insert(user)
dao.update(user)
dao.delete(user)
將來我們學習了 dependency injection 之後,還可以進一步優化這整個流程喔!
有發現我們在 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