8-Room持久性庫

Room持久性庫

概覽

Room 在 SQLite 上提供了一個抽象層,以便在充分利用 SQLite 的強大功能的同時,能夠流暢地訪問數據庫。

Room 包含 3 個主要組件:

  • 數據庫:包含數據庫持有者,並作爲應用已保留的持久關係型數據的底層連接的主要接入點。

    使用 @Database 註釋的類應滿足以下條件:

    • 是擴展 RoomDatabase 的抽象類。
    • 在註釋中添加與數據庫關聯的實體列表。
    • 包含具有 0 個參數且返回使用 @Dao 註釋的類的抽象方法。

    在運行時,您可以通過調用 [Room.databaseBuilder()](https://developer.android.com/reference/androidx/room/Room#databaseBuilder(android.content.Context, java.lang.Class, java.lang.String)) 或 [Room.inMemoryDatabaseBuilder()](https://developer.android.com/reference/androidx/room/Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)) 獲取 Database 的實例。

  • Entity:表示數據庫中的表。

  • DAO:包含用於訪問數據庫的方法。

Room架構圖如下所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FdAI8CbD-1592585026074)(/Users/liubo/Desktop/筆記/Jetpack/room_architecture.png)]

示例

以下代碼段包含具有一個實體和一個 DAO 的示例數據庫配置。

User

    @Entity
    data class User(
        @PrimaryKey val uid: Int,
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )

UserDao

    @Dao
    interface UserDao {
        @Query("SELECT * FROM user")
        fun getAll(): List<User>

        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        fun loadAllByIds(userIds: IntArray): List<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
               "last_name LIKE :last LIMIT 1")
        fun findByName(first: String, last: String): User

        @Insert
        fun insertAll(vararg users: User)

        @Delete
        fun delete(user: User)
    }

AppDatabase

    @Database(entities = arrayOf(User::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

獲取數據庫實例:

    val db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java, "database-name"
            ).build()

注意:如果您的應用在單個進程中運行,則在實例化 AppDatabase 對象時應遵循單例設計模式。每個 RoomDatabase 實例的成本相當高,而您幾乎不需要在單個進程中訪問多個實例。

如果您的應用在多個進程中運行,請在數據庫構建器調用中包含 enableMultiInstanceInvalidation()。這樣,如果您在每個進程中都有一個 AppDatabase 實例,就可以在一個進程中使共享數據庫文件失效,並且這種失效會自動傳播到其他進程中的 AppDatabase 實例。

聲明依賴項

    dependencies {
      def room_version = "2.2.3"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

      // optional - Kotlin Extensions and Coroutines support for Room
      implementation "androidx.room:room-ktx:$room_version"

      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"

      // optional - Guava support for Room, including Optional and ListenableFuture
      implementation "androidx.room:room-guava:$room_version"

      // Test helpers
      testImplementation "androidx.room:room-testing:$room_version"
    }

配置編譯器選項

    dependencies {
      def room_version = "2.2.3"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

      // optional - Kotlin Extensions and Coroutines support for Room
      implementation "androidx.room:room-ktx:$room_version"

      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"

      // optional - Guava support for Room, including Optional and ListenableFuture
      implementation "androidx.room:room-guava:$room_version"

      // Test helpers
      testImplementation "androidx.room:room-testing:$room_version"
    }
    

以下代碼段舉例說明了如何配置這些選項:

    android {
        ...
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [
                        "room.schemaLocation":"$projectDir/schemas".toString(),
                        "room.incremental":"true",
                        "room.expandProjection":"true"]
                }
            }
        }
    }
    

使用實體定義數據

以下代碼段展示瞭如何定義實體:

    @Entity
    data class User(
        @PrimaryKey var id: Int,
        var firstName: String?,
        var lastName: String?
    )

要保留某個字段,Room 必須擁有該字段的訪問權限。您可以將某個字段設爲公開字段,也可以爲其提供 getter 和 setter。如果您使用 getter 和 setter 方法,則請注意,這些方法需遵循 Room 中的 JavaBeans 規範。

