Room 在 SQLite 之上提供了一個抽象層,以便在利用 SQLite 所有功能的同時可以流暢的訪問數據庫。javascript
注:在 Android 項目中導入 Room,請參閱添加組件到項目中html
將數據持久化到本地對於應用程序處理大量結構化數據有很是大的好處。最多見的狀況是緩存相關數據。這樣,當設備沒法訪問網絡時,用戶仍然能夠在離線狀態下瀏覽內容。而後,在設備從新上線後,任何用戶發起的內容變動都會同步到服務器。java
核心框架提供了內置支持來處理原始的 SQL 內容。雖然這些 API 是強大的,可是它們很是低級而且使用起來須要話費巨大的時間和精力:android
原始的 SQL 查詢沒有編譯時驗證。隨着數據圖的更改,須要手動更新收影響的 SQL 查詢。這個過程是耗時的而且容易出錯。git
須要使用大量的樣板代碼用於在 SQL 查詢和 Java 數據對象之間轉換。github
Room 在 SQLite 之上提供的抽象層幫助處理這些問題。sql
Room 有 3 個重大的組件:數據庫
Database:可使用此組件建立數據庫的持有者。經過註解定義實體列表,經過類的內容定義數據庫中數據訪問對象(DAO)列表。它是底層鏈接的主要接入點。設計模式
註解的類應該是一個繼承了 RoomDatabase 的抽象類。在運行時,能夠經過調用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 獲取一個實例。數組
Entity:該組件表示一個保存有數據庫行的類。對於每一個 Entity,建立一個數據庫表來保存項目。必須經過 Database 中的 entities 字段引用 Entity 類。Entity 中的每一個字段都會持久化到數據庫中,除非使用 @Ignore 註解。
注:Entity 能夠擁有一個空的構造函數(若是 DAO 能夠訪問每一個持久化的字段)或一個參數的類型和名字和 Entity 中的字段都匹配的構造函數。Room 還可使用所有或部分的構造函數,如只接受部分字段的構造函數。
DAO:該組件表示一個數據訪問對象(DAO)的類或接口。DAO 是 Room 的主要組件,其職責是定義方法來訪問數據庫。被 @Database 註解的類必須包含一個沒有參數的抽象方法,該方法的返回值是被 @Dao 註解的類。在編譯時生成代碼時,Room 建立該類的實現。
注:經過使用 DAO 訪問數據庫,而不是使用查詢建造者或直接查詢,能夠分離數據庫結構的不一樣組件。另外,DAO 能夠在測試應用時很容易的模擬數據庫。
這些組件及其與應用程序其它部分的關係,如圖 1 所示:
圖 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 和 setters,
// 可是 Room 須要它們。
}複製代碼
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 會在數據庫中爲該 Entity 建立一個數據庫表。
默認狀況下,Room 爲每一個定義在 Entity 中的字段建立列。若是不但願 Entity 中某些字段被持久化,可使用 @Ignore 註解這些字段。以下面的代碼片斷所示:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}複製代碼
要持久化一個字段,Room 必須有權限訪問它。能夠將字段設置爲 public,或爲該字段提供 setter 和 getter 方法。若是使用 setter 和 getter 方法,請記住,它們基於 Room 中的 Java Beans 約定。
每一個 Entity 必須定義至少一個字段做爲主鍵。即便只有一個字段,仍然須要使用 @PrimaryKey 註解該字段。若是但願 Room 爲 Entity 分配自動 ID,能夠設置 @PrimaryKey 的 autoGenerate) 屬性。若是 Entity 有複合主鍵,可使用 @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 添加索引,須要在 @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 來實施惟一性。如下示例代碼能夠防止表中的兩行數據具備一組相同的 firstNmae 列和 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;
}複製代碼
由於 SQLite 是一個關係型數據庫,能夠指定兩個對象間的關係。即便大多數 ORM 庫容許 Entity 對象之間互相引用,可是 Room 明確禁止這樣作。詳細信息,請參閱附錄:Entity 之間無對象引用。
雖然不能使用直接關係,可是 Room 容許在 Entity 之間定義外鍵約束。
例如,有一個名爲 Book 的 Entity,可使用 @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;
}複製代碼
外鍵是很是有用的,外鍵可讓你指定當應用的 Entity 更新時作什麼。例如,經過在 @ForeignKey 註解中包含 onDelete=CASCADE) 能夠告訴 SQLite 若是相應的 User 實例被刪除,則刪除該用戶的全部圖書。
注:SQLite 將 @Insert(OnConflict=REPLACE) 看成一組 REMOVE 和 REPLACE 操做來處理,而不是看成單獨的 UPDATE 操做處理。這種替換衝突值的方法可能會影響外鍵約束。更多詳細信息,請參閱 SQLite 文檔的 ON_CONFLICT 子句。
有時,可能會須要將一個 Entity 或普通 Java 對象(POJO)做爲數據庫邏輯中的一個連貫的總體,即便對象包含多個字段。對於這些狀況,可使用 @Embedded 註解來表示要在表中分解爲其子字段的對象。能夠像查詢其它獨立字段同樣查詢嵌套的字段。
例如:User 類可能包含一個類型爲 Address 的字段,表示一個名爲 street,city,state 和 postCode 的字段組合。要在表中單獨存儲組合列,請在 User 類中包含一個用 @Embedded 註解的 Address 字段,以下面的代碼片斷所示:
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 對象的表包含如下名稱的列:id,firstName,street,state,city 和 post_code。
注:嵌套字段能夠包含其它的嵌套字段。
若是 Entity 有多個相同類型的嵌套字段,能夠經過設置 prefix) 屬性保持列惟一。Room 會將提供的值添加到嵌套對象每一個列名的開頭。
Room 的主要組件是 Dao 類。DAP 以乾淨的方式抽象訪問數據庫。
注:除非在建造者上調用了 allowMainThreadQueries()),不然 Room 不容許在主線程上訪問數據庫,由於這樣可能會長時間鎖定 UI。異步查詢(返回 LiveData 或 RxJava 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
更多詳細信息,請參閱 @Insert 註解的參考文檔或 SQLite 文檔的 Rowid Tables。
Update 是一個用於在數據庫中更新以參數形式給到的一組 Entity 的簡便方法。它使用一個查詢以與每一個 Entity 的主鍵匹配。如下代碼片斷演示瞭如何定義該方法:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}複製代碼
雖然一般不是必須的,可讓該方法返回一個 int 值,表示數據庫中更新的行數。
Delete 是一個用於從數據庫中刪除以參數形式給到的一組 Entity 的簡便方法。它使用主鍵來查找要刪除的 Entity。如下代碼片斷演示瞭如何定義該方法:
@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 和方法參數 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);
}複製代碼
大多數狀況下,只須要獲取到一個 Entity 的幾個字段。例如,UI 可能只顯示用戶的名字和姓氏,而不是用戶的每一個詳細信息。經過只獲取應用程序 UI 中顯示的列,能夠節省寶貴的資源,而且查詢完成更快。
Room 能夠返回任何 Java 對象,只要能夠將返回列的集合映射到返回的對象中。例如,能夠建立以下的 POJO 來獲取用戶的名字和姓氏:
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 會顯示警告。
注:這些 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 對象。
Room 還能夠從定義的查詢返回 RxJava2 的 Publisher 和 Flowable 對象。要使用該功能,須要將 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 容許編寫任何查詢,因此也能夠鏈接表。另外,若是響應是一個可觀察類型,如:Flowable 或 LiveData,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;
}
}複製代碼
Room 提供內置支持基本數據類型及其封裝類型。然而,有時候須要使用自定義類型,並將其值存儲在數據庫中單獨的列裏。要添加自定義類型的支持,須要提供一個 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] 對象,另外一個執行逆向轉換,將 [Long] 轉換爲 Date。由於 Room 已經知道如何持久化 [Long] 對象,因此它可使用該轉換器來持久化 Date 類型的值。
下面,給 AppDatabase 添加 [@TypeConverter] 註解,以便 Room 能夠在 AppDatabase 對每一個 Entity 和 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 限制不一樣的做用域,包括單個 Entity,DAO 和 DAO 方法,更多詳細信息,請參閱 @TypeConverters 註解的參考文檔。
當給應用程序添加或修改功能時,須要修改 Entity 類來反映這些修改。當用戶更新到應用程序的最新版本時,你不但願他們丟失現有數據,特別是若是沒法從遠程服務端恢復數據時。
Room 容許以編寫 Migration 類的方式保留用戶數據。每一個 Migration 類都指定一個 startVersion 和 endVersion。在運行時,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");
}
};複製代碼
警告:爲了使遷移邏輯保持正常運行,請使用完整的查詢,而不是引用表明查詢的常量。
遷移過程結束後,Room驗證數據庫架構以確保遷移執行正確。若是 Room 發現問題,將會拋出含有不匹配信息的異常。
遷移不是簡單的寫入,而且沒法正確寫入它們可能會致使應用程序崩潰。爲了保持應用的穩定性,應該實現測試遷移。Room 提供了一個測試 Maven 組件來幫助進行該測試過程。然而,爲了使該組件正常工做,須要導出數據庫的結構
彙編後,Room 將數據庫的架構信息導出爲 JSON 文件。要導出架構,須要在 build.gradle 文件中設置 room.schemaLocation 註解數據處理屬性,如如下代碼片斷所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}複製代碼
應該將導出的 JSON 文件(其表示數據庫架構的歷史記錄)存儲在版本控制系統中,由於它容許 Room 建立舊版本的數據庫以進行測試。
要測試這些遷移,請未來自 Room 的 android.arch.persistence.room:testing Maven 組件添加到測試依賴中,並將架構文件的位置添加爲資產文件,如如下代碼片斷所示:
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.getContext(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db 的架構版本是 1. 使用 SQL 查詢插入一些數據。
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);
// 準備下一個版本。
db.close();
// 使用 version2 從新打開數據庫
// 而且提供 MIGRATION_1_2 做爲遷移過程。
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
// MigrationTestHelper 自動驗證架構的更改,
// 可是須要驗證數據是否遷移正確。
}
}複製代碼
當應用程序運行測試時,若是不測試數據庫自己則沒有必要建立完整的數據庫。Room 容許在測試中輕鬆的模擬數據訪問層。這個過程是可能的,由於 DAO 不會泄漏數據庫的任何細節。當測試應用程序的其他部分時,應該建立 DAO 類的模擬或假的實例。
有兩種方式能夠測試數據庫:
在開發主機上。
在 Android 設備上。
Room 使用 SQLite 支持庫,它提供與 Android Framework 類中一致的接口。該支持庫容許傳遞支持庫的自定義實現來測試數據庫查詢。
即便此設置可讓測試快速運行,可是不推薦使用,由於你的設備上(和用戶設備上)運行的 SQLite 版本和你主機上運行的版本不一致。
用於測試數據庫實現的推薦方式是編寫在 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 更新超時,會產生難以預料和調試的問題。
例如:讓 UI Book 對象的列表,而且沒本書都有一個 Author 對象。最初可能設計使用惰性加載查詢,如 Book 實例使用 getAuthor() 方法返回做者。getAuthor() 的第一次調用會調用查詢數據庫。一段時間後,還須要在應用程序的 UI 顯示做者名字。能夠輕鬆的添加方法調用,如如下代碼片斷所示:
authorNameTextView.setText(user.getAuthor().getName());複製代碼
然而,這個看似無辜的變化形成在主線程上查詢 Author 表。
若是急需查詢做者信息,當不在須要數據時,很難改變數據的加載方式,例如,應用程序的 UI 再也不須要顯示有關特定做者的信息。所以,當數據不在顯示時應用程序必須繼續加載數據。若是 Author 類引用其它的表(如,有 getBooks() 方法)狀況會更糟。
基於這些緣由,Room 禁止 Entity 之間的對象引用。相反,必須明確的請求應用程序須要的數據。