使用Room持久庫儲存資料

weixin_33866037發表於2017-12-14

Room永續性庫在SQLite的基礎上提供了一個抽象層,允許流利的資料庫訪問,同時利用的SQLite的全部力量。
該庫可幫助在執行應用程式的裝置上建立應用程式資料快取。這個快取作為您的應用程式的單一來源,允許使用者在應用程式中檢視關鍵資訊的一致副本,而不管使用者是否具有網際網路連線。
要將Room匯入Android專案,請參閱新增元件
有關將Room功能應用於應用程式的資料儲存永續性解決方案的指南請參閱 Room培訓指南

理解Room的資料遷移
示例應用程式

一.簡介

處理巨大數量的結構化資料的應用程式可以從本地儲存資料中獲益。最常見的用例是快取相關的資料。這樣,當裝置無法訪問網路時,使用者仍然可以在離線狀態下瀏覽該內容。裝置重新聯機後,任何使用者啟動的內容更改都會同步到伺服器。

由於Room會為您處理這些問題,所以強烈建議您使用Room而不是SQLite。但是,如果您更喜歡使用SQLite API來處理應用程式的資料庫,Android仍然支援使用SQLite直接訪問資料庫。
Room有三個主要部分:
Database資料庫:包含資料庫持有者,並作為與應用持久關係資料的底層連線的主要接入點。@Database註解的類應該滿足以下條件:

Entity實體:表示資料庫中的一個表。
DAO::包含用於訪問資料庫的方法。

這些元件以及與應用程式其餘部分的關係如圖1所示:

1159344-0e52e86e34369a56.png
該應用程式使用Room資料庫來獲取與該資料庫相關聯的資料訪問物件或DAO。 然後,應用程式使用每個DAO從資料庫中獲取實體,並將對這些實體的任何更改儲存回資料庫。 最後,應用程式使用一個實體來獲取和設定與資料庫中的表列對應的值。

圖1.Room架構圖

以下程式碼片段包含具有1個Entity實體和1個DAO的示例資料庫配置:

User.java

@Entity
public class User {
    @PrimaryKey
    private int uid;

    @ColumnInfo(name = "first_name")
    private String firstName;

    @ColumnInfo(name = "last_name")
    private String lastName;

    // Getters and setters are ignored for brevity,
    // but they're required for Room to work.
}

UserDao.java

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

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

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

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

建立上面的檔案後,使用以下程式碼獲取建立的資料庫例項:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

注意:在例項化AppDatabase物件時應該遵循單例設計模式,因為每個 RoomDatabase 例項都相當昂貴,而且很少需要訪問多個例項。

二、具體使用

1.使用Room entities定義資料

使用 Room永續性庫時,可以將相關欄位集合定義為實體。對於每個實體,在關聯的Database物件內建立一個表來儲存這些專案。
預設情況下,Room會為實體中定義的每個欄位建立一個列。如果實體具有不想保留的欄位,可以使用@Ignore註釋。您必須通過entities陣列在Database類中的引用實體類。
以下程式碼片段顯示瞭如何定義一個實體:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

去訪問一個欄位,Room必須有許可權訪問它。你可以公開一個欄位,或者你可以為它提供一個getter和setter。如果您使用getter和setter方法,請記住它們基於Room中的JavaBeans約定。

注意:實體可以有一個空的建構函式(如果相應的DAO類可以訪問每個持久化欄位)或者一個建構函式的引數包含與實體中的欄位型別和名稱相匹配的型別和名稱。房間也可以使用全部或部分建構函式,例如只接收一些欄位的建構函式。

1.1使用主鍵

每個實體必須至少定義一個欄位作為主鍵。<即使只有1個欄位,仍然需要使用註釋對欄位進行 @PrimaryKey 註釋。另外,如果你想室自動分配ID的實體,您可以設定@PrimaryKeyautoGenerate 屬性。如果實體具有複合主鍵,則可以使用@Entity註釋的 primaryKeys 屬性,如以下程式碼片段所示:

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

預設情況下,Room使用類名作為資料庫表名。如果您希望表具有不同的名稱,請設定@Entity註釋的 tableName屬性,如下面的程式碼片段所示:

@Entity(tableName = "users")
class User {
    ...
}

警告:SQLite中的表名是不區分大小寫的。

與該tableName 屬性類似,Room使用欄位名稱作為資料庫中的列名稱。如果您希望列的名稱不同,請將@ColumnInfo註釋新增到欄位中,如以下程式碼片段所示:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