注意:實體可以具有空的構造函數(如果相應的 DAO 類可以訪問保留的每個字段),也可以具有其參數包含的類型和名稱與該實體中字段的類型和名稱一致的構造函數。Room 還可以使用完整或部分構造函數,例如僅接收部分字段的構造函數。

使用主鍵

每個實體必須將至少 1 個字段定義爲主鍵。即使只有 1 個字段,您仍然需要爲該字段添加 @PrimaryKey 註釋。此外,如果您想讓 Room 爲實體分配自動 ID,則可以設置 @PrimaryKeyautoGenerate 屬性。如果實體具有複合主鍵,您可以使用 @Entity 註釋的 primaryKeys 屬性,如

以下代碼段所示:

    @Entity(primaryKeys = arrayOf("firstName", "lastName"))
    data class User(
        val firstName: String?,
        val lastName: String?
    )

默認情況下,Room 將類名稱用作數據庫表名稱。如果您希望表具有不同的名稱,請設置 @Entity 註釋的 tableName 屬性,如以下代碼段所示:

    @Entity(tableName = "users")
    data class User (
        // ...
    )

注意:SQLite 中的表名稱不區分大小寫。

tableName 屬性類似,Room 將字段名稱用作數據庫中的列名稱。如果您希望列具有不同的名稱,請將 @ColumnInfo 註釋添加到字段,如以下代碼段所示:

    @Entity(tableName = "users")
    data class User (
        @PrimaryKey val id: Int,
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )

忽略字段

默認情況下,Room 會爲在實體中定義的每個字段創建一個列。如果某個實體中有您不想保留的字段,則可以使用 @Ignore 爲這些字段註釋,如以下代碼段所示:

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        val lastName: String?,
        @Ignore val picture: Bitmap?
    )

如果實體繼承了父實體的字段,則使用 @Entity 屬性的 ignoredColumns 屬性通常會更容易:

    open class User {
        var picture: Bitmap? = null
    }

    @Entity(ignoredColumns = arrayOf("picture"))
    data class RemoteUser(
        @PrimaryKey val id: Int,
        val hasVpn: Boolean
    ) : User()

提供表搜索支持

Room 支持多種類型的註釋,可讓您更輕鬆地搜索數據庫表中的詳細信息。

除非應用的 minSdkVersion 低於 16,否則請使用全文搜索。

支持全文搜索(FTS:Full-text Search)

如果您的應用需要通過全文搜索 (FTS) 快速訪問數據庫信息,請使用虛擬表(使用 FTS3 或 FTS4 SQLite 擴展模塊)爲您的實體提供支持。要使用 Room 2.1.0 及更高版本中提供的這項功能,請將 @Fts3 或 @Fts4 註釋添加到給定實體,如以下代碼段所示:

    // Use `@Fts3` only if your app has strict disk space requirements or if you
    // require compatibility with an older SQLite version.
    @Fts4
    @Entity(tableName = "users")
    data class User(
        /* Specifying a primary key for an FTS-table-backed entity is optional, but
           if you include one, it must use this type and column name. */
        @PrimaryKey @ColumnInfo(name = "rowid") val id: Int,
        @ColumnInfo(name = "first_name") val firstName: String?
    )

注意:啓用 FTS 的表始終使用 INTEGER 類型的主鍵且列名稱爲“rowid”。如果是由 FTS 表支持的實體定義主鍵,則必須使用相應的類型和列名稱。

如果表支持以多種語言顯示的內容,請使用 languageId 選項指定用於存儲每一行語言信息的列:

    @Fts4(languageId = "lid")
    @Entity(tableName = "users")
    data class User(
        // ...
        @ColumnInfo(name = "lid") val languageId: Int
    )

Room 提供了其他幾個選項來定義由 FTS 支持的實體,包括結果排序、令牌生成器類型以及作爲外部內容管理的表。如需詳細瞭解這些選項,請參閱 FtsOptions 參考。

將特定列編入索引

如果您的應用必須支持不允許使用由 FTS3 或 FTS4 表支持的實體的 SDK 版本,您仍可以將數據庫中的某些列編入索引,以加快查詢速度。要爲實體添加索引,請在 @Entity 註釋中添加 indices 屬性,以列出要在索引或複合索引中包含的列的名稱。

