【譯】Google官方推出的Android架構組件系列文章(六)Room持久化庫

系列文章導航

  1. 【譯】Google官方推出的Android架構組件系列文章(一)App架構指南
  2. 【譯】Google官方推出的Android架構組件系列文章(二)將Architecture Components引入工程
  3. 【譯】Google官方推出的Android架構組件系列文章(三)處理生命周期
  4. 【譯】Google官方推出的Android架構組件系列文章(四)LiveData
  5. 【譯】Google官方推出的Android架構組件系列文章(五)ViewModel
  6. 【譯】Google官方推出的Android架構組件系列文章(六)Room持久化庫

原文地址:https://developer.android.com/topic/libraries/architecture/room.html

Room在SQLite之上提供了一個抽象層,可以在使用SQLite的全部功能的同時流暢訪問數據庫。

注意:將Room導入工程,請參考將Architecture Components引入工程

需要處理大量結構化數據的應用能從本地持久化數據中受益匪淺。最常見的使用場景是緩存相關的數據。比如,當設備無法訪問網絡時,用戶仍然可以在離線時瀏覽內容。當設備重新聯網后,任何用戶發起的內容更改將同步到服務器。

核心框架提供了操作原始SQL內容的內置支持。盡管這些API很強大,但它們相對較低層,需要大量的時間和精力才能使用:

  • 沒有對原始SQL查詢語句的編譯時驗證。 當你的數據圖變化時,你需要手動更新受影響的SQL查詢語句。這個過程可能很耗時,而且容易出錯。
  • 你需要使用大量模板代碼來進行SQL語句和Java數據對象的轉換。

RoomSQLite之上提供一個抽象層,來幫助你處理這些問題。

Room包含三大組件:

  • Database:利用這個組件來創建一個數據庫持有者。注解定義一系列實體,類的內容定義一系列DAO。它也是底層連接的主入口點。

    注解類應該是繼承RoomDatabase的抽象類。在運行期間,你可以通過調用Room.databaseBuilder()Room.inMemoryDatabaseBuilder()方法獲取其實例。

  • Entity:這個組件表示持有數據庫行的類。對于每個實體,將會創建一個數據庫表來持有他們。你必須通過Database類的entities數組來引用實體類。實體類的中的每個字段除了添加有@Ignore注解外的,都會存放到數據庫中。

注意:Entity可以有一個空的構造函數(如果DAO類可以訪問每個持久化字段),或者一個構造函數其參數包含與實體類中的字段匹配的類型和名字。Room還可以使用全部或部分構造函數,比如只接收部分字段的構造函數。

  • DAO: 該組件表示作為數據訪問對象(DAO)的類或接口。DAORoom的主要組件,負責定義訪問數據庫的方法。由@Database注解標注的類必須包含一個無參數且返回使用@Dao注解的類的抽象方法。當在編譯生成代碼時,Room創建該類的實現。

注意:通過使用DAO類代替查詢構建器或者直接查詢來訪問數據庫,你可以分離數據庫架構的不同組件。此外,DAO允許你在測試應用時輕松地模擬數據庫訪問。

這些組件,以及與應用程序其他部分的關系,如圖所示:

room_architecture.png

以下代碼片段包含一個數據庫配置樣例,其包含一個實體和一個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實例都相當昂貴,而且很少需要訪問多個實例。

實體

當一個類由@Entity注解,并且由@Database注解的entities屬性引用,Room將在數據庫中為其創建一張數據庫表。

默認,Room會為實體類中的每個字段創建一列。如果實體類中包含你不想保存的字段,你可以給他們加上@Ignore注解,如下面代碼片段所示:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

要持久化一個字段,Room必須能夠訪問它。你可以將字段設置為public,或為它提供gettersetter。如果你使用settergetter,請記住,它們基于RoomJava Bean約定。

主鍵

每個實體必須定義至少一個字段作為主鍵。甚至當僅僅只有一個字段時,你仍然需要為該字段加上@PrimaryKey注解。另外,如果你想讓Room為實體分配自增ID,你可以設置@PrimaryKey注解的autoGenerate屬性。如果實體包含組合主鍵,你可以使用@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;
}

索引和唯一約束

根據訪問數據的方式,你可能希望對數據庫中的某些字段進行索引,以加快查詢速度。要向實體添加索引,請在@Entity注解中包含indices屬性,列出要包含在索引或組合索引中的列的名字。

以下代碼片段演示此注解過程:

