在 Google I/O 2019,我們分享了 Room 2.2 的最新進展。儘管當時已經支援了很多功能,如 支援 Flow API,支援預填充資料庫,支援一對一及多對多資料庫關係,但是開發者們對 Room 有著更高的期望,我們也致力於此,在 2.2.0 - 2.4.0 版本中釋出了很多開發者們期待的新功能!包括自動化遷移,關係查詢方法以及支援 Kotlin Symbol Processing (KSP) 等等。下面我們就來逐一介紹這些新功能!
如果您更喜歡通過視訊瞭解此內容,請在此處檢視:
https://www.bilibili.com/vide...
△ 深入探討 Room 2.4.0 的最新進展
自動化遷移
在談自動化遷移之前,先看看什麼是資料庫遷移。假如您更改了資料庫 schema,就需要根據資料庫版本進行遷移,以防使用者裝置內建資料庫中現有資料丟失。
如果您使用 Room,那麼在 資料庫遷移 過程中會進行檢查並驗證更新後的 schema,另外您也可以在 @Database 中設定 exportSchema,來匯出 schema 資訊。
對於 Room 2.4.0 版本之前的資料庫遷移,您需要實現 Migration 類,並在其中編寫大量複雜冗長的 SQL 語句,來處理不同版本之間的遷移。這種手動遷移的形式,非常容易引發各種錯誤。
現在 Room 支援了自動遷移,讓我們通過兩個示例來對比手動遷移和自動遷移:
修改表名
假設有一個包含兩個表的資料庫,表名分別是 Artist 和 Track,現在想要將表名 Track 改為 Song。
如果使用手動遷移,必須編寫和執行 SQL 語句才能更改,需要如下操作:
val MIGRATION_1_2: Migration = Migration(1, 2) {
fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `Track` RENAME TO `Song`")
}
}
如果使用自動遷移,您只需要在定義資料庫時新增 @AutoMigration 配置,同時提供兩個版本資料庫匯出的 schema。Auto Migration API 將為您生成並實現 migrate 函式,編寫並執行遷移所需的 SQL 語句。程式碼如下:
@Database(
version = MusicDatabase.LATEST_VERSION
entities = {Song.class, Artist.class}
autoMigrations = {
@AutoMigration (from = 1,to = 2)
}
exprotSchema = true
)
修改欄位名
現在,演示一個更復雜的場景,假設我們要將 Artist 表中的 singerName 欄位修改為 artistName。
雖然這看起來很簡單,但是由於 SQLite 並沒有提供用於此操作的 API,因此我們需要根據 ALERT TABLE 實現,有如下幾步操作:
- 獲取需要執行更改的表
- 建立一個新表,滿足更改後的表結構
- 將舊錶的資料插入到新表中
- 刪除舊錶
- 把新表重新命名為原表名稱
- 進行外來鍵檢查
遷移程式碼如下:
val MIGRATION_1_2: Migration = Mirgation(1, 2) {
fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_Artist`(`id` INTEGER NOT
NULL, artistName` TEXT, PRIMARY KEY(`id`)"
)
db.execSQL("INSERT INTO `_new_Artist` (id,artistName)
SELECT id, singerName FROM `Artist`"
)
db.execSQL("DROP TABLE `Artist`")
db.execSQL("ALTER TABLE `_new_Artist` RENAME TO `Artist`")
db.execSQL("PRAGMA foreign_key_check(`Artist`)")
}
}
從上面的程式碼就可以看出,如果使用手動遷移,即使兩個版本之間僅有一處更改,也可能需要繁瑣的操作,並且這些操作極易出錯。
那我們來看看自動遷移該如何使用。在上面的示例中,自動遷移無法直接處理重新命名錶中的某一列,因為 Room 在進行自動遷移時,會遍歷兩個版本的資料庫 schema,通過比較來檢測兩者之間的更改。在處理列或者表的重新命名時,Room 無法明確發生了什麼更改,此時可能有兩種情況,是刪除後新新增的?還是進行了重新命名?處理列或者表的刪除操作時也會有同樣問題。
所以我們需要給 Room 新增一些配置來說明這些不確定的場景——定義 AutoMigrationSpec。AutoMigrationSpec 是定義自動遷移規範的介面,我們需要實現該類,並在實現類上新增和修改相對應的註解。本例中,我們使用 @RenameColumn 註解,並在註解引數中,提供表名、列的原始名稱以及更新後的名稱。如果在遷移完成之後,還需要執行其他任務,可以在 AutoMigrationSpec 的 onPostMigrate 函式中進行處理,相關程式碼如下:
@RenameColumn(
tableName = "Artist",
fromColumnName = "singerName",
toColumnName = "artistName"
)
static class MySpec : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// 遷移工作完成後處理任務的回撥
}
}
完成 AutoMigrationSpec 的實現後,還需要將其新增到資料庫定義時配置的 @AutoMigation 中,同時提供兩個版本的資料庫 schema,Auto Migration API 將生成和實現 migrate 函式,配置程式碼如下:
@Database(
version = MusicDatabase.LATEST_VERSION
entities = {Song.class, Artist.class}
autoMigrations = {
@AutoMigration (from = 1,to = 2,spec = MySpec.class)
}
exprotSchema = true
)
上面的案例提到了 @RenameColumn,相關的變更處理註解有如下幾種:
- @DeleteColumn
- @DeleteTable
- @RenameColumn
- @RenameTable
假設在同一遷移中有多個更改需要配置,我們還可以通過這些可複用的註解簡化處理。
測試自動遷移
假設您在一開始就使用了自動遷移,現在希望測試其是否正常工作,可以使用現有的 MigrationTestHelper API 無需任何更改。如以下程式碼:
@Test
fun v1ToV2() {
val helper = MigrationTestHelper(
InstrumentationRegisty.getInstrumentation(),
AutoMigrationDbKotlin::class.java
)
val db: SupportSQLiteDatabase = helper.runMigrationsAndValidate(
name = TEST_DB,
version = 2,
validateDroppedTables = true
)
}
在無需額外配置的情況下,MigrationTestHelper 將自動執行並驗證所有自動遷移。在 Room 內部,如果存在自動遷移,它們將自動新增到需要執行和驗證的遷移列表中。
需要注意的是,開發者提供的遷移具有更高的優先順序,也就是說,如果您定義自動遷移的兩個版本之間,已經定義了手動遷移,那麼手動遷移將優先於自動遷移。
關係查詢方法
關係查詢也是新增的一個重要功能,我們還是用一個示例說明。
假設我們使用與之前相同的資料庫和表,現在表名分別為 Artist 和 Song。如果我們希望獲得音樂人到歌曲的對映集合,就要在 artistName 和 songName 之間建立關係。如下圖中 Purple Lloyd 與其熱門歌曲《Another Tile in the Ceiling》和《The Great Pig in the Sky》匹配,AB/CD 將與其熱門歌曲《Back in White》和《Highway to Heaven》匹配。
使用 @Relation
如果使用 @Relation 和 @Embedded 反應該對映關係,則有如下程式碼:
data class ArtistAndSongs(
@Embedded
val artist: Artist,
@Relation(...)
val songs: List<Song>
)
@Query("SELECT * FROM Artist")
fun getArtistsAndSongs(): List<ArtistAndSongs>
在此方案中,我們建立了全新的 資料類,將音樂人和歌曲列表相關係。但是這種額外建立 data 類的方式,容易造成程式碼繁冗的問題。而 @Relation 中並不支援過濾、排序、分組或組合鍵,其設計初衷也是用於資料庫中只有一些簡單的關係,雖然受限於關係結果,但這是一種快速完成較簡單任務的便捷方法。
所以為了支援複雜關係的處理,我們並沒有擴充套件 @Relation,而是希望您充分發揮 SQL 的潛能,因為它的功能非常強大。
接下來讓我們來看看 Room 如何利用全新的功能來解決這一問題。
使用全新關係查詢功能
為了表示前面所示的音樂人與其歌曲之間的關係,我們現在可以編寫一個簡單的 DAO 方法,其返回型別為 Map,而我們需要做的僅僅是提供 @Query 和返回標記,Room 將為您處理其餘的一切!相關程式碼如下:
@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getAllArtistAndTheirSongsList(): Map<Artist, List<Song>>
在 Room 內部,實際上要做的是找到音樂人、歌曲和 Cursor 並將它們放入 Map 中的 Key 和 Value 中。
在本例中,涉及到一對多的對映關係,其中單個音樂人對映到一個歌曲集合。當然我們也可以使用一對一對映,如下文所示:
// 一對一對映關係
@Query("SELECT * FROM Song JOIN Artist ON Song.songArtistName = Artist.artistName")
fun getSongAndArtist(): Map<Song, Artist>
使用 @MapInfo
實際上,您可以通過 @MapInfo 在對映的使用中更加靈活。
MapInfo 是用於說明開發者配置的輔助程式 API,類似於前面談到的自動遷移更改註解。您可以使用 MapInfo 明確說明您希望如何處理查詢到的 Cursor 所包含的資訊。使用 MapInfo 註解您可以指定輸出的資料結構中用於查詢的 Key 和 Value 所對映的列。需要注意,用於 Key 的型別必須實現 equals 和 hashCode 函式因為這對對映過程非常重要。
假設我們希望以 artistName 作為 Key,獲得歌曲列表作為 Value,則程式碼實現如下:
@MapInfo(keyColumn = "artistName")
@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getArtistNameToSongs(): Map<String, List<Song>>
在該示例中,artistName 用作 Key,音樂人被對映到其歌曲名稱列表,最後 artistName 被對映到其歌曲名稱列表。
MapInfo 註解使您可以靈活地使用特定列,而不是整個 data 類從而進行更加自定義的對映。
其他優勢
關係查詢方法的另一個好處是支援更多的資料操作,可以通過這個新功能來支援分組、篩選等功能。示例程式碼如下:
@MapInfo(valueColumn = "songCount")
@Query("
SELECT *, COUNT(songId) as songCount FROM Artist JOIN Song ON
Artist.artistName = Song.songArtistName
GROUP BY artistName WHERE songCount = 2
")
fun getArtistAndSongCountMap(): Map<Artist, Integer>
最後需要注意多重對映是一個核心返回型別,可以使用 Room 已經支援的各種可觀察型別封裝 (包括 LiveData、Flowable、Flow)。因此,關係查詢方法可讓您輕鬆地在資料庫中定義任意數量的關聯關係。
更多新功能
內建 Enum 型別轉換器
現在,如果系統未提供任何型別轉換器,Room 將預設使用 "列舉 - 字串" 雙向型別轉換器。如果已存在適用於列舉的型別轉換器,Room 將優先使用該轉換器,而不使用預設轉換器。
支援查詢回撥
現在,Room 提供了一個通用 callback API RoomDatabase.QueryCallback,此 API 會在執行查詢時被呼叫,這將非常有助於我們在 Debug 模式下記錄日誌。可通過 RoomDatabase.Builder#setQueryCallback() 設定此回撥。
如果您希望記錄查詢以瞭解資料庫中發生了什麼,該功能可以幫助您進行記錄,示例程式碼如下:
fun setUp() {
database = databaseBuilder.setQueryCallback(
RoomDatabase.QueryCallback{ sqlQuery, bindArgs ->
// 記錄所有觸發的查詢
Log.d(TAG, "SQL Query $sqlQuery")
},
myBackgroundExecutor
).build()
}
支援原生 Paging 3.0 API
Room 現在支援為返回值型別為 androidx.paging.PagingSource 且帶 @Query 註解的方法生成實現。
支援 RxJava3
Room 現在支援 RxJava3 型別。通過依賴 androidx.room:room-rxjava3,您可以宣告返回值型別為 Flowable、Single、Maybe 和 Completable 的 DAO 方法。
支援 Kotlin Symbol Processing (KSP)
KSP 用於替代 KAPT,它能夠在 Kotlin 編譯器上以原生方式執行註解處理器,從而顯著縮短構建時間。
對於 Room,使用 KSP 有如下好處:
- 提高 2 倍的構建速度;
- 直接處理 Kotlin 程式碼,更好的支援空安全。
隨著 KSP 的穩定,Room 將使用其功能實現 value 類、生成 Kotlin 程式碼等。
從 KAPT 遷移到 KSP 非常簡單,只需使用 KSP 外掛替換 KAPT 外掛,並使用 KSP 配置 Room 註解處理器,示例程式碼如下:
plugins{
// 使用 KSP 外掛替換 KATP 外掛
// id("kotlin-kapt")
id("com.google.devtools.ksp")
}
dependencies{
// 使用 KSP 配置替代 KAPT
// kapt "androidx.room:room-compiler:$version"
ksp "androidx.room:room-compiler:$version"
}
總結
自動化遷移、關係查詢方法、KSP——Room 帶來了很多新功能,希望大家和我們一樣對所有這些 Room 更新感到興奮,記得檢視並開始在您的應用中使用這些新功能!
歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!