以下代碼段演示了此註釋過程:

    @Entity(indices = arrayOf(Index(value = ["last_name", "address"])))
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        val address: String?,
        @ColumnInfo(name = "last_name") val lastName: String?,
        @Ignore val picture: Bitmap?
    )

有時,數據庫中的某些字段或字段組必須是唯一的。您可以通過將 @Index 註釋的 unique 屬性設爲 true 來強制實施此唯一性屬性。

以下代碼示例可防止表格具有包含 firstNamelastName 列的同一組值的兩行:

    @Entity(indices = arrayOf(Index(value = ["first_name", "last_name"],
            unique = true)))
    data class User(
        @PrimaryKey val id: Int,
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?,
        @Ignore var picture: Bitmap?
    )

添加基於AutoValue的對象

此功能旨在用於基於 Java 的實體。要在基於 Kotlin 的實體中實現相同的功能,最好改用數據類

在 Room 2.1.0 及更高版本中,您可以將基於 Java 的不可變值類(使用 @AutoValue 爲其註釋)用作應用的數據庫中的實體。此支持在實體的兩個實例被視爲相等(如果這兩個實例的列包含相同的值)時尤爲有用。

將帶有 @AutoValue 註釋的類用作實體時,您可以使用 @PrimaryKey@ColumnInfo@Embedded@Relation 爲類的抽象方法註釋。不過,您必須在每次使用這些註釋時添加 @CopyAnnotations 註釋,以便 Room 可以正確解釋這些方法的自動生成實現。

以下代碼段展示了一個使用 @AutoValue 註釋的類(Room 將其標識爲實體)的示例:

User.java

    @AutoValue
    @Entity
    public abstract class User {
        // Supported annotations must include `@CopyAnnotations`.
        @CopyAnnotations
        @PrimaryKey
        public abstract long getId();

        public abstract String getFirstName();
        public abstract String getLastName();

        // Room uses this factory method to create User objects.
        public static User create(long id, String firstName, String lastName) {
            return new AutoValue_User(id, firstName, lastName);
        }
    }

定義對象之間的關係

由於 SQLite 是關係型數據庫,因此您可以指定各個對象之間的關係。儘管大多數對象關係映射庫都允許實體對象互相引用,但 Room 明確禁止這樣做

定義一對多關係

即使您不能使用直接關係,Room 仍允許您定義實體之間的外鍵約束。

例如,如果存在另一個名爲 Book 的實體,您可以使用 @ForeignKey 註釋定義該實體與 User 實體的關係,如以下代碼段所示:

    @Entity(foreignKeys = arrayOf(ForeignKey(
                entity = User::class,
                parentColumns = arrayOf("id"),
                childColumns = arrayOf("user_id"))
           )
    )
    data class Book(
        @PrimaryKey val bookId: Int,
        val title: String?,
        @ColumnInfo(name = "user_id") val userId: Int
    )

由於零個或更多個 Book 實例可以通過 user_id 外鍵關聯到一個 User 實例,因此這會在 UserBook 之間構建一對多關係模型。

外鍵非常強大,可讓您指定引用的實體更新後會發生什麼。例如,您可以通過在 @ForeignKey 註釋中添加 onDelete = CASCADE,在 User 的對應實例刪除後告知 SQLite 刪除該用戶的所有圖書。

注意:SQLite 將 @Insert(onConflict = REPLACE) 作爲一組 REMOVEREPLACE 操作(而不是單個 UPDATE 操作)處理。這種替換衝突值的方法可能會影響您的外鍵約束。如需瞭解詳情,請參閱有關 ON_CONFLICT 子句的 SQLite 文檔

創建嵌套對象

有時,您可能希望在數據庫邏輯中將某個實體或數據對象表示爲一個緊密的整體,即使該對象包含多個字段也是如此。在這些情況下,您可以使用 @Embedded 註釋表示要解構到表中其子字段的對象。然後,您可以像查詢其他各個列一樣查詢嵌套字段。

例如,您的 User 類可以包含類型 Address 的字段,該類型表示一組分別名爲 streetcitystatepostCode 的字段。要在表中單獨存儲組成的列,請在 User 類(使用 @Embedded 註釋)中添加 Address 字段,如以下代碼段所示:

    data class Address(
        val street: String?,
        val state: String?,
        val city: String?,
        @ColumnInfo(name = "post_code") val postCode: Int
    )

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        @Embedded val address: Address?
    )