@Entity(indices = {@Index("name"), @Index("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來強制滿足唯一屬性。下面代碼樣例阻止表含有對于firstNamelastName列包含同樣的值的兩條記錄:

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

關系

因為SQLite是關系型數據庫,你可以指定對象間的關系。盡管大部分的ORM庫允許實體對象互相引用,但是Room明確禁止此操作。更多詳細信息,請參考附錄:實體間無對象引用

盡管你無法直接使用關系,Room仍然允許你定義實體間的外鍵約束。

例如,假如有另外一個叫做Book的實體,你可以使用@ForeignKey注解來定義它和User實體的關系,如下面代碼所示:

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

外鍵是很強大的,因為它允許你指明當引用的實體更新時應該怎么處理。比如,你可以通過在@ForeignKey注解中包含onDelete=CASCADE,來告訴SQLite如果某個User實例被刪除,則刪除該用戶的所有書。

注意SQLite處理@Insert(onConfilict=REPLACE)作為一組REMOVEREPLACE操作,而不是單個UPDATE操作。這個替換沖突值的方法將會影響到你的外鍵約束。更多詳細信息,請參見SQLite文檔ON_CONFLICT語句。

嵌套對象

有時,你希望將一個實體或POJO表達作為數據庫邏輯中的一個整體,即使對象包含了多個字段。在這種情況下,你可以使用@Embeded注解來表示要在表中分為為子字段的對象。然后,你可以像其他單獨的列一樣查詢嵌入的字段。

例如,我們的User類可以包含一個類型為Address的字段,其表示了一個字段組合,包含streetcitystatepostCode。為了將這些組合列單獨的存放到表中,將Address字段加上@Embedde注解,如下代碼片段所示:

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

這張表示User對象的表將包含以下名字的列:idfirstNamestreetstatecitypost_code

注意:嵌入字段也可以包含其他潛入字段。

如果實體包含了多個同一類型的嵌入字段,你可以通過設置prefix屬性來保持每列的唯一性。Room然后將提供的值添加到嵌入對象的每個列名的開頭。

數據訪問對象(DAO)

Room的主要組件是Dao類。DAO以簡潔的方式抽象了對于數據庫的訪問。

Dao要么是一個接口,要么是一個抽象類。如果它是抽象類,它可以有一個使用RoomDatabase作為唯一參數的可選構造函數。

注意Room不允許在主線程中訪問數據庫,除非你可以builder上調用allowMainThreadQueries(),因為它可能會長時間鎖住UI。異步查詢(返回LiveDataRxJava Flowable的查詢)則不受此影響,因為它們在有需要時異步運行在后臺線程上。

方便的方法

可以使用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方法接收僅僅一個參數,它可以返回一個long,表示插入項的新的rowId。如果參數是一個數組或集合,它應該返回long []List<Long>

更多詳情,參見@Insert注解的引用文檔,以及SQLite文檔的rowId表

更新

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的方法

@QueryDAO類中使用的主要注解。可以讓你執行數據庫讀/寫操作。每個@Query方法會在編譯時驗證,因此如果查詢有問題,則會發生編譯錯誤而不是運行時故障。

Room還會驗證查詢的返回值,以便如果返回對象中的字段名與查詢相應中的相應列名不匹配,Room則會以下面兩種方式的一種提醒你:

  • 如果僅僅某些字段名匹配,則給出警告
  • 如果沒有字段匹配,則給出錯誤。

簡單查詢

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

這是一條非常簡單的用于加載所有用戶的查詢。在編譯時,Room知道它是查詢user表的所有列。如果查詢包含語法錯誤,或者如果user表不存在于數據庫,Room會在應用編譯時,展示相應的錯誤消息。

給查詢傳遞參數

大部分情況,你需要給查詢傳遞參數以便執行過濾操作,比如僅僅展示年齡大于某個值的用戶。為了完成這個任務,在Room注解中使用方法參數,如下面代碼所示:

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

當查詢在編譯時處理時,Room匹配:minAge綁定參數和: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上顯示的幾列,你可以節省寶貴的資源,并且更快完成查詢。

Room允許你從查詢中返回任意的java對象,只要結果列集能被映射到返回的對象。比如,你可以創建下面的POJO來拉取用戶的first namelast 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_namelast_name列的值,并且這些值可以映射成NameTuple類的字段。因此,Room可以生成正確的代碼。如果查詢返回太多列,或者有列不存在NameTuple類,Room則顯示一個警告。

注意:這些POJO也可以使用@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.0版本,Room使用查詢中訪問的表列表來決定是否更新LiveData對象。

RxJava

Room還能從你定義的查詢中返回RxJava2PublisherFlowable對象。要使用此功能,請將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);
}

直接光標訪問

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

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

警告:非常不鼓勵使用Cursor API,因為它無法保證是否行存在,或者行包含什么值。僅當你已經具有期望使用Cursor的代碼,并且不能輕易重構時使用。

查詢多張表

一些查詢可能要求查詢多張表來計算結果。Room允許你寫任何查詢,因此你還可以連接表。此外,如果響應是一個可觀察的數據類型,比如FlowableLiveDataRoom會監視查詢中引用的所有無效的表。(Furthermore, if the response is an observable data type, such as Flowable or LiveData, Room watches all tables referenced in the query for invalidation)

以下代碼片段顯示了如何執行表連接,以整合包含借書用戶的表和包含目前借出的書信息的表之間的信息。

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

使用類型轉換器

Room提供對于基本類型和其包裝類的內置支持。然后,你有時候使用打算以單一列存放到數據庫中的自定義數據類型。為了添加對于這種自定義類型的支持,你可以提供一個TypeConverter,它將負責處理自定義類和Romm可以保存的已知類型之間的轉換。

比如,如果我們想要保存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類型的值。

接下來,你將@TypeConverters注解添加到AppDatabase類,以便Room可以使用你在AppDatabase中為每個實體和DAO定義的轉換器。

AppDatabase.java

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.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到不同的作用域,包括單獨的實體,DAODAO方法。更多信息,參見@TypeConverters的引用文檔。

數據庫遷移

當你添加和更改App功能時,你需要修改實體類來反映這些更改。當用戶更新到你的應用最新版本時,你不想要他們丟失所有存在的數據,尤其是你無法從遠端服務器恢復數據時。

Room允許你編寫Migration類來保留用戶數據。每個Migration類指明一個startVersionendVersion。在運行時,Room運行每個Migration類的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會驗證模式以確保遷移正確。如果Room發現問題,將還會拋出包含不匹配信息的異常。

測試遷移

遷移并不是簡單的寫入,并且一旦無法正確寫入,可能導致應用程序循環崩潰。為了保持應用程序的穩定性,你應該事先測試遷移。Room提供了一個測試Maven組件來輔助測試過程。然而,要使這個組件工作,你需要導出數據庫的模式。

導出數據庫模式

匯編后,Room將你的數據庫模式信息導出到一個JSON文件中。為了導出模式,在build.gradle文件中設置room.schemaLocation注解處理器屬性,如下所示:

build.gradle

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

你可以將導出的JSON文件(代表了你的數據庫模式歷史)保存到你的版本控制系統中,因為它可以讓Room創建舊版本的數據庫以進行測試。

為了測試這些遷移,添加Roomandroid.arch.persistence.room:testing組件到測試依賴,然后添加模式位置作為一個asset文件夾,如下所示:

build.gradle

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

測試包提供一個MigrationTestHelper類,該類可以讀取這些模式文件。它也是一個JUnit4TestRule類,因此它可以管理創建的數據庫。

遷移測試示例如下所示:

@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可以讓你在測試過程中輕松模擬數據訪問層。這個過程是可能的,因為你的DAO不會泄漏任何數據庫的細節。當測試應用的其余部分時,你應該創建DAO類的模擬或假的實例。

有兩種方式測試數據庫:

  • 在你的宿主開發機上
  • 在一臺Android設備上

在宿主機上測試

Room使用SQLite支持庫,它提供了與Android Framework類相匹配的接口。該支持允許你傳遞自定義的支持庫實現來測試數據庫查詢。

即使這些設置能讓你的測試運行非常快,也不推薦。因為運行在你的設備上的SQLite版本以及用戶設備上的,可能和你宿主機上的版本并不匹配。

在Android設備上測試

推薦的測試數據庫實現的方法是編寫運行在Android設備上的JUnit測試。因為這些測試并不需要創建activity,它們相比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));
    }
}

