1. 程式人生 > >Android Room 官方指南

Android Room 官方指南

官方文件翻譯

簡介

Room持久庫提供了一個SQLite抽象層,讓你訪問資料庫更加穩健,提升資料庫效能。

該庫幫助您在執行應用程式的裝置上建立應用程式的資料快取。這個快取是你的應用程式唯一的真實來源,允許使用者檢視應用程式中關鍵資訊的一致副本,而不管使用者是否有Internet連線。

匯入庫

dependencies {
    def room_version = "1.1.1"

    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version" // use
kapt for Kotlin // optional - RxJava support for Room implementation "android.arch.persistence.room:rxjava2:$room_version" // optional - Guava support for Room, including Optional and ListenableFuture implementation "android.arch.persistence.room:guava:$room_version" // Test helpers testImplementation "android.arch.persistence.room:testing:$room_version" }

AndroidX

dependencies {
    def room_version = "2.0.0-beta01"

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

    // 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"
}

使用Room儲存本地資料到資料庫中

Room在SQLite上提供了一個抽象層,以便在發揮SQLite能力的同時允許流暢的資料庫訪問。

處理複雜的結構化資料的應用程式可以極大地受益於本地資料的持久化。最常見的用例是快取相關的資料片段。這樣,當裝置無法訪問網路時,使用者仍然可以在離線時瀏覽該內容。在裝置返回聯機之後,任何使用者發起的內容更改都會同步到伺服器。

由於Room負責這些問題,我們強烈建議使用Room而不是SQLite。但是,如果您更習慣於直接使用SQLite API,請使用SQLite讀取儲存資料

Room有3個主要的元件:

Database:包含資料庫持有者,並充當與應用程式持久化的、關係型的資料的底層連線的主要訪問點。

@Database註解的類應滿足以下條件:

  • 是一個繼承RoomDatabase的抽象類。

  • 在註釋中包含與資料庫相關聯的實體列表。

  • 包含一個具有0個引數的抽象方法,並返回用@Dao註釋的類。

在執行時,您可以通過呼叫Room.databaseBuilder()Room.inMemoryDatabaseBuilder()獲取資料庫例項。

Entity:表示資料庫內的表。

DAO: 包含用於訪問資料庫的方法。

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

image

圖1. Room架構圖

下面的程式碼片段示例了包含一個entity和一個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例項都相當消耗效能,並且您很少需要訪問多個例項。

為體驗Room,嘗試 Android Room with a ViewAndroid永續性 codelabs。若要瀏覽房間程式碼示例,請參閱 Android Architecture Components samples

使用Room實體定義資料

預設情況下,Room為實體中定義的每個欄位建立一個列。如果實體有不想持久的欄位,則可以使用@Ignore來註解它們。必須通過Database類中的entities陣列引用實體類。

下面的程式碼片段顯示瞭如何定義實體:

@Entity
public class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

要持久化一個欄位,Room必須有機會進入它。你可以把一個欄位公開,或者你可以為它提供一個getter和setter方法。如果使用getter和setter方法,請記住它們是基於Room中的JavaBeans約定。

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

使用主鍵

每個實體必須定義至少1個欄位作為主鍵。即使只有1個欄位,仍然需要用@PrimaryKey註解欄位。此外,如果您想Room自動分配IDs給實體,則可以設定@ PrimaryKey的autoGenerate屬性。如果實體具有複合主鍵,則可以使用@Entity註解的primaryKeys屬性,如下面的程式碼片段所示:

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

    @Ignore
    Bitmap picture;
}

By default, Room uses the class name as the database table name. If you want the table to have a different name, set the tableName property of the @Entity annotation, as shown in the following code snippet:
預設情況下,Room使用類名作為資料庫表名。如果希望表具有不同的名稱,請設定@Entity註解的tableName屬性,如下面的程式碼片段所示:

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

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

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

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

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

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

    @Ignore
    Bitmap picture;
}