然後,表示 User 對象的表會包含具有以下名稱的列:idfirstNamestreetstatecitypost_code

注意:嵌套字段還可以包含其他嵌套字段。

如果某個實體具有同一類型的多個嵌套字段,您可以通過設置 prefix 屬性確保每個列都獨一無二。然後,Room 會將提供的值添加到嵌套對象中每個列名稱的開頭。

	 @Embedded(prefix = "loc_")
   Coordinates coordinates;

定義多對多關係

您通常希望在關係型數據庫中構建的另一種關係模型是兩個實體之間的多對多關係,其中每個實體都可以關聯到另一個實體的零個或更多個實例。

例如,假設有一個音樂在線播放應用,用戶可以在該應用中將自己喜愛的歌曲整理到播放列表中。每個播放列表都可以包含任意數量的歌曲,每首歌曲都可以包含在任意數量的播放列表中。

要構建這種關係的模型,您需要創建下面三個對象:

  1. 播放列表的實體類。
  2. 歌曲的實體類。
  3. 用於保存每個播放列表中的歌曲相關信息的中間類。

您可以將實體類定義爲獨立單元:

    @Entity
    data class Playlist(
        @PrimaryKey var id: Int,
        val name: String?,
        val description: String?
    )

    @Entity
    data class Song(
        @PrimaryKey var id: Int,
        val songName: String?,
        val artistName: String?
    )

然後,將中間類定義爲包含對 SongPlaylist 的外鍵引用的實體:

    @Entity(tableName = "playlist_song_join",
            primaryKeys = arrayOf("playlistId","songId"),
            foreignKeys = arrayOf(
                             ForeignKey(entity = Playlist::class,
                                        parentColumns = arrayOf("id"),
                                        childColumns = arrayOf("playlistId")),
                             ForeignKey(entity = Song::class,
                                        parentColumns = arrayOf("id"),
                                        childColumns = arrayOf("songId"))
                                  )
            )
    data class PlaylistSongJoin(
        val playlistId: Int,
        val songId: Int
    )

這會生成一個多對多關係模型。藉助該模型,您可以使用 DAO 按歌曲查詢播放列表和按播放列表查詢歌曲:

    @Dao
    interface PlaylistSongJoinDao {
        @Insert
        fun insert(playlistSongJoin: PlaylistSongJoin)

        @Query("""
               SELECT * FROM playlist
               INNER JOIN playlist_song_join
               ON playlist.id=playlist_song_join.playlistId
               WHERE playlist_song_join.songId=:songId
               """)
        fun getPlaylistsForSong(songId: Int): Array<Playlist>

        @Query("""
               SELECT * FROM song
               INNER JOIN playlist_song_join
               ON song.id=playlist_song_join.songId
               WHERE playlist_song_join.playlistId=:playlistId
               """)
        fun getSongsForPlaylist(playlistId: Int): Array<Song>
    }

在數據庫中創建視圖

2.1.0 及更高版本的 Room 持久性庫SQLite 數據庫視圖提供了支持,從而允許您將查詢封裝到類中。Room 將這些查詢支持的類稱爲視圖,在 DAO 中使用時,它們的行爲與簡單數據對象的行爲相同。

注意:與實體類似,您可以針對視圖運行 SELECT 語句。不過,您無法針對視圖運行 INSERTUPDATEDELETE 語句。

創建視圖

要創建視圖,請將 @DatabaseView 註釋添加到類中。將註釋的值設爲類應該表示的查詢。

    @DatabaseView("SELECT user.id, user.name, user.departmentId," +
            "department.name AS departmentName FROM user " +
            "INNER JOIN department ON user.departmentId = department.id")
    data class UserDetail(
        val id: Long,
        val name: String?,
        val departmentId: Long,
        val departmentName: String?
    )

將視圖與數據庫相關聯