1.2註釋索引和唯一性

根據您訪問資料的方式,您可能需要對資料庫中的某些欄位進行索引以加快查詢速度。要將索引新增到實體,請在@Entity註釋中包含indices屬性,列出要包含在索引或組合索引中的列的名稱。以下程式碼片段演示了這個註釋過程:

@Entity(indices = {@Index("name"),
        @Index(value = {"last_name", "address"})})
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String address;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

有時,資料庫中的某些欄位或欄位組必須是唯一的。您可以通過強行設定@Index註釋的unique 屬性為true來確保唯一性。以下程式碼示例可防止表中有兩列包含與firstName lastName列相同的一組值

@Entity(indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

1.3定義物件之間的關係

因為SQLite是一個關聯式資料庫,所以你可以指定物件之間的關係。儘管大多數物件關係對映庫允許實體物件相互引用,但Room明確禁止這樣做。要了解此決定背後的技術推理,請參閱瞭解Room不允許使用物件引用的原因
即使您不能使用直接關係,Room仍允許您定義實體之間的外來鍵約束。
例如,如果有另一個實體被呼叫,您可以使用@ForeignKey註釋來定義它與實體的關係,如以下程式碼片段所示:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}

外來鍵非常強大,因為它們允許您指定在引用實體更新時做什麼。比如,您可以告訴SQLite刪除使用者所有的書,如果相應的user例項通過在 @ForeignKey註釋中包含 onDelete = CASCADE 進行刪除。

注:SQLite在處理 @Insert(onConflict = REPLACE) 是做一組REMOVEREPLACE操作,而不是一個單一的UPDATE 操作。這種替換衝突值的方法可能會影響您的外來鍵約束。有關更多詳細資訊,請參閱該SQLite文件)的ON_CONFLICT條款。


1.4建立巢狀的物件

有時,即使物件包含多個欄位,您也希望在資料庫邏輯中將實體或普通Java物件(PO​​JO)表示為一個有凝聚力的整體。在這些情況下,可以使用@Embedded 註釋來表示要分解到表中的子欄位的物件。然後,您可以像檢視其他單個列一樣查詢嵌入的欄位。
例如,我們的User類可以包含Address型別的欄位,它代表一系列欄位streetcitystatepostCode的組合。要在表中單獨的儲存組合列,請在User類中包含一個註釋為 @EmbeddedAddress欄位,如以下程式碼片段所示:

class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

該表表示一個包含列名為:id firstName street state citypost_code的列的user物件

注意:嵌入欄位也可以包含其他嵌入欄位。

如果實體具有多個相同型別的嵌入欄位,則可以通過設定prefix 屬性來保持每個列的唯一性。然後,Room會將提供的值新增到嵌入物件中每個列名的開頭。


2.使用Room DAOs訪問資料

要使用Room永續性庫訪問應用程式的資料,您需要使用資料訪問物件或DAOs。這一系列的 Dao物件構成了Room的主要元件,因為每個DAO都包含提供對應用程式資料庫的抽象訪問的方法。通過使用DAO類而不是查詢構建器或直接查詢訪問資料庫,可以分離資料庫體系結構的不同元件。此外,DAO允許您在測試應用程式時輕鬆模擬資料庫訪問

一個DAO可以是一個介面或一個抽象類。如果它是一個抽象類,它可以有一個只有RoomDatabase作為引數的建構函式。Room在編譯時建立每個DAO實現

注意:除非已在構建器上呼叫allowMainThreadQueries(),否則會Room不支援主執行緒上的資料庫訪問,因為它可能會長時間鎖定UI。非同步查詢以及返回 LiveData Flowable例項的查詢或免於此規則,因為它們在需要時非同步地在後臺執行緒上執行查詢。


  • 2.1定義方法以方便使用

您可以使用DAO類來表示多個便利查詢。這個檔案包括幾個常見的例子。

2.1.1.插

當您建立DAO方法並對其進行註釋時 @Insert,Room會生成一個實現,該實現將所有引數插入到單個事務中的資料庫中。
以下程式碼片段顯示了幾個示例查詢:

@Dao
public interface MyDao {
   @Insert(onConflict = OnConflictStrategy.REPLACE)
   public void insertUsers(User... users);

   @Insert
   public void insertBothUsers(User user1, User user2);

   @Insert
   public void insertUsersAndFriends(User user, List<User> friends);
}

如果@Insert 方法只接收到1個引數,它可以返回一個long,這是插入的專案的新rowId。如果引數是一個陣列或一個集合,它應該返回long[]List<Long>代替。有關更多詳細資訊,請參閱@Insert註釋的參考文件以及 SQLite documentation for rowid tables