註解宣告與唯一性

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

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

    public String firstName;
    public String address;

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

    @Ignore
    Bitmap picture;
}

有時,資料庫中的某些欄位或欄位組必須是唯一的。可以通過將@Index註解的唯一屬性設定為true來強制執行此唯一性屬性。下面的程式碼示例防止表中包含兩個行,它們包含firstName和lastName列的相同值集:

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

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

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

    @Ignore
    Bitmap picture;
}

定義物件之間的關係

因為SQLite是關係資料庫,所以可以指定物件之間的關係。儘管大多數物件關係對映庫允許實體物件相互引用,但Room明確禁止這一點。要了解這一決定背後的技術原理,請了解為什麼Room不允許物件引用

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

例如,如果有另一個實體稱為Book,則可以使用@ForeignKey 註解定義其與使用者實體的關係,如下面的程式碼片段所示:

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

    public String title;

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

外來鍵非常強大,因為它們允許您指定被引用實體更新時發生的事情。例如,如果在@ForeignKey註解中包含onDelete = CASCADE,則可以告訴SQLite刪除使用者的所有圖書,如果使用者的相應例項被刪除。

注意:SQLite處理@Insert( onconflict=REPLACE)作為一套REMOVE和REPLACE操作而不是單一的更新操作。本替換衝突值的方法有可能影響您的外來鍵約束。更多的細節,請看SQLite的文件ON_CONFLICT的條款。

建立巢狀物件

有時,即使物件包含多個欄位,您也希望在資料庫邏輯中將實體或普通Java物件(POJO)表示為一個有粘性的整體。在這些情況下,可以使用@Embedded註解來表示要分解成表中的子欄位的物件。然後,可以像其他單個列一樣查詢嵌入式欄位。

例如,我們的使用者類可以包括一個型別地址欄位,該欄位表示名為“街道”、“城市”、“狀態”和“郵政編碼”的欄位的組合。若要將組合列單獨儲存在表中,請在使用者類中使用@Embedded註解的地址欄位,如下面的程式碼片段所示:

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

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

@Entity
public class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

表示使用者物件的表包含包含以下名稱的列:id, firstName, street, state, city, and post_code。

注意:嵌入式欄位還可以包括其他嵌入式欄位。

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

使用Room DAOs訪問資料

使用“Room persistence library”訪問應用程式的資料,可以使用資料訪問物件或DAOs。這組DAO物件構成了Room的主要元件,因為每個DAO都包含了對應用程式資料庫訪問的抽象方法。

通過使用DAO類訪問資料庫而不是查詢構造器或直接查詢,可以分離資料庫體系結構的不同元件。此外,DAOs允許您在測試應用程式時輕鬆模擬資料庫訪問。

注意:在嚮應用程式新增DAO類之前,將架構元件庫新增到應用程式的build.gradle中。

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

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

定義查詢方法

有多種查詢方法,可以使用DAO類來表示。這個文件包括幾個常見的例子。

插入

當您建立一個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型別的值。

有關詳細資訊,請參閱@Insert註解的參考文件,以及SQLite documentation for rowid tables

更新

Update方法在資料庫中用於修改一組實體的欄位。它使用每個實體的主鍵來匹配查詢。

下面的程式碼片段演示如何定義此方法:

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

雖然通常不需要,但可以使此方法返回int型值,以指示資料庫中更新的行數。

刪除

刪除

Delete方法用於從資料庫中刪除給定引數的一系列實體,它使用主鍵匹配資料庫中相應的行。

下面的程式碼片段演示如何定義此方法:

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

雖然通常不需要,但可以使用此方法返回int值,以指示從資料庫中刪除的行數。

資訊查詢

@Query是DAO類中使用的主要註解。它允許您在資料庫上執行讀/寫操作。每個@Query方法在編譯時被驗證,因此,如果存在查詢問題,則會發生編譯錯誤而不是執行時故障。