要將此視圖添加爲應用數據庫的一部分,請在應用的 @Database 註釋中添加 views 屬性:

    @Database(entities = arrayOf(User::class),
              views = arrayOf(UserDetail::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

使用DAO訪問數據

要使用 Room 持久性庫訪問應用的數據,您需要使用數據訪問對象 (DAO)。這些 Dao 對象構成了 Room 的主要組件,因爲每個 DAO 都包含一些方法,這些方法提供對應用數據庫的抽象訪問權限。

DAO 既可以是接口,也可以是抽象類。如果是抽象類,則該 DAO 可以選擇有一個以 RoomDatabase 爲唯一參數的構造函數。Room 會在編譯時創建每個 DAO 實現。

注意:除非您對構建器調用 allowMainThreadQueries(),否則 Room 不支持在主線程上訪問數據庫,因爲它可能會長時間鎖定界面。異步查詢(返回 LiveDataFlowable 實例的查詢)無需遵守此規則,因爲此類查詢會根據需要在後臺線程上異步運行查詢。

自定義方法

Insert

當您創建 DAO 方法並使用 @Insert 對其進行註釋時,Room 會生成一個實現,該實現在單個事務中將所有參數插入到數據庫中。

以下代碼段展示了幾個示例查詢:

    @Dao
    interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertUsers(vararg users: User)

        @Insert
        fun insertBothUsers(user1: User, user2: User)

        @Insert
        fun insertUsersAndFriends(user: User, friends: List<User>)
    }

如果 @Insert 方法只接收 1 個參數,則可返回 long,這是插入項的新 rowId。如果參數是數組或集合,則應返回 long[]List

如需瞭解詳情,請參閱 @Insert 註釋的參考文檔以及 rowid 表格的 SQLite 文檔

Update

Update 便捷方法會修改數據庫中以參數形式給出的一組實體。它使用與每個實體的主鍵匹配的查詢。

以下代碼段演示瞭如何定義此方法:

    @Dao
    interface MyDao {
        @Update
        fun updateUsers(vararg users: User)
    }

雖然通常沒有必要,但您可以讓此方法返回一個 int 值,表示數據庫中更新的行數

Delete

Delete 便捷方法會從數據庫中刪除一組以參數形式給出的實體。它使用主鍵查找要刪除的實體。

以下代碼段演示瞭如何定義此方法:

    @Dao
    interface MyDao {
        @Delete
        fun deleteUsers(vararg users: User)
    }

雖然通常沒有必要,但您可以讓此方法返回一個 int 值,表示從數據庫中刪除的行數。

查詢信息

@Query 是 DAO 類中使用的主要註釋。它允許您對數據庫執行讀/寫操作。每個 @Query 方法都會在編譯時進行驗證,因此如果查詢出現問題,則會發生編譯錯誤,而不是運行時失敗。

Room 還會驗證查詢的返回值,這樣的話,當返回的對象中的字段名稱與查詢響應中的對應列名稱不匹配時,Room 會通過以下兩種方式之一提醒您:

  • 如果只有部分字段名稱匹配,則會發出警告。

  • 如果沒有任何字段名稱匹配,則會發出錯誤。

簡單查詢

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user")
        fun loadAllUsers(): Array<User>
    }

這是一個極其簡單的查詢,可加載所有用戶。在編譯時,Room 知道它在查詢用戶表中的所有列。如果查詢包含語法錯誤,或者數據庫中沒有用戶表格,則 Room 會在您的應用編譯時顯示包含相應消息的錯誤。

將參數傳遞給查詢

在大多數情況下,您需要將參數傳遞給查詢以執行過濾操作,例如僅顯示某個年齡以上的用戶。要完成此任務,請在 Room 註釋中使用方法參數,如以下代碼段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        fun loadAllUsersOlderThan(minAge: Int): Array<User>
    }

在編譯時處理此查詢時,Room 會將 :minAge 綁定參數與 minAge 方法參數相匹配。Room 通過參數名稱進行匹配。如果有不匹配的情況,則應用編譯時會出現錯誤。

您還可以在查詢中傳遞多個參數或多次引用這些參數,如以下代碼段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        fun findUserWithName(search: String): List<User>
    }

返回列的子集

