系列文章導航:
- 【譯】Google官方推出的Android架構組件系列文章(一)App架構指南
- 【譯】Google官方推出的Android架構組件系列文章(二)將Architecture Components引入工程
- 【譯】Google官方推出的Android架構組件系列文章(三)處理生命周期
- 【譯】Google官方推出的Android架構組件系列文章(四)LiveData
- 【譯】Google官方推出的Android架構組件系列文章(五)ViewModel
- 【譯】Google官方推出的Android架構組件系列文章(六)Room持久化庫
原文地址:https://developer.android.com/topic/libraries/architecture/room.html
Room
在SQLite之上提供了一個抽象層,可以在使用SQLite的全部功能的同時流暢訪問數據庫。
注意:將
Room
導入工程,請參考將Architecture Components引入工程
需要處理大量結構化數據的應用能從本地持久化數據中受益匪淺。最常見的使用場景是緩存相關的數據。比如,當設備無法訪問網絡時,用戶仍然可以在離線時瀏覽內容。當設備重新聯網后,任何用戶發起的內容更改將同步到服務器。
核心框架提供了操作原始SQL內容的內置支持。盡管這些API很強大,但它們相對較低層,需要大量的時間和精力才能使用:
- 沒有對原始SQL查詢語句的編譯時驗證。 當你的數據圖變化時,你需要手動更新受影響的SQL查詢語句。這個過程可能很耗時,而且容易出錯。
- 你需要使用大量模板代碼來進行SQL語句和Java數據對象的轉換。
Room
在SQLite
之上提供一個抽象層,來幫助你處理這些問題。
Room
包含三大組件:
-
Database:利用這個組件來創建一個數據庫持有者。注解定義一系列實體,類的內容定義一系列DAO。它也是底層連接的主入口點。
注解類應該是繼承RoomDatabase的抽象類。在運行期間,你可以通過調用
Room.databaseBuilder()
或Room.inMemoryDatabaseBuilder()
方法獲取其實例。 Entity:這個組件表示持有數據庫行的類。對于每個實體,將會創建一個數據庫表來持有他們。你必須通過Database類的entities數組來引用實體類。實體類的中的每個字段除了添加有@Ignore注解外的,都會存放到數據庫中。
注意:Entity可以有一個空的構造函數(如果DAO類可以訪問每個持久化字段),或者一個構造函數其參數包含與實體類中的字段匹配的類型和名字。
Room
還可以使用全部或部分構造函數,比如只接收部分字段的構造函數。
-
DAO: 該組件表示作為數據訪問對象(
DAO
)的類或接口。DAO
是Room
的主要組件,負責定義訪問數據庫的方法。由@Database
注解標注的類必須包含一個無參數且返回使用@Dao
注解的類的抽象方法。當在編譯生成代碼時,Room
創建該類的實現。
注意:通過使用DAO類代替查詢構建器或者直接查詢來訪問數據庫,你可以分離數據庫架構的不同組件。此外,DAO允許你在測試應用時輕松地模擬數據庫訪問。
這些組件,以及與應用程序其他部分的關系,如圖所示:
以下代碼片段包含一個數據庫配置樣例,其包含一個實體和一個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
,或為它提供getter
和setter
。如果你使用setter
和getter
,請記住,它們基于Room
的Java 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
來強制滿足唯一屬性。下面代碼樣例阻止表含有對于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;
}
關系
因為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)
作為一組REMOVE
和REPLACE
操作,而不是單個UPDATE
操作。這個替換沖突值的方法將會影響到你的外鍵約束。更多詳細信息,請參見SQLite文檔的ON_CONFLICT
語句。
嵌套對象
有時,你希望將一個實體或POJO表達作為數據庫邏輯中的一個整體,即使對象包含了多個字段。在這種情況下,你可以使用@Embeded
注解來表示要在表中分為為子字段的對象。然后,你可以像其他單獨的列一樣查詢嵌入的字段。
例如,我們的User
類可以包含一個類型為Address
的字段,其表示了一個字段組合,包含street
、city
、state
和postCode
。為了將這些組合列單獨的存放到表中,將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
對象的表將包含以下名字的列:id
,firstName
,street
,state
,city
和post_code
注意:嵌入字段也可以包含其他潛入字段。
如果實體包含了多個同一類型的嵌入字段,你可以通過設置prefix
屬性來保持每列的唯一性。Room
然后將提供的值添加到嵌入對象的每個列名的開頭。
數據訪問對象(DAO)
Room
的主要組件是Dao
類。DAO
以簡潔的方式抽象了對于數據庫的訪問。
Dao
要么是一個接口,要么是一個抽象類。如果它是抽象類,它可以有一個使用RoomDatabase
作為唯一參數的可選構造函數。
注意:
Room
不允許在主線程中訪問數據庫,除非你可以builder上調用allowMainThreadQueries(),因為它可能會長時間鎖住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<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
的方法
@Query是DAO
類中使用的主要注解。可以讓你執行數據庫讀/寫操作。每個@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 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
則顯示一個警告。
注意:這些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
還能從你定義的查詢中返回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
會監視查詢中引用的所有無效的表。(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
對象,另一個則執行從Long
到Date
的逆向轉換。由于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
到不同的作用域,包括單獨的實體,DAO
和DAO
方法。更多信息,參見@TypeConverters的引用文檔。
數據庫遷移
當你添加和更改App功能時,你需要修改實體類來反映這些更改。當用戶更新到你的應用最新版本時,你不想要他們丟失所有存在的數據,尤其是你無法從遠端服務器恢復數據時。
Room
允許你編寫Migration類來保留用戶數據。每個Migration
類指明一個startVersion
和endVersion
。在運行時,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
創建舊版本的數據庫以進行測試。
為了測試這些遷移,添加Room
的android.arch.persistence.room:testing
組件到測試依賴,然后添加模式位置作為一個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
可以讓你在測試過程中輕松模擬數據訪問層。這個過程是可能的,因為你的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
禁止實體類之間的對象引用。相反,你必須顯式請求你的應用程序需要的數據。