2.1.2更新

通過給定的引數Update方法方便從資料庫中修改一組實體。它使用與每個實體的主鍵相匹配的查詢。下面的程式碼片段演示瞭如何定義這個方法:

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

雖然通常不是必需的,但是您可以讓此方法返回一個int值,表示在資料庫中更新的行數。

2.1.3刪除

通過給定引數Delete方法很方便從資料庫中刪除一組實體。它使用主鍵來查詢要刪除的實體。下面的程式碼片段演示瞭如何定義這個方法:

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

雖然通常不是必需的,但是您可以讓此方法返回一個int值,表示從資料庫中刪除的行數。


  • 2.2查詢資訊

@Query是DAO類中使用的主要註釋。它允許您在資料庫上執行讀取/寫入操作。每種 @Query方法都在編譯時進行驗證,所以如果查詢出現問題,則會發生編譯錯誤而不是執行時失敗。Room還驗證查詢的返回值,以便如果返回物件中的欄位名稱與查詢響應中相應的列名稱不匹配,Room會以下列兩種方式之一提醒您:

  • 如果只有一些欄位名稱匹配,它會發出警告。
  • 如果沒有欄位名稱匹配,則會出現錯誤。
2.2.1簡單的查詢
@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

這是一個載入所有使用者的非常簡單的查詢。在編譯時,Room知道它正在查詢使用者表中的所有列。<如果查詢包含語法錯誤,或者使用者表不存在於資料庫中,則Room會在您的應用程式編譯時顯示相應訊息的錯誤。

2.2.2將引數傳遞給查詢

大多數時候,您需要將引數傳遞到查詢中以執行過濾操作,例如只顯示位元定年齡更早的使用者。要完成此任務,請使用Room註釋中的方法引數,如以下程式碼片段所示:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

在編譯時處理此查詢時,Room將bind引數與minAge方法引數進行匹配,Room使用引數名稱執行匹配。如果不匹配,則應用程式編譯時發生錯誤。
您還可以傳遞多個引數或在查詢中多次引用它們,如以下程式碼片段所示:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}
2.2.3返回列的子集

大多數情況下,你只需要得到一個實體的幾個欄位。例如,您的使用者介面可能只顯示使用者的名字和姓氏,而不是每個使用者的詳細資訊。通過只提取出現在應用的使用者介面中的列,您可以節省寶貴的資源,並且查詢更快完成。
Room允許您從查詢中返回任何基於Java的物件,只要結果列的集合可以對映到返回的物件中即可。例如,您可以建立以下普通的基於Java的舊物件(PO​​JO)來獲取使用者的名字和姓氏:

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;

    @ColumnInfo(name="last_name")
    public String lastName;
}

現在,你可以在你的查詢方法中使用這個POJO:

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

Room理解查詢返回first_namelast_name列的值,這些值可以對映到類的欄位。因此,Room可以生成正確的程式碼。如果查詢返回太多的列或類中不存在的列,則Room會顯示警告。

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

2.2.4傳遞一組引數

有些查詢可能需要傳遞可變數量的引數,直到執行時才能知道引數的確切數量。例如,您可能希望從一部分地區中檢索有關所有使用者的資訊。當一個參數列示一個集合時,Room會理解,並根據提供的引數數量在執行時自動擴充套件它。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
2.2.5可觀察的查詢

在執行查詢時,您經常希望應用程式的UI在資料更改時自動更新。為了達到這個目的,在你的查詢方法的描述中使用LiveData型別的返回值。 資料庫更新時,Room生成所有必要的程式碼來更新LiveData

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

注意:從版本1.0開始,Room使用查詢中訪問的表的列表來決定是否更新LiveData

2.2.6使用RxJava進行響應式查詢

Room也可以從你定義的查詢中返回RxJava2 PublisherFlowabl物件。要使用此功能,請將Room組中的android.arch.persistence.room:rxjava2工件新增到您的構建Gradle依賴項中。然後,您可以返回RxJava2中定義的型別的物件,如以下程式碼片段所示:

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

有關更多詳細資訊,請參閱Google DevelopersRoom和RxJava文章

2.2.7直接遊標訪問

如果您的應用程式的邏輯需要直接訪問返回行,則可以從查詢中返回Cursor物件,如以下程式碼片段所示:

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