大多數情況下,您只需獲取實體的幾個字段。例如,您的界面可能僅顯示用戶的名字和姓氏,而不是用戶的每一條詳細信息。通過僅提取應用界面中顯示的列,您可以節省寶貴的資源,並且您的查詢也能更快完成。

藉助 Room,您可以從查詢中返回任何基於 Java 的對象,前提是結果列集合會映射到返回的對象。例如,您可以創建以下基於 Java 的普通對象 (POJO) 來獲取用戶的名字和姓氏:

    data class NameTuple(
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )

現在,您可以在查詢方法中使用此 POJO:

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        fun loadFullName(): List<NameTuple>
    }

Room 知道該查詢會返回 first_namelast_name 列的值,並且這些值會映射到 NameTuple 類的字段。因此,Room 可以生成正確的代碼。如果查詢返回太多的列,或者返回 NameTuple 類中不存在的列,則 Room 會顯示一條警告。

注意:這些 POJO 也可以使用 @Embedded 註釋。

傳遞參數的集合

部分查詢可能要求您傳入數量不定的參數,參數的確切數量要到運行時才知道。例如,您可能希望從部分區域中檢索所有用戶的相關信息。Room 知道參數何時表示集合,並根據提供的參數數量在運行時自動將其展開。

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
    }

可觀察查詢

執行查詢時,您通常會希望應用的界面在數據發生變化時自動更新。爲此,請在查詢方法說明中使用 LiveData 類型的返回值。當數據庫更新時,Room 會生成更新 LiveData 所必需的所有代碼。

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
    }

注意:自版本 1.0 起,Room 會根據在查詢中訪問的表格列表決定是否更新 LiveData 實例。

使用RxJava進行響應式查詢

Room 爲 RxJava2 類型的返回值提供了以下支持:

要使用此功能,請在應用的 build.gradle 文件中添加最新版本的 rxjava2 工件:

app/build.gradle

  dependencies {    
    def room_version = "2.1.0"    
    implementation 'androidx.room:room-rxjava2:$room_version'  
  }

以下代碼段展示了幾個如何使用這些返回類型的示例:

    @Dao
    interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        fun loadUserById(id: Int): Flowable<User>

        // Emits the number of users added to the database.
        @Insert
        fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

        // Makes sure that the operation finishes successfully.
        @Insert
        fun insertLargeNumberOfUsers(varargs users: User): Completable

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        fun deleteAllUsers(users: List<User>): Single<Int>
    }

參閱 Google Developers Room 和 RxJava 一文

直接光標訪問

如果應用的邏輯需要直接訪問返回行,您可以從查詢返回 Cursor 對象,如以下代碼段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        fun loadRawUsersOlderThan(minAge: Int): Cursor
    }    

注意:強烈建議您不要使用 Cursor API,因爲它無法保證行是否存在或者行包含哪些值。只有當您已具有需要光標且無法輕鬆重構的代碼時,才使用此功能。

查詢多個表格

部分查詢可能需要訪問多個表格才能計算出結果。藉助 Room,您可以編寫任何查詢,因此您也可以聯接表格。此外,如果響應是可觀察數據類型(如 FlowableLiveData),Room 會觀察查詢中引用的所有表格,以確定是否存在無效表格。

以下代碼段展示瞭如何執行表格聯接來整合兩個表格的信息:一個表格包含當前借閱圖書的用戶,另一個表格包含當前處於已被借閱狀態的圖書的數據。

    @Dao
    interface MyDao {
        @Query(
            "SELECT * FROM book " +
            "INNER JOIN loan ON loan.book_id = book.id " +
            "INNER JOIN user ON user.id = loan.user_id " +
            "WHERE user.name LIKE :userName"
        )
        fun findBooksBorrowedByNameSync(userName: String): List<Book>
    }

您還可以從這些查詢中返回 POJO。例如,您可以編寫一條加載某位用戶及其寵物名字的查詢,如下所示:

    @Dao
    interface MyDao {
        @Query(
            "SELECT user.name AS userName, pet.name AS petName " +
            "FROM user, pet " +
            "WHERE user.id = pet.user_id"
        )
        fun loadUserAndPetNames(): LiveData<List<UserPet>>

        // You can also define this class in a separate file.
        data class UserPet(val userName: String?, val petName: String?)
    }    