Room還驗證查詢的返回值,這樣如果返回物件中欄位的名稱與查詢響應中的相應列名不匹配,則Room將以以下兩種方式之一提醒您:

  • 如果只有一些欄位名匹配,則發出警告。

  • 如果沒有欄位名匹配,則會出錯。

簡單查詢

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

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

將引數傳遞到查詢中

大多數情況下,需要將引數傳遞到查詢中以執行篩選操作,例如只顯示年齡大於某一年齡的使用者。要完成此任務,請在您的Room註解中使用方法引數,如下面的程式碼片段所示:

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

當在編譯時處理此查詢時,Room繫結引數與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);
}

返回列的子集

大多數時候,你只需要得到一個實體的幾個欄位。例如,您的UI可能只顯示使用者的first name和last name,而不顯示使用者的每一個細節。通過只獲取應用程式UI中出現的列,可以節省寶貴的資源,並且查詢完成得更快。

只要可以將結果列對映到返回的物件中,就可以讓您從查詢中返回任何基於Java的物件。例如,您可以建立以下普通的基於Java的物件(POJO)來獲取使用者的first name和last name:

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_name和last_name列的值,並且這些值可以對映到NameTuple類的欄位中。因此,Room可以生成適當的程式碼。如果查詢返回太多列,或一列在NameTuple類中不存在,則Room將顯示警告。

注意:這些POJOs也可以使用@Embedded註解。

傳遞引數集合

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

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

可觀察的查詢

當執行查詢時,您經常希望應用程式的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中,Room根據查詢列表來決定是否更新LiveData的例項。

RXJava的響應式查詢

Room還可以從您定義的查詢中返回RXJava2 PublisherFlowable動物件。若要使用此功能,請將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);
}

有關詳細資訊,請參閱谷歌開發人員Room和RXJava文章。

直接Cursor訪問

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

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

注意:使用Cursor API是非常令人沮喪的,因為它不能保證行是否存在或行包含哪些值。只有當你已經有了期望Cursor的程式碼,並且程式碼不容易重構時,才使用這個功能。

多表查詢

有些查詢可能需要訪問多個表來計算結果。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);
}

還可以從這些查詢中返回POJOs。例如,您可以編寫一個查詢,載入使用者和他們的寵物的名字如下:

@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;
   }
}

Room資料庫遷移

在應用程式中新增和更改特性時,你需要修改實體類以反映這些更改。當用戶更新應用程式到最新版本時,不希望它們丟失所有現有資料,尤其是如果無法從遠端伺服器恢復資料。

Room persistence library“庫允許您編寫Migration類來儲存使用者資料。每個遷移類指定起始版本和終結版本。在執行時,Room執行每個遷移類的migrate()方法,使用正確的順序將資料庫遷移到後面的版本。

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

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");
    }
};

注意:為了使遷移邏輯正常執行,請使用完整查詢而不是引用表示查詢的常量。

遷移過程結束後,Room驗證schema以確保遷移正確地發生。如果Room發現問題,它會丟擲包含不匹配資訊的異常。

測試遷移

遷移不是微不足道的,無法正確寫入,可能會導致應用程式崩潰。為了保持應用程式的穩定性,您應該事先測試遷移。Room提供了測試的Maven元件來幫助這個測試過程。但是,要使這個元件生效,您需要匯出資料庫的schema。

匯出schemas

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

build.gradle

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

您應該在您的版本控制系統中儲存表示資料庫的schema歷史的匯出JSON檔案,因為它允許Room建立用於測試目的的資料庫的舊版本。

為了測試這些遷移,新增android.arch.persistence.room:testing 的Maven依賴,將並schema新增到asset資料夾,如下面的程式碼片段所示:

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.
    }
}

測試資料庫

在使用Room庫建立資料庫時,驗證應用程式的資料庫和使用者的資料的穩定性是很重要的。

測試資料庫有2種方法:

  • 在Android裝置上。

  • 在主機開發機器上(不推薦)。

有關資料庫遷移的specific,請參見遷移測試

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

Android裝置測試