警告:非常不鼓勵使用Cursor API,因為它不能保證行是否存在或者行包含的值。只有當您已經擁有需要遊標的程式碼並且無法輕鬆重構時才使用此功能。

2.2.8查詢多個表

有些查詢可能需要訪問多個表來計算結果。Room允許你寫任何查詢,你也可以加入表。此外,如果返回值是可觀察的資料型別(如FlowableLiveData),Room會檢視查詢中引用的所有表以使失效。
以下程式碼片段顯示如何執行表連線來合併包含借用圖書的使用者的表和包含當前正在借閱的圖書的資料的表之間的資訊:

@Dao
public 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")
   public List<Book> findBooksBorrowedByNameSync(String userName);
}

您也可以從這些查詢中返回POJO。例如,您可以編寫一個查詢來載入使用者及其寵物的名稱,如下所示:

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

   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserPet {
       public String userName;
       public String petName;
   }
}


3. 遷移Room資料庫

在您的應用中新增和更改功能時,您需要修改實體類以反映這些更改。當使用者更新到最新版本的應用程式時,您不希望它們丟失所有的現有資料,特別是當您無法從遠端伺服器恢復資料時。
Room永續性庫允許你編寫 Migration 的類以這種方式儲存使用者資料。每個 Migration 類指定一個startVersionendVersion。在執行時,Room執行每個Migration 類的migrate() 方法,使用正確的順序將資料庫遷移到更高版本。

警告:如果您不提供必要的遷移,則會重建資料庫,這意味著您將丟失資料庫中的所有資料。

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
    }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");
    }
};

警告:要保持遷移邏輯按預期執行,請使用完整查詢,而不是引用代表查詢的常量。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">遷移過程完成後,Room會驗證模式以確保正確遷移。</font><font style="vertical-align: inherit;">如果Room找到問題,則會丟擲包含不匹配資訊的異常。</font></font>

3.1 測試遷移

遷移不是微不足道的,寫入失敗可能會導致您的應用程式崩潰迴圈。為了保持您的應用程式的穩定性,您應該事先測試您的遷移。Room提供了一個testingMaven工件來協助這個測試過程。但是,要使這個工件正常工作,您需要匯出資料庫的模式。

  • 3.1.1匯出模式

編譯前,Room將資料庫的模式資訊匯出到JSON檔案中。要匯出模式,在build.gradle檔案中設定room.schemaLocation註釋處理屬性,如下面的程式碼片段所示:

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

您應該將匯出的JSON檔案(代表資料庫的模式歷史記錄)儲存在版本控制系統中,因為它允許Room為測試目的建立較舊版本的資料庫。
要測試這些遷移,請將Room中的 android.arch.persistence.room:testingMaven工件新增到您的測試依賴項中,並將該模式​​位置作為資原始檔夾新增,如以下程式碼片段所示:
build.gradle

android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

測試包提供了一個MigrationTestHelper 類,它可以讀取這些模式檔案。它也實現了JUnit4 TestRule介面,所以它可以管理建立的資料庫。
示例遷移測試出現在以下程式碼片段中:

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                MigrationDb.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrate1To2() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

        // db has schema version 1. insert some data using SQL queries.
        // You cannot use DAO classes because they expect the latest schema.
        db.execSQL(...);

        // Prepare for the next version.
        db.close();

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
    }
}


4.測試Room資料庫

使用Room永續性庫建立資料庫時,驗證應用資料庫和使用者資料的穩定性非常重要
有兩種方法來測試你的資料庫:

  • 在Android裝置上。
  • 在您的主機開發機器上(不推薦)。

有關特定於資料庫遷移的測試的資訊,請參閱 測試遷移

注意:在為您的應用程式執行測試時,Room允許您建立DAO類的模擬例項。這樣,如果您不測試資料庫本身,則不需要建立完整的資料庫。這個功能是可能的,因為你的DAO不會洩露你的資料庫的任何細節。


4.1在Android裝置上測試

測試資料庫實現的推薦方法是編寫在Android裝置上執行的JUnit測試。因為這些測試不需要建立一個活動,它們應該比你的UI測試更快執行。<設定測試時,應該建立一個記憶體版本的資料庫,以使測試更密切,如下例所示:

@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
    private UserDao mUserDao;
    private TestDatabase mDb;

    @Before
    public void createDb() {
        Context context = InstrumentationRegistry.getTargetContext();
        mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
        mUserDao = mDb.getUserDao();
    }

    @After
    public void closeDb() throws IOException {
        mDb.close();
    }

    @Test
    public void writeUserAndReadInList() throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        mUserDao.insert(user);
        List<User> byName = mUserDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user));
    }
}

