深入探討 Room 2.4.0 的最新進展

Android開發者發表於2022-01-25

在 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 實現,有如下幾步操作:

  1. 獲取需要執行更改的表
  2. 建立一個新表,滿足更改後的表結構
  3. 將舊錶的資料插入到新表中
  4. 刪除舊錶
  5. 把新表重新命名為原表名稱
  6. 進行外來鍵檢查

遷移程式碼如下:

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 更新感到興奮,記得檢視並開始在您的應用中使用這些新功能!

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章