使用Kotlin協程編寫異步方法

suspend Kotlin 關鍵字添加到 DAO 方法,以使用 Kotlin 協程功能使這些方法成爲異步方法。這樣可確保不會在主線程上執行這些方法。

@Dao
    interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        suspend fun insertUsers(vararg users: User)

        @Update
        suspend fun updateUsers(vararg users: User)

        @Delete
        suspend fun deleteUsers(vararg users: User)

        @Query("SELECT * FROM user")
        suspend fun loadAllUsers(): Array<User>
    }

注意:要將 Room 與 Kotlin 協程一起使用,您需要使用 Room 2.1.0、Kotlin 1.3.0 和 Cordoines 1.0.0 或更高版本。如需瞭解詳情,請參閱聲明依賴項

本指南也適用於帶有 @Transaction 註釋的 DAO 方法。您可以使用此功能通過其他 DAO 方法構建暫停數據庫方法。然後,這些方法會在單個數據庫事務中運行。

@Dao
    abstract class UsersDao {
        @Transaction
        open suspend fun setLoggedInUser(loggedInUser: User) {
            deleteUser(loggedInUser)
            insertUser(loggedInUser)
        }

        @Query("DELETE FROM users")
        abstract fun deleteUser(user: User)

        @Insert
        abstract suspend fun insertUser(user: User)
    }

注意:應避免在單個數據庫事務中執行額外的應用端工作,因爲 Room 會將此類事務視爲獨佔事務,並且按順序每次僅執行一個事務。也就是說,包含不必要操作的事務很容易鎖定您的數據庫並影響性能。

預填充數據庫

遷移數據庫

測試和調試數據庫

引用複雜數據

使用類型轉換器

瞭解Room爲何不允許對象引用

要點:Room 不允許實體類之間進行對象引用。因此,您必須明確請求您的應用所需的數據。

映射從數據庫到相應對象模型之間的關係是一種常見做法,極其適用於服務器端。即使程序在訪問字段時加載字段,服務器仍然可以正常工作。

但在客戶端,這種延遲加載是不可行的,因爲它通常發生在界面線程上,並且在界面線程上查詢磁盤上的信息會導致嚴重的性能問題。界面線程通常需要大約 16 毫秒來計算和繪製 Activity 的更新後的佈局,因此,即使查詢只用了 5 毫秒,您的應用仍然可能會用盡剩餘的時間來繪製框架,從而導致明顯的顯示故障。如果有一個並行運行的單獨事務,或者設備正在運行其他磁盤密集型任務,則查詢可能需要更多時間才能完成。不過,如果您不使用延遲加載,則應用會抓取一些不必要的數據,從而導致內存消耗問題。

對象關係型映射通常將決定權留給開發者,以便他們可以針對自己的應用用例執行最合適的操作。開發者通常會決定在應用和界面之間共享模型。不過,這種解決方案並不能很好地擴展,因爲界面會不斷髮生變化,共享模型會出現開發者難以預測和調試的問題。

例如,假設界面加載了 Book 對象的列表,其中每本圖書都有一個 Author 對象。您最初可能設計讓查詢使用延遲加載,從而讓 Book 實例檢索作者。對 author 字段的第一次檢索會查詢數據庫。一段時間後,您發現還需要在應用的界面中顯示作者姓名。您可以輕鬆訪問此名稱,如以下代碼段所示:

authorNameTextView.text = book.author.name

不過,這種看似無害的更改會導致在主線程上查詢 Author 表。

如果您事先查詢作者信息,則在您不再需要這些數據時,就會很難更改數據加載方式。例如,如果應用的界面不再需要顯示 Author 信息,則應用會有效地加載不再顯示的數據,從而浪費寶貴的內存空間。如果 Author 類引用其他表(例如 Books),則應用的效率會進一步下降。

要使用 Room 同時引用多個實體,請改爲創建包含每個實體的 POJO,然後編寫用於聯接相應表的查詢。這種結構合理的模型結合 Room 強大的查詢驗證功能,可讓您的應用在加載數據時消耗較少的資源,從而改善應用的性能和用戶體驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章