4.2 在主機上測試

Room使用SQLite支援庫,它提供了與Android Framework類中的介面相匹配的介面。此支援允許您傳遞支援庫的自定義實現來測試您的資料庫查詢。

注意:即使此設定允許您的測試執行速度非常快,也不建議這樣做,因為裝置上執行的SQLite版本以及您的使用者裝置可能與主機上的版本不匹配。



5.使用Room引用複雜資料

Room提供了原始型別和盒裝型別之間轉換的功能,但不允許實體之間的物件引用。本文件解釋瞭如何使用型別轉換器,以及為什麼Room不支援物件引用。


5.1 使用型別轉換器

有時,您的應用程式需要使用您想要儲存在單個資料庫列中的自定義資料型別。要為自定義型別新增這種支援,您需要提供一個 TypeConverter將自定義類轉換為Room可以保留的已知型別的類。
例如,如果我們想要儲存例項Date,我們可以編寫如下 TypeConverter 在資料庫中儲存等價的Unix時間戳:

public class Converters {
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }

    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}

前面的例子中定義了兩個函式,一個轉換Date物件到Long物件和另一個執行逆變換,從LongDate。由於Room已經知道如何保持Long物件,所以它可以使用這個轉換器來儲存型別的值Date

接下來,為了Room可以使用您在AppDatabaseentity
DAO中定義的轉換器 ,您需要新增 @TypeConverters 註釋到AppDatabase類:
AppDatabase.java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

使用這些轉換器,您可以使用自定義型別進行其他查​​詢,就像使用基本型別一樣,如以下程式碼片段所示:
User.java

@Entity
public class User {
    ...
    private Date birthday;
}

UserDao.java

@Dao
public interface UserDao {
    ...
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}

您也可以限制 @TypeConverters 不同的範圍,包括單個實體,DAO和DAO方法。有關更多詳細資訊,請參閱@TypeConverters 註釋的參考文件


5.2瞭解房間不允許使用物件引用的原因

關鍵要點:Room不允許實體類之間的物件引用。相反,您必須明確地請求您的應用程式需要的資料。

將資料庫中的關係對映到相應的物件模型是一種常見的做法,在伺服器端執行良好。即使程式在訪問時載入欄位,伺服器仍然執行良好。
但是,在客戶端,這種延遲載入是不可行的,因為它通常發生在UI執行緒上,並且在UI執行緒中查詢磁碟上的資訊會造成顯著的效能問題。UI執行緒通常有大約16毫秒的時間來計算和繪製一個活動的更新佈局,所以即使查詢只需要5毫秒,仍然可能是您的應用程式將耗盡時間畫框,造成明顯的視覺故障。如果有單獨的事務並行執行,或者裝置正在執行其他磁碟密集型任務,則查詢可能需要更多時間才能完成。但是,如果您不使用延遲載入,則應用程式將獲取比所需更多的資料,從而導致記憶體消耗問題。物件關係對映通常將這個決定留給開發人員,以便他們可以做任何最適合他們的應用程式的用例。開發人員通常決定在應用程式和使用者介面之間共享模型。但是,這種解決方案並不能很好地擴充套件,因為隨著UI的變化,共享模型會產生難以讓開發人員預測和除錯的問題。
例如,考慮載入Book物件列表的UI,每本書都有一個Author物件。您最初可能會設計查詢以使用延遲載入,以便Book使用getAuthor()方法返回作者的例項。呼叫的第一個呼叫將getAuthor()查詢資料庫。一段時間後,你意識到你也需要在應用的使用者介面中顯示作者的名字。您可以輕鬆地新增方法呼叫,如下面的程式碼片段所示:

authorNameTextView.setText(user.getAuthor().getName());

但是,這個看似無辜的變化使得表格在主線上被查詢。
如果提前查詢作者資訊,如果不再需要這些資料,則很難更改資料的載入方式。例如,如果您的應用程式的使用者介面不再需要顯示Author資訊,則您的應用程式將有效地載入不再顯示的資料,浪費寶貴的記憶體空間。如果Author類引用另一個表(例如,Books)則應用程式的效率會進一步降低。
要使用Room同時引用多個實體,您需要建立一個包含每個實體的POJO,然後編寫一個連線相應表的查詢。這種結構良好的模型與Room健壯的查詢驗證功能相結合,可讓您的應用程式在載入資料時消耗更少的資源,從而改善應用程式的效能和使用者體驗。

相關文章