Room持久化庫(kù)
Room為SQLite提供一個(gè)抽象層,在充分利用SQLite的同時(shí),允許流暢的數(shù)據(jù)庫(kù)訪問
注意:引入Room到你的android工程,參看 adding components to your project
應(yīng)用處理大量的結(jié)構(gòu)化數(shù)據(jù)能夠從本地持久化數(shù)據(jù)獲益很多,最通用的例子是緩存相關(guān)的數(shù)據(jù)碎片。那樣,當(dāng)設(shè)備不能訪問網(wǎng)絡(luò)的時(shí)候,用戶仍然可以瀏覽內(nèi)容。任何用戶發(fā)起的內(nèi)容改變?cè)谠O(shè)備恢復(fù)網(wǎng)絡(luò)的時(shí)候同步到服務(wù)器上。
核心框架對(duì)raw SQL內(nèi)容提供嵌入支持。盡管這些APIs是很給力的,但是他們相當(dāng)?shù)图?jí)并且需要大量的時(shí)間和精力去使用:
- raw SQL查詢沒有編譯時(shí)驗(yàn)證。當(dāng)你的數(shù)據(jù)圖改變,你需要手動(dòng)的更新受影響的SQL查詢。這個(gè)過程是耗時(shí)的和容易出錯(cuò)的。
- 你需要使用大量的樣板代碼在數(shù)據(jù)查詢和java數(shù)據(jù)對(duì)象之間轉(zhuǎn)換
Room為你處理這些問題。在Room中有三個(gè)主要組件。
Database(數(shù)據(jù)庫(kù)): 你可以使用這個(gè)組件創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)holder。注解定義了一系列entities并且類的內(nèi)容提供了一系列DAOs,它也是下層的主要連接 的訪問點(diǎn)。
注解的類應(yīng)該是一個(gè)抽象的繼承 RoomDatabase的類。在運(yùn)行時(shí),你能獲得一個(gè)實(shí)例通過調(diào)用Room.databaseBuilder()
或者Room.inMemoryDatabaseBuilder()
Entity(實(shí)體):這個(gè)組件代表了一個(gè)持有數(shù)據(jù)行的類。對(duì)于每個(gè)entity,一個(gè)數(shù)據(jù)庫(kù)表被創(chuàng)建用于持有items。你必須引用entity類通過
Database
類中的entities
數(shù)組。每個(gè)entity字段被持久化到數(shù)據(jù)庫(kù)中除非你注解它通過@Ignore
.
注意:Entities能夠有一個(gè)空的構(gòu)造函數(shù)(如果dao類能夠訪問每個(gè)持久化的字段)或者一個(gè)參數(shù)帶有匹配entity中的字段的類型和名稱的構(gòu)造函數(shù),例如一個(gè)只接收其中一些字段的構(gòu)造函數(shù)。
- DAO(數(shù)據(jù)訪問對(duì)象):這個(gè)組件代表了一個(gè)類或者接口作為DAO。DAOs 是Room中的主要組件,并且負(fù)責(zé)定義訪問數(shù)據(jù)庫(kù)的方法。被注解為@Database的類必須包含一個(gè)沒有參數(shù)的抽象方法并且返回注解為@Dao的類。當(dāng)在編譯時(shí)生成代碼,Room創(chuàng)建一個(gè)這個(gè)類的實(shí)現(xiàn)。
注意:使用DAO類訪問數(shù)據(jù)庫(kù)而不是query builders或者直接查詢。你可以把數(shù)據(jù)庫(kù)分成幾個(gè)組件。還有,DAOs允許你輕松的模擬數(shù)據(jù)庫(kù)訪問當(dāng)你測(cè)試你的應(yīng)用的時(shí)候。
這些組件和rest app的關(guān)系,如圖:
如下代碼片段包含一個(gè)數(shù)據(jù)庫(kù)配置的例子、一個(gè)entity,一個(gè)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();
}
通過創(chuàng)建以上文件,你可以使用如下代碼創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)實(shí)例:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
注意:你必須遵守單例模式當(dāng)初始化一個(gè)AppDatabase對(duì)象,因?yàn)槊總€(gè)RoomDatabase實(shí)例是相當(dāng)昂貴的,并且你幾乎不需要訪問多個(gè)實(shí)例。
Entities(實(shí)體)
當(dāng)一個(gè)類被注解為@Entity
并且引用到帶有@Database
注解的entities
屬性,Room為這個(gè)數(shù)據(jù)庫(kù)做的entity創(chuàng)建一個(gè)數(shù)據(jù)表。
默認(rèn)情況下,Room為每個(gè)定義在entity中的字段創(chuàng)建一個(gè)列。如果一個(gè)entity的一些字段你不想持久化,你可以使用@Ignore
注解它們,像如下展示的代碼片段:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
為了持久化一個(gè)字段,Room必須有它的入口。你可以使字段為public,或者你可以提供一個(gè)setter或者getter。如果你使用setter或者getter方法,記住在Room中他們遵守Java Beans的慣例。
Primary Key(主鍵)
每個(gè)entity必須至少定義一個(gè)field作為主鍵(primary key)。即使只有一個(gè)field,你也必須用@PrimaryKey注釋這個(gè)field。如果你想讓Room為entity設(shè)置自增ID,你可以設(shè)置@PrimaryKey的autoGenerate屬性。
@Entity(tableName = "user")
public class User {
@PrimaryKey(autoGenerate = true)
private Integer id;
...
}
如果你的entity有一個(gè)組合主鍵,你可以使用@Entity注解的primaryKeys屬性,具體用法如下:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
Room默認(rèn)把類名作為數(shù)據(jù)庫(kù)的表名。如果你想用其它的名稱,使用@Entity注解的tableName屬性,如下:
@Entity(tableName = "users")
class User {
...
}
注意:SQLite中的表名是大小寫敏感的。
與tablename屬性相似的是,Room使用字段名稱作為列名稱。如果你希望一個(gè)列有不同的名稱,為字段增加@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;
}
Indices and uniqueness(索引和唯一性)
為了提高查詢的效率,你可能想為特定的字段建立索引。要為一個(gè)entity添加索引,在@Entity注解中添加indices屬性,列出你想放在索引或者組合索引中的字段。下面的代碼片段演示了這個(gè)注解的過程:
@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;
}
有時(shí)候,某個(gè)字段或者幾個(gè)字段必須是唯一的。你可以通過把@Index注解的unique屬性設(shè)置為true來實(shí)現(xiàn)唯一性。下面的代碼防止了一個(gè)表中的兩行數(shù)據(jù)出現(xiàn)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;
}
Relationships(關(guān)系)
因?yàn)镾QLite是關(guān)系數(shù)據(jù)庫(kù),你可以指定對(duì)象之間的關(guān)聯(lián)。雖然大多數(shù)ORM庫(kù)允許entity對(duì)象相互引用,但是Room明確禁止了這種行為。更多細(xì)節(jié)請(qǐng)參考 Addendum: No object references between entities.
雖然不可以使用直接的關(guān)聯(lián),Room仍然允許你定義entity之間的外鍵(Foreign Key)約束。
比如,假設(shè)有另外一個(gè)entity叫做Book,你可以使用@ForeignKey
注解定義它和User entity之間的關(guān)聯(lián),如下:
@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;
}
外鍵非常強(qiáng)大,因?yàn)樗试S你指定當(dāng)被關(guān)聯(lián)的entity更新時(shí)做什么操作。例如,通過在@ForeignKey注解中包含Delete = CASCADE, 你可以告訴SQLite,如果相應(yīng)的User實(shí)例被刪除,那么刪除這個(gè)User下的所有book。
注意:SQLite處理
@Insert(OnConflict=REPLACE)
作為一個(gè)REMOVE
和REPLACE
操作而不是單獨(dú)的UPDATE操作。這個(gè)替換沖突值的方法能夠影響你的外鍵約束。更多細(xì)節(jié),參看 SQLite documentation。
Nested objects(內(nèi)嵌對(duì)象)
有時(shí),你希望entity或者POJOs作為一個(gè)整體在你數(shù)據(jù)庫(kù)的邏輯當(dāng)中,即使對(duì)象包含幾個(gè)字段。在這種情況下,你可以使用@Embedded注解去代表一個(gè)你希望分解成一個(gè)表中的次級(jí)字段的對(duì)象。接著你就可以查詢嵌入字段就像其他單獨(dú)的字段那樣。
例如,我們的user類能夠包含一個(gè)代表了street,city,state,postCode的組合字段Address。為了分別的保存組合列,包括被@Embedded注解的user類中的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;
}
Table表示了一個(gè)包含如下名稱列的User對(duì)象:id,firstName,street,state,city和post_code。
注意:嵌入字段也包括其他嵌入字段
如果一個(gè)字段有多個(gè)同一類型的嵌入字段,你能保持每個(gè)列是獨(dú)一無二的通過設(shè)置prefix屬性。Room然后將所提供的值添加到嵌入對(duì)象中每個(gè)列名的開頭
Data Access Objects (DAOs)(數(shù)據(jù)訪問對(duì)象)
Room中的主要組件是Dao類。DAOs抽象地以一種干凈的方式去訪問數(shù)據(jù)庫(kù)。
Dao可以是接口,也可以是抽象類。如果它是一個(gè)抽象類,那么它可以有一個(gè)構(gòu)造函數(shù),它將一個(gè)RoomDatabase作為它唯一的參數(shù)。
注意:Room不允許在主線程中訪問數(shù)據(jù)庫(kù)除非你在建造器中調(diào)用allowMainThreadQueries(),因?yàn)樗赡荛L(zhǎng)時(shí)間的鎖住UI。異步查詢(返回LiveData或者RxJava流的查詢)是從這個(gè)規(guī)則中豁免的因?yàn)樗鼈儺惒降脑诤笈_(tái)線程中進(jìn)行查詢。
Methods for convenience(便捷方法)
這里有很多你可表示的查詢慣例使用DAO類。這篇文檔包括幾個(gè)通用的例子:
Insert
當(dāng)你創(chuàng)建一個(gè)DAO方法并且使用@Insert
注解它,Room生成一個(gè)在單獨(dú)事務(wù)中插入所有參數(shù)到數(shù)據(jù)庫(kù)中的實(shí)現(xiàn)。
如下代碼展示了幾個(gè)查詢實(shí)例:
@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方法接收只有一個(gè)參數(shù),它可以返回一個(gè)插入item的新rowId 的long值,如果參數(shù)是一個(gè)集合的數(shù)組,它應(yīng)該返回long[]
或者List<Long>
更多細(xì)節(jié),參看文檔 @Insert
注解,和 SQLite documentation for rowid tables
Update
Update 是更新一系列entities集合、給定參數(shù)的慣例方法。它使用query來匹配每個(gè)entity的主鍵。如下代碼說明如何定義這個(gè)方法:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
盡管通常不是必須的,你能夠擁有這個(gè)方法返回int值指示數(shù)據(jù)庫(kù)中更新的數(shù)量。
Delete
Delete是一個(gè)從數(shù)據(jù)庫(kù)中刪除一系列給定參數(shù)的entities的慣例方法。它使用主鍵找到要?jiǎng)h除的entities。如下所示:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
盡管通常不是必須的,你能夠擁有這個(gè)方法返回int值指示數(shù)據(jù)庫(kù)中刪除的數(shù)量。
Methods using @Query(使用@Query)
@Query 是用于DAO類的主要注解。它允許你在數(shù)據(jù)庫(kù)上執(zhí)行讀寫操作。每個(gè)@Query方法都會(huì)在編譯時(shí)驗(yàn)證,因此如果查詢語(yǔ)句有問題,那么編譯時(shí)就會(huì)報(bào)錯(cuò),而不是在運(yùn)行時(shí)發(fā)生。
- 如果僅僅部分成員名相符,則發(fā)出警告
- 如果沒有成員名相符,則發(fā)出錯(cuò)誤
查詢示例:
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
這是載入所有用戶的非常簡(jiǎn)單的查詢例子。在編譯時(shí),Room知道這是查詢user表中的所有列。如果查詢包含語(yǔ)法錯(cuò)誤,或者如果用戶表不存在,Room在你app編譯時(shí)會(huì)報(bào)出合適的錯(cuò)誤消息。
往查詢中傳入?yún)?shù):
大多數(shù)時(shí)間,你需要傳入?yún)?shù)到查詢中去過濾操作,例如只展示比一個(gè)特定年齡大的用戶,為了完成這個(gè)任務(wù),在你的Room注解中使用方法參數(shù),如下所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
當(dāng)編譯時(shí)處理這個(gè)查詢時(shí),,Room將:minAge
和minAge
匹配在一起。Room使用參數(shù)名進(jìn)行匹配,如果匹配不成功,會(huì)在編譯時(shí)報(bào)錯(cuò)。
你也可以通過傳入多個(gè)參數(shù)或者多次引用它們?cè)谝粋€(gè)查詢當(dāng)中,如下所示:
@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);
}
Returning subsets of columns(返回列中的子集)
大多數(shù)時(shí)候,我們只需要一個(gè)entity的部分字段。比如,你的界面也許只需顯示user的first name 和 last name,而不是用戶的每個(gè)詳細(xì)信息。只獲取UI需要的字段可以節(jié)省可觀的資源,查詢也更快。
只要結(jié)果的字段可以和返回的對(duì)象匹配,Room允許返回任何的Java對(duì)象。比如,你可以創(chuàng)建如下的POJO獲取user的first name 和 last name:
public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
現(xiàn)在,你可以使用這個(gè)POJO在你的查詢方法中:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
Room理解查詢返回first_name
和last_name
的列值并且這些值被映射到NameTuple
類的字段中。因此,Room能夠生成合適的代碼。如果查詢返回太多columns
,或者一個(gè)列不存在,Room將會(huì)報(bào)警。
注意:這些POJOs也使用@Embedded注解
Passing a collection of arguments(傳遞參數(shù)集合)
你的部分查詢可能需要你傳入可變數(shù)量的參數(shù),確切數(shù)量的參數(shù)直到運(yùn)行時(shí)才知道。例如,你可能想提取來自某個(gè)地區(qū)所有用戶的信息。Room理解當(dāng)一個(gè)參數(shù)代表一個(gè)集合并且自動(dòng)的在運(yùn)行時(shí)擴(kuò)展它根據(jù)提供的參數(shù)數(shù)量。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
Observable queries(可觀察查詢)
你經(jīng)常希望你的app'sUI自動(dòng)更新當(dāng)數(shù)據(jù)發(fā)生改變。為了實(shí)現(xiàn)這點(diǎn),使用返回值類型為liveData
在你的查詢方法描述中。當(dāng)數(shù)據(jù)庫(kù)被更新,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使用被訪問的table列表在查詢中決定是否更新數(shù)據(jù)對(duì)象。
RxJava
Room也能返回RxJava2 Publisher
和Flowable
對(duì)象從你定義的查詢當(dāng)中。為了使用這個(gè)功能,添加android.arch.persistence.room:rxjava2
到你的build Gradle依賴。你能夠返回Rxjava2定義的對(duì)象,如下所示:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
有關(guān)更多細(xì)節(jié),請(qǐng)參見谷歌開發(fā)者Room and RxJava文章
Direct cursor access(直接游標(biāo)訪問)
如果你的應(yīng)用邏輯直接訪問返回的行,你可以返回一個(gè)Cursor對(duì)象從你的查詢當(dāng)中,如下所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
注意:非常不建議使用Cursor API 因?yàn)樗荒鼙WC行是否存在或者行包含什么值。使用這個(gè)功能僅僅是因?yàn)槟阋呀?jīng)有期望返回一個(gè)cursor的代碼并且你不能輕易的重構(gòu)。
Querying multiple tables(查詢多張表)
你的一些查詢可能訪問多個(gè)表去計(jì)算結(jié)果。Room允許你寫任何查詢,所以你也能連接表格。還有,如果答復(fù)是一個(gè)observable數(shù)據(jù)類型,例如Flowable或者LiveData
,Room監(jiān)視所有被查詢中被引用的無效的表格。
如下代碼段展示如何執(zhí)行一個(gè)表格連接去聯(lián)合當(dāng)前正在借出的書和借的有書的人的信息。
@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從這些查詢當(dāng)中,例如,你可以寫一個(gè)查詢?nèi)パb載user和他們的寵物名稱,如下:
@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;
}
}
Using type converters (使用類型轉(zhuǎn)換)
Room為原始類型和可選的裝箱類型提供嵌入支持。然而,有時(shí)你可能使用一個(gè)單獨(dú)存入數(shù)據(jù)庫(kù)的自定義數(shù)據(jù)類型。為了添加這種類型的支持,你可以提供一個(gè)把自定義類轉(zhuǎn)化為一個(gè)Room能夠持久化的已知類型的TypeConverter。
例如:如果我們想持久化日期的實(shí)例,我們可以寫如下TypeConverter去存儲(chǔ)相等的Unix時(shí)間戳在數(shù)據(jù)庫(kù)中:
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();
}
}
之前的例子定義了兩個(gè)函數(shù),一個(gè)把Date對(duì)象轉(zhuǎn)換為L(zhǎng)ong對(duì)象。另一個(gè)逆向轉(zhuǎn)換,從Long到Date。因?yàn)镽oom已經(jīng)知道了如何持久化Long對(duì)象,它能使用轉(zhuǎn)換器持久化Date類型。
接著,你增加@TypeConverters注解到AppDatabase類為了Room能夠使用你已經(jīng)為每個(gè)entity定義的轉(zhuǎn)換器和DAO
AppDatabase.java
AppDatabase.java
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
使用這些轉(zhuǎn)換器,你可以使用你自定義類型在其他查詢中,就像你使用的原始類型,如下代碼片段所示:
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);
}
您還可以將@typeconverter限制在不同的范圍內(nèi),包含單獨(dú)的entities,DAOs,和DAO methods。更多細(xì)節(jié),請(qǐng)參考@typeconverter
文檔
Database migration(數(shù)據(jù)庫(kù)遷移)
當(dāng)你添加或改變你app的特性,你需要修改你的entity類去反映這些改變。當(dāng)一個(gè)用戶更新你應(yīng)用到最近的版本,你不希望他們丟失已經(jīng)存在的數(shù)據(jù),特別是你無法從遠(yuǎn)程服務(wù)器恢復(fù)數(shù)據(jù)。
Room允許你使用Migration
類保留用戶數(shù)據(jù)以這種方式。每個(gè)Migration
類在運(yùn)行時(shí)指明一個(gè)開始版本和一個(gè)結(jié)束版本,Room執(zhí)行每個(gè)Migration
類的migrate()
方法,使用正確的順序去遷移數(shù)據(jù)庫(kù)到一個(gè)最近版本。
注意:如果你不提供必需的migrations類,Room重建數(shù)據(jù)庫(kù),也就意味你將丟失數(shù)據(jù)庫(kù)中的所有數(shù)據(jù)。
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");
}
};
注意:為了保持你的遷移邏輯與預(yù)期一致,使用完全查詢而不是代表查詢的引用常量。
當(dāng)遷移過程結(jié)束,Room驗(yàn)證schema去保證遷移成功。如果Room發(fā)現(xiàn)問題,它將拋出不匹配異常。
Testing migrations(測(cè)試遷移)
遷移并不是一件簡(jiǎn)單的事情,如果不能正確編寫將會(huì)造成應(yīng)用崩潰。為了保證你應(yīng)用的穩(wěn)定性,你應(yīng)該在提交前測(cè)試你的遷移類。Room提供一個(gè)測(cè)試Maven組件去協(xié)助測(cè)試過程。然而,為了讓這個(gè)組件工作,你需要到處你的數(shù)據(jù)庫(kù)schema。
Exporting schemasI(導(dǎo)出 schemas)
根據(jù)編譯,Room導(dǎo)出你的數(shù)據(jù)庫(kù)Schema到一個(gè)JSON文件中。為了導(dǎo)出schema,設(shè)置 注釋處理器的屬性room.schemaLocation在你的build.gradle文件中,如下所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
你應(yīng)該存儲(chǔ)導(dǎo)出的JSON文件-代表了你數(shù)據(jù)庫(kù)schema的歷史-在你的版本控制系統(tǒng)中,正如它允許創(chuàng)建老版本的數(shù)據(jù)庫(kù)去測(cè)試。
為了測(cè)試這些migrations,添加 android.arch.persistence.room:testing Maven artifac從Room當(dāng)中到你的測(cè)試依賴當(dāng)中,并且把schema 位置當(dāng)做一個(gè)asset文件添加,如下所示:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
測(cè)試package提供一個(gè) 可以讀取這些schema文件的MigrationTestHelper類。它也是Junit4 TestRule類,所以它能管理創(chuàng)建的數(shù)據(jù)庫(kù)。
如下代碼展示了一個(gè)測(cè)試migration的例子:
@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.
}
}
Testing your database(測(cè)試你的數(shù)據(jù)庫(kù))
當(dāng)運(yùn)行你app的測(cè)試時(shí),你不應(yīng)該創(chuàng)建一個(gè)完全的數(shù)據(jù)庫(kù)如果你不測(cè)試數(shù)據(jù)庫(kù)本身。Room允許你輕松的模仿數(shù)據(jù)訪問層在測(cè)試當(dāng)中。這個(gè)過程是可能的因?yàn)槟愕腄AOs不暴漏任何你數(shù)據(jù)庫(kù)的細(xì)節(jié)。當(dāng)測(cè)試你的應(yīng)用,你應(yīng)該創(chuàng)建模仿你的DAO類的假的實(shí)例。
這兒有兩種方式去測(cè)試你的數(shù)據(jù)庫(kù):
- 在你的開發(fā)主機(jī)上
- 在一個(gè)Android設(shè)備上
Testing on your host machine(在你的主機(jī)上測(cè)試)
Room使用SQLite支持庫(kù),這個(gè)支持庫(kù)提供匹配這些Android Framework類的接口并且允許你通過自定義支持庫(kù)實(shí)現(xiàn)去測(cè)試你的數(shù)據(jù)庫(kù)查詢。
即使這個(gè)裝置允許你的測(cè)試運(yùn)行很快,它是不建議的因?yàn)橛脩粼O(shè)備的SQLite版本和可能與host主機(jī)不匹配。
Testing on an Android device(在Android設(shè)備上測(cè)試)
測(cè)試你的數(shù)據(jù)庫(kù)推薦的方法實(shí)現(xiàn)是寫一個(gè)單元測(cè)試在Android設(shè)備上。因?yàn)檫@些測(cè)試不需要?jiǎng)?chuàng)建一個(gè)activity,他講bicentennialUI單元測(cè)試快。
當(dāng)裝置你的測(cè)試用例時(shí),你應(yīng)該創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)的內(nèi)存版本好讓你的測(cè)試更密閉,如下所示:
@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));
}
}
更多關(guān)于測(cè)試數(shù)據(jù)庫(kù)migrations的信息參看 Migration Testing
附加:沒有實(shí)體鍵的對(duì)象引用
從數(shù)據(jù)庫(kù)到對(duì)象間關(guān)系的映射是一個(gè)很常見的實(shí)踐,并且在服務(wù)端運(yùn)行良好,在它們被訪問的時(shí)候進(jìn)行高性能的惰性加載。
但是在客戶端,惰性加載并不可行,這是因?yàn)楹苡锌赡馨l(fā)生在主線程,在主線程查詢磁盤信息會(huì)導(dǎo)致很嚴(yán)重的性能問題。主線程有大概16ms來計(jì)算并繪制一個(gè)Activity的界面更新,因此甚至一個(gè)查詢僅僅耗費(fèi)5ms,你的app仍然會(huì)耗光繪制畫面的時(shí)間,導(dǎo)致顯著的Jank[1]問題。更糟的是,如果有個(gè)并發(fā)運(yùn)行的數(shù)據(jù)庫(kù)事務(wù),或者如果設(shè)備正忙于處理其他磁盤相關(guān)的繁重工作,查詢會(huì)花費(fèi)更多的時(shí)間完成。如果你不使用惰性加載的方式,app會(huì)獲取多余其所需要的數(shù)據(jù),從而導(dǎo)致內(nèi)存消耗的問題。
ORM通常將該問題交給開發(fā)者決定,使得他們可以根據(jù)自己的用例選擇最佳的方式。不幸地是,開發(fā)者通常終止模型和UI之間的共享。當(dāng)UI變更超時(shí)時(shí),問題隨之發(fā)生并且很難預(yù)感和解決。
舉個(gè)例子,UI界面讀取一組Book
列表,每本書擁有一個(gè)Author
對(duì)象。你可能開始會(huì)設(shè)計(jì)你的查詢?nèi)ナ褂枚栊约虞d,從而Book
實(shí)例使用getAuthor()
方法查詢數(shù)據(jù)庫(kù)。過了一些時(shí)間,你意識(shí)到你需要在app的UI界面顯示作者名。你可以添加以下方法:
authorNameTextView.setText(user.getAuthor().getName());
但是這種看似沒有問題的代碼會(huì)導(dǎo)致Author
表在主線程被查詢。
如果你急于查詢作者信息,這會(huì)變得很難去改變數(shù)據(jù)是如何加載的,如果你不再需要這個(gè)數(shù)據(jù)的話,例如當(dāng)你app的UI不再需要顯示關(guān)于特定作者信息的時(shí)候。于是你的app必須繼續(xù)加載不再顯示的信息。這種方式更為糟糕,如果Author
類引用了其他表,例如getBooks()
方法。
由于這些原因,Room禁止實(shí)體間的對(duì)象引用。作為替換,你必須顯式地請(qǐng)求你所需要的數(shù)據(jù)。