使用 Android 資料庫: Room
摘要:編譯錯誤,出現 error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. 這錯誤表示 Room 無法將我們實體類別裡面的某些欄位存到資料庫,因為那些欄位不是基本型態,可能是集合類別或者我們自定義的類別, Room 不知道該如何儲存這些類別的欄位到資料庫去,所以才會出現這錯誤。最後就是資料庫本身的宣告,資料庫類別包含了表格的實體類別、版本,類別內包含了取得每個 DAO 的抽象方法,且類別要宣告為抽象類別,和 DAO 同樣原因,因為 Room 幫我們產生所有底層我們不在意的實作細節,我們使用上只需要知道這些介面和方法即可。
我們今天來介紹如何導入使用 Android 的資料庫 Room,Android Jetpack 套件元件之一,如何融入 MVVM 架構,並且善用一些特性減少開發上的困難。
先來看看 Room 有哪些特點:
- Room 把一些 SQLite 底層實作封裝起來讓我們能更方便存取資料庫,不需要再寫冗長的程式碼才能將 SQL 和 Kotli n程式資料類別轉換
- Room支援編譯時期的 SQL 語法檢查,不需要等到執行後才能發現錯誤。
- 容易整合且語法簡單需多,少掉很多囉唆的程式碼。
- 支援LiveData / RxJava,可以使用觀察者模式來訂閱資料變更。
這篇文章將會使用社群影片 App 發文草稿當作範例來說明,需求是在 App 上可以發布新的影片貼文,發布過程中要把新貼文當作草稿暫存下來,且在草稿列表中顯示上傳狀態,直到上傳成功後才把暫存貼文刪除。
這邊我們完全不會解釋資料庫相關的概念,這篇我們會假設你都已經有資料庫基礎,知道資料庫、表格、欄位、primary key、foreign key…等是什麼
設計架構
我們使用 MVVM 架構,Model 提供一個資料流給 ViewModel,ViewModel 再讓這資料流給 View 去訂閱然後顯示。 依照需求來看我們需要把新貼文儲存起來在資料庫,當要發新貼文時,我們會將新產生的貼文物件寫入資料庫,然後因為 View 觀察(訂閱)資料的變更,當有資料庫有新貼文寫入或更新時,列表自動會收到更新顯示新貼文或變更。
元件架構圖和資料流如下:
Room 在版本 v2.1.0 已經對於 RxJava 有非常完整的整合,所以我們採用 RxJava + Room 架構來實做。
實作
Room 有三個主要元件,架構如下圖:
1. Entity
首先,第一個元件是 Entity (以下稱「實體」),這是一個類別用來表示資料庫的表格,會用 @Entity
來標註(可以用 tableName = ...
來設定表格名稱),接著用 @ColumnInfo
標註類別內的屬性成為表格欄位,預設情況在實體類別內的所有屬性都會儲存到資料庫,對於不想儲存的屬性,可以加上 @Ignore
來略過。
對於新訊息我們會儲存文字、上傳檔案、貼文種類、打卡位置,所以我們的實體類別會是:
依據我們實體類別的宣告,資料庫表格就會長這樣:
id | caption | mediaFile | type | location |
57ad8bce0a | Hi, this is … | /… | … | … |
2. DAO
有了實體類別後,再來需要 DAO (Data Access Object) 資料存取物件來定義所有資料庫的存取方法,裡面包含了最常見的 CURD (Create, Update, Read, Delete) 方法。
Room 的 DAO 宣告就像是 Retrofit 的 API 都要宣告為 interface,每個查詢方法都加上 @Query(SQL)
/ @Insert
/ … 標注,底層 Room 自動幫我們產生對應的資料庫操作實作,這樣的架構設計出發點就是讓我們可以容易測試,我們更容易 Mock 資料庫存取,可以不用接真正的資料庫就可以測試:
這邊還有一點值得一提,就是在DAO @Query
裡面,Room編譯器會幫我們在開發階段的時候,就會檢查SQL語法,不需要等到執行階段才知道SQL語法錯誤。
3. Database
最後就是資料庫本身的宣告,資料庫類別包含了表格的實體類別、版本,類別內包含了取得每個 DAO 的抽象方法,且類別要宣告為抽象類別,和 DAO 同樣原因,因為 Room 幫我們產生所有底層我們不在意的實作細節,我們使用上只需要知道這些介面和方法即可。
這邊要注意的是,資料庫實體的產生和取得,官方建議用 singleton 的方式取得,因為實體的產生很耗資源,而且也不需要多個資料庫實體,所以宣告為 singleton 即可。
整合
資料庫元件都定義好之後,我們就可以來整合串接這些元件,依照上述的架構圖我們把列表串接起來。 記得!資料庫的存取操作不能在 Main Thread 執行,要記得切換 Schedulers 。
Repository
View & ViewModel
執行
整合完後就可以執行看看結果了,Oops!! 編譯錯誤,出現 error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. 這錯誤表示 Room 無法將我們實體類別裡面的某些欄位存到資料庫,因為那些欄位不是基本型態,可能是集合類別或者我們自定義的類別, Room 不知道該如何儲存這些類別的欄位到資料庫去,所以才會出現這錯誤。 這時候我們需要增加一個 TypeConverter 來告訴 Room 這些欄位該如何轉換後儲存。
我們實體類別有包含三個需要轉換的類別: File
, PostType
, Location
,所以我們寫一個轉接器:
這個轉換類別轉換了 PostType
和 File
,還有一個 Location
物件還沒轉換,這邊我們會另外使用 Room 另一個annotation: @Embedded
來處理,我們回到 entity class,在 Location
加上 @Embedded
,Room 在儲存這個欄位的時候,就會把 Location
裡面的每個欄位攤平一起成為 NewPost
這個表格裡面的欄位,攤平物件後的表格欄位就會長這樣:
id | caption | mediaFile | type | name | lat | lng |
57ad8bce0a | Hi, …. | /… | … | … | … | … |
為了預防攤平後的欄位名稱衝突 (可能 NewPost
和 Location
都有定義相同的屬性名稱),我們可以在 @Embedded
加上(prefix= …),Room 儲存這些攤平欄位時,就會幫我們加上這些前綴以避免命名衝突。
Reactive Design Pattern
在 MVVM 的架構中,有一個重要的核心概念就是資料流,Model 提供原始的資料流,ViewModel 把 Model 的資料流轉換為 UI 呈現的資料流讓 View 來訂閱/監聽。每當 Model 的資料流有變更時,View 因為訂閱了該資料流,所以 UI 也會自動跟著變更。
Room 本身的設計也是支援這樣的模式,每當資料庫有變更時,能夠自動觸發 UI 的更新,我們的草稿列表是訂閱 Room 的 SELECT * FROM table 的查詢,每當表格有變動時,Room 就會發出資料變更的通知,那我們 UI 有訂閱所以會收到這通知而自動更新 UI。
結語
Room 使用上比原本的 SQLiteOpenHelper 簡單許多,只需要幾個簡單的標注即可完成資料庫、DAO、Entity 的產生和設定,也支援 Reactive 方式來讓 UI 綁定資料庫的資料變更,讓資料有變動時,UI 可以自動變更,而不需要另外設值,對開發者來說是相當方便的。
Advertisements