測試資料庫實現的推薦方法是編寫一個執行在Android裝置上的JUnit測試。因為這些測試不需要建立一個activity,所以它們應該比UI測試更快執行。

在設定測試時,應建立資料庫的in-memory版本,以使測試更加封閉,如以下示例所示:

@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));
    }
}

主機測試

Room 使用SQLite支援庫,它提供與Android框架類中的那些介面匹配的介面。此支援允許您通過支援庫的自定義實現來測試資料庫查詢。

注意:即使這個設定允許您的測試執行得很快,但不推薦使用,因為在您的裝置和使用者裝置上執行的SQLite版本可能與主機上的版本不匹配。

使用Room引用複雜資料

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

使用型別轉換器

有時,應用程式需要使用自定義資料型別,其值要儲存在單個數據庫列中。為了向自定義型別新增這種支援,您提供了一個TypeConverter,它將自定義類轉換為一個已知的型別,該型別可以持久。

例如,如果我們想儲存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();
    }
}

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

接下來,向AppDatabase類新增@TypeConverters註釋,以便AppDatabase可以使用您為每個entity定義的轉換器和DAO:

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 findUsersBornBetweenDates(Date from, Date to);
}

您還可以將@TypeConverters限制為不同的範圍,包括單個實體、DAOs和DAO方法。有關詳細資訊,請參閱@TypeConverters註解的參考文件。

理解為什麼Room不允許物件引用

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

從資料庫到相應物件模型的對映關係是一種常見的操作,在伺服器端工作得很好。即使程式在訪問時載入欄位,伺服器仍然執行得很好。

但是,在客戶端,這種型別的懶載入是不可行的,因為它通常發生在UI執行緒上,並且在UI執行緒上查詢disk上的資訊會產生顯著的效能問題。UI執行緒通常具有大約16ms來計算並繪製活動的更新佈局,因此即使查詢僅佔用5毫秒,您的應用程式仍然可能耗盡繪製幀的時間,從而引起明顯的視覺延遲。如果有一個單獨的事務並行執行,或者如果裝置正在執行其他磁碟密集型任務,則查詢可能需要更多的時間來完成。但是,如果你不使用延遲載入,你的應用程式會獲取比它需要更多的資料,從而造成記憶體消耗問題。

物件關係對映通常把這個決定留給開發人員,這樣他們就可以為他們的應用程式用例做任何最好的事情。開發人員通常決定在應用程式和UI之間共享模型。然而,這個解決方案規模不大,因為隨著時間的推移UI改變,共享模型產生了開發人員難以預料和除錯的問題。

例如,考慮一個UI,它載入一個書本物件列表,每一本書都有一個作者物件。您可能最初設計查詢使用惰性載入,以便使用getAuthor()方法來返回作者。getAuthor()呼叫的第一個呼叫查詢資料庫。一段時間後,您意識到需要在應用程式的UI中顯示作者姓名。您可以很容易地新增方法呼叫,如下面的程式碼片段所示:

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

然而,這種看似無害的更改導致在主執行緒上查詢作者表。

如果您提前查詢作者資訊,那麼如果不再需要該資料,則難以更改載入資料的方式。例如,如果你的應用程式的UI不再需要顯示作者資訊,你的應用程式就有效地載入不再顯示的資料,浪費寶貴的記憶體空間。如果作者類引用另一個表(如書籍),則應用程式的效率會進一步降低。

在使用Room同時引用多個實體時,您需要建立一個包含每個實體的POJO,然後編寫一個連線相應表的查詢(多表查詢,必須先查出需要的資料)。這種結構良好的模型,結合Room強大的查詢驗證能力,可以讓應用程式在載入資料時消耗更少的資源,提高應用程式的效能和使用者體驗。

官方Sample:https://github.com/googlesamples/android-architecture-components/tree/master/PersistenceContentProviderSample

資料庫遷移Sample:https://github.com/googlesamples/android-architecture-components/tree/master/PersistenceMigrationsSample