更多信息關于測試數據庫遷移,參見測試遷移

附錄:實體間無對象引用

將數據庫的關系映射到相應的對象模型是一種常見的做法,在服務端可以很好地運行。在服務端當訪問時,使用高性能的延遲加載字段。

然而,在客戶端,延遲加載是不可行的,因為它可能發生在UI線程上,并且在UI線程上查詢磁盤信息會產生顯著的性能問題。UI線程有大約16ms的時間來計算以及繪制activity的更新布局,因此即使一個查詢僅僅耗費5ms,仍然有可能你的應用會沒有時間繪制幀,引發可見的卡頓。更糟糕的是,如果并行運行一個單獨的事務,或者設備忙于其他磁盤重任務,則查詢可能需要更多時間完成。但是,如果你不使用延遲加載,應用獲取比其需要的更多數據,從而造成內存消耗問題。

ORM通常將此決定留給開發人員,以便他們可以基于應用的使用場景來做最好的事情。不幸的是,開發人員通常最終在他們的應用和UI之間共享模型,隨著UI隨著時間的推移而變化,難以預料和調試的問題出現。

舉個例子,使用加載Book對象列表的UI,每個Book對象都有一個Author對象。你可能最初設計你的查詢使用延遲加載,這樣的話Book實例使用getAuthor()方法來返回作者。第一次調用getAuthor()會調用數據庫查詢。一段時間后,你會意識到你需要在應用UI上顯示作者名字,你可以輕松添加方法調用,如以下代碼片段所示:

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

然而,這個看起來無害的修改,會導致Author表在主線程被查詢。

如果你頻繁的查詢作者信息,如果你不再需要數據,后續將會很難更改數據的加載方式,比如你的應用UI不再需要展示有關特定作者的信息的情況。因此,你的應用必須繼續加載并不需要顯示的數據。如果作者類引用另一個表,例如使用getBooks()方法,這種情況會更糟。

由于這些原因,Room禁止實體類之間的對象引用。相反,你必須顯式請求你的應用程序需要的數據。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容