使用 Android 資料庫: Room

我們今天來介紹如何導入使用 Android 的資料庫 Room,Android Jetpack 套件元件之一,如何融入 MVVM 架構,並且善用一些特性減少開發上的困難。

先來看看 Room 有哪些特點:

  1. Room 把一些 SQLite 底層實作封裝起來讓我們能更方便存取資料庫,不需要再寫冗長的程式碼才能將 SQL 和 Kotlin程式資料類別轉換
  2. Room支援編譯時期的 SQL 語法檢查,不需要等到執行後才能發現錯誤。
  3. 容易整合且語法簡單需多,少掉很多囉唆的程式碼。
  4. 支援LiveData / RxJava,可以使用觀察者模式來訂閱資料變更。

這篇文章將會使用社群 App 發文草稿當作範例來說明,需求是在 App 上可以發布新的影片貼文,發布過程中要把新貼文當作草稿暫存下來,且在草稿列表中顯示上傳狀態,直到上傳成功後才把暫存貼文刪除。

這邊我們完全不會解釋資料庫相關的概念,這篇我們會假設你都已經有資料庫基礎,知道資料庫、表格、欄位、primary key、foreign key…等是什麼

設計架構

我們使用 MVVM 架構,Model 提供一個資料流給 ViewModel,ViewModel 再讓這資料流給 View 去訂閱然後顯示。 依照需求來看我們需要把新貼文儲存起來在資料庫,當要發新貼文時,我們會將新產生的貼文物件寫入資料庫,然後因為 View 觀察(訂閱)資料的變更,當有資料庫有新貼文寫入或更新時,列表自動會收到更新顯示新貼文或變更。

元件架構圖和資料流如下:

MVVM Data Stream

Room 在版本 v2.1.0 已經對於 RxJava 有非常完整的整合,所以我們採用 RxJava + Room 架構來實做。

實作

Room 有三個主要元件,架構如下圖:

Photo credit: Android Developer Doc

1. Entity

首先,第一個元件是 Entity (以下稱「實體」),這是一個類別用來表示資料庫的表格,會用 @Entity 來標註(可以用 tableName = ... 來設定表格名稱),接著用 @ColumnInfo 標註類別內的屬性成為表格欄位,預設情況在實體類別內的所有屬性都會儲存到資料庫,對於不想儲存的屬性,可以加上 @Ignore 來略過。

對於新訊息我們會儲存文字、上傳檔案、貼文種類、打卡位置,所以我們的實體類別會是:

依據我們實體類別的宣告,資料庫表格就會長這樣:

idcaptionmediaFiletypelocation
57ad8bce0aHi, 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,所以我們寫一個轉接器:

這個轉換類別轉換了 PostTypeFile,還有一個 Location 物件還沒轉換,這邊我們會另外使用 Room 另一個annotation: @Embedded 來處理,我們回到 entity class,在 Location 加上 @Embedded ,Room 在儲存這個欄位的時候,就會把 Location 裡面的每個欄位攤平一起成為 NewPost 這個表格裡面的欄位,攤平物件後的表格欄位就會長這樣:

idcaptionmediaFiletypenamelatlng
57ad8bce0aHi, …./…

為了預防攤平後的欄位名稱衝突 (可能 NewPostLocation 都有定義相同的屬性名稱),我們可以在 @Embedded 加上(prefix= …),Room 儲存這些攤平欄位時,就會幫我們加上這些前綴以避免命名衝突。

Reactive Design Pattern

在 MVVM 的架構中,有一個重要的核心概念就是資料流,Model 提供原始的資料流,ViewModel 把 Model 的資料流轉換為 UI 呈現的資料流讓 View 來訂閱/監聽。每當 Model 的資料流有變更時,View 因為訂閱了該資料流,所以 UI 也會自動跟著變更。

Reactive

Room 本身的設計也是支援這樣的模式,每當資料庫有變更時,能夠自動觸發 UI 的更新,我們的草稿列表是訂閱 Room 的 SELECT * FROM table 的查詢,每當表格有變動時,Room 就會發出資料變更的通知,那我們 UI 有訂閱所以會收到這通知而自動更新 UI。

結語

Room 使用上比原本的 SQLiteOpenHelper 簡單許多,只需要幾個簡單的標注即可完成資料庫、DAO、Entity 的產生和設定,也支援 Reactive 方式來讓 UI 綁定資料庫的資料變更,讓資料有變動時,UI 可以自動變更,而不需要另外設值,對開發者來說是相當方便的。


4 thoughts on “使用 Android 資料庫: Room

Add yours

    1. 我文中的DAO裡面其實有實作一個方法叫做upsert,因為Room再insert的時候會回傳成功寫入的筆數,然後搭配 onConflict = OnConflictStrategy.IGNORE,如果寫入重複資料的時候,會回傳 -1,這時候你就可以換執行 update() 方法。

      Like

Leave a Reply to Kevin Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