Room Persistence Library(官網(wǎng)文檔翻譯)

官方原文地址

Room持久化庫

Room為SQLite提供一個抽象層蝌以,在充分利用SQLite的同時哪替,允許流暢的數(shù)據(jù)庫訪問

注意:引入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ò)的時候,用戶仍然可以瀏覽內(nèi)容帅霜。任何用戶發(fā)起的內(nèi)容改變在設(shè)備恢復(fù)網(wǎng)絡(luò)的時候同步到服務(wù)器上匆背。

核心框架對raw SQL內(nèi)容提供嵌入支持。盡管這些APIs是很給力的身冀,但是他們相當(dāng)?shù)图壊⑶倚枰罅康臅r間和精力去使用:

  • raw SQL查詢沒有編譯時驗證钝尸。當(dāng)你的數(shù)據(jù)圖改變,你需要手動的更新受影響的SQL查詢搂根。這個過程是耗時的和容易出錯的珍促。
  • 你需要使用大量的樣板代碼在數(shù)據(jù)查詢和java數(shù)據(jù)對象之間轉(zhuǎn)換

Room為你處理這些問題。在Room中有三個主要組件剩愧。

  • Database(數(shù)據(jù)庫): 你可以使用這個組件創(chuàng)建一個數(shù)據(jù)庫holder猪叙。注解定義了一系列entities并且類的內(nèi)容提供了一系列DAOs,它也是下層的主要連接 的訪問點仁卷。
    注解的類應(yīng)該是一個抽象的繼承 RoomDatabase的類穴翩。在運行時,你能獲得一個實例通過調(diào)用Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()

  • Entity(實體):這個組件代表了一個持有數(shù)據(jù)行的類锦积。對于每個entity芒帕,一個數(shù)據(jù)庫表被創(chuàng)建用于持有items。你必須引用entity類通過Database類中的entities數(shù)組丰介。每個entity字段被持久化到數(shù)據(jù)庫中除非你注解它通過@Ignore.

注意:Entities能夠有一個空的構(gòu)造函數(shù)(如果dao類能夠訪問每個持久化的字段)或者一個參數(shù)帶有匹配entity中的字段的類型和名稱的構(gòu)造函數(shù)背蟆,例如一個只接收其中一些字段的構(gòu)造函數(shù)鉴分。

  • DAO(數(shù)據(jù)訪問對象):這個組件代表了一個類或者接口作為DAO。DAOs 是Room中的主要組件淆储,并且負責(zé)定義訪問數(shù)據(jù)庫的方法冠场。被注解為@Database的類必須包含一個沒有參數(shù)的抽象方法并且返回注解為@Dao的類家浇。當(dāng)在編譯時生成代碼本砰,Room創(chuàng)建一個這個類的實現(xiàn)。

注意:使用DAO類訪問數(shù)據(jù)庫而不是query builders或者直接查詢钢悲。你可以把數(shù)據(jù)庫分成幾個組件点额。還有,DAOs允許你輕松的模擬數(shù)據(jù)庫訪問當(dāng)你測試你的應(yīng)用的時候莺琳。

這些組件和rest app的關(guān)系还棱,如圖:

room 架構(gòu)圖

如下代碼片段包含一個數(shù)據(jù)庫配置的例子、一個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();
}

通過創(chuàng)建以上文件珍手,你可以使用如下代碼創(chuàng)建一個數(shù)據(jù)庫實例:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

注意:你必須遵守單例模式當(dāng)初始化一個AppDatabase對象,因為每個RoomDatabase實例是相當(dāng)昂貴的辞做,并且你幾乎不需要訪問多個實例琳要。

Entities(實體)

當(dāng)一個類被注解為@Entity并且引用到帶有@Database 注解的entities屬性,Room為這個數(shù)據(jù)庫做的entity創(chuàng)建一個數(shù)據(jù)表秤茅。

默認情況下稚补,Room為每個定義在entity中的字段創(chuàng)建一個列。如果一個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的慣例润绎。

Primary Key(主鍵)

每個entity必須至少定義一個field作為主鍵(primary key)。即使只有一個field匙监,你也必須用@PrimaryKey注釋這個field凡橱。如果你想讓Room為entity設(shè)置自增ID,你可以設(shè)置@PrimaryKey的autoGenerate屬性亭姥。

@Entity(tableName = "user")
public class User {
    @PrimaryKey(autoGenerate = true)
     private  Integer id;
     ...
}

如果你的entity有一個組合主鍵稼钩,你可以使用@Entity注解的primaryKeys屬性,具體用法如下:

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

    @Ignore
    Bitmap picture;
}

Room默認把類名作為數(shù)據(jù)庫的表名达罗。如果你想用其它的名稱坝撑,使用@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;
}

Indices and uniqueness(索引和唯一性)

為了提高查詢的效率殊橙,你可能想為特定的字段建立索引。要為一個entity添加索引狱从,在@Entity注解中添加indices屬性膨蛮,列出你想放在索引或者組合索引中的字段。下面的代碼片段演示了這個注解的過程:

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

有時候季研,某個字段或者幾個字段必須是唯一的敞葛。你可以通過把@Index注解的unique屬性設(shè)置為true來實現(xiàn)唯一性。下面的代碼防止了一個表中的兩行數(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)系)

因為SQLite是關(guān)系數(shù)據(jù)庫与涡,你可以指定對象之間的關(guān)聯(lián)惹谐。雖然大多數(shù)ORM庫允許entity對象相互引用,但是Room明確禁止了這種行為驼卖。更多細節(jié)請參考 Addendum: No object references between entities.

雖然不可以使用直接的關(guān)聯(lián)氨肌,Room仍然允許你定義entity之間的外鍵(Foreign Key)約束。

比如款慨,假設(shè)有另外一個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;
}

外鍵非常強大檩奠,因為它允許你指定當(dāng)被關(guān)聯(lián)的entity更新時做什么操作桩了。例如,通過在@ForeignKey注解中包含Delete = CASCADE埠戳, 你可以告訴SQLite井誉,如果相應(yīng)的User實例被刪除,那么刪除這個User下的所有book整胃。

注意:SQLite處理@Insert(OnConflict=REPLACE) 作為一個REMOVEREPLACE操作而不是單獨的UPDATE操作颗圣。這個替換沖突值的方法能夠影響你的外鍵約束。更多細節(jié)屁使,參看 SQLite documentation在岂。

Nested objects(內(nèi)嵌對象)

有時,你希望entity或者POJOs作為一個整體在你數(shù)據(jù)庫的邏輯當(dāng)中蛮寂,即使對象包含幾個字段蔽午。在這種情況下,你可以使用@Embedded注解去代表一個你希望分解成一個表中的次級字段的對象酬蹋。接著你就可以查詢嵌入字段就像其他單獨的字段那樣及老。

例如抽莱,我們的user類能夠包含一個代表了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表示了一個包含如下名稱列的User對象:id,firstName,street,state,city和post_code。

注意:嵌入字段也包括其他嵌入字段

如果一個字段有多個同一類型的嵌入字段僧鲁,你能保持每個列是獨一無二的通過設(shè)置prefix屬性虐呻。Room然后將所提供的值添加到嵌入對象中每個列名的開頭

Data Access Objects (DAOs)(數(shù)據(jù)訪問對象)

Room中的主要組件是Dao類。DAOs抽象地以一種干凈的方式去訪問數(shù)據(jù)庫悔捶。

Dao可以是接口铃慷,也可以是抽象類单芜。如果它是一個抽象類蜕该,那么它可以有一個構(gòu)造函數(shù),它將一個RoomDatabase作為它唯一的參數(shù)洲鸠。

注意:Room不允許在主線程中訪問數(shù)據(jù)庫除非你在建造器中調(diào)用allowMainThreadQueries()堂淡,因為它可能長時間的鎖住UI。異步查詢(返回LiveData或者RxJava流的查詢)是從這個規(guī)則中豁免的因為它們異步的在后臺線程中進行查詢扒腕。

Methods for convenience(便捷方法)

這里有很多你可表示的查詢慣例使用DAO類绢淀。這篇文檔包括幾個通用的例子:

Insert

當(dāng)你創(chuàng)建一個DAO方法并且使用@Insert注解它,Room生成一個在單獨事務(wù)中插入所有參數(shù)到數(shù)據(jù)庫中的實現(xiàn)瘾腰。
如下代碼展示了幾個查詢實例:

@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方法接收只有一個參數(shù)皆的,它可以返回一個插入item的新rowId 的long值,如果參數(shù)是一個集合的數(shù)組蹋盆,它應(yīng)該返回long[]或者List<Long>

更多細節(jié)费薄,參看文檔 @Insert
注解,和 SQLite documentation for rowid tables

Update

Update 是更新一系列entities集合栖雾、給定參數(shù)的慣例方法楞抡。它使用query來匹配每個entity的主鍵。如下代碼說明如何定義這個方法:

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

盡管通常不是必須的析藕,你能夠擁有這個方法返回int值指示數(shù)據(jù)庫中更新的數(shù)量召廷。

Delete

Delete是一個從數(shù)據(jù)庫中刪除一系列給定參數(shù)的entities的慣例方法。它使用主鍵找到要刪除的entities账胧。如下所示:

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

盡管通常不是必須的竞慢,你能夠擁有這個方法返回int值指示數(shù)據(jù)庫中刪除的數(shù)量。

Methods using @Query(使用@Query)

@Query 是用于DAO類的主要注解治泥。它允許你在數(shù)據(jù)庫上執(zhí)行讀寫操作筹煮。每個@Query方法都會在編譯時驗證,因此如果查詢語句有問題车摄,那么編譯時就會報錯寺谤,而不是在運行時發(fā)生仑鸥。

  • 如果僅僅部分成員名相符,則發(fā)出警告
  • 如果沒有成員名相符变屁,則發(fā)出錯誤
查詢示例:
@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

這是載入所有用戶的非常簡單的查詢例子眼俊。在編譯時,Room知道這是查詢user表中的所有列粟关。如果查詢包含語法錯誤疮胖,或者如果用戶表不存在,Room在你app編譯時會報出合適的錯誤消息闷板。

往查詢中傳入?yún)?shù):

大多數(shù)時間澎灸,你需要傳入?yún)?shù)到查詢中去過濾操作,例如只展示比一個特定年齡大的用戶遮晚,為了完成這個任務(wù)性昭,在你的Room注解中使用方法參數(shù),如下所示:

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

當(dāng)編譯時處理這個查詢時县遣,糜颠,Room將:minAgeminAge匹配在一起。Room使用參數(shù)名進行匹配萧求,如果匹配不成功其兴,會在編譯時報錯。

你也可以通過傳入多個參數(shù)或者多次引用它們在一個查詢當(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ù)時候元旬,我們只需要一個entity的部分字段。比如守问,你的界面也許只需顯示user的first name 和 last name匀归,而不是用戶的每個詳細信息。只獲取UI需要的字段可以節(jié)省可觀的資源酪碘,查詢也更快朋譬。

只要結(jié)果的字段可以和返回的對象匹配,Room允許返回任何的Java對象兴垦。比如徙赢,你可以創(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)在,你可以使用這個POJO在你的查詢方法中:

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

Room理解查詢返回first_namelast_name的列值并且這些值被映射到NameTuple類的字段中探越。因此狡赐,Room能夠生成合適的代碼。如果查詢返回太多columns钦幔,或者一個列不存在枕屉,Room將會報警。

注意:這些POJOs也使用@Embedded注解

Passing a collection of arguments(傳遞參數(shù)集合)

你的部分查詢可能需要你傳入可變數(shù)量的參數(shù)鲤氢,確切數(shù)量的參數(shù)直到運行時才知道搀擂。例如西潘,你可能想提取來自某個地區(qū)所有用戶的信息。Room理解當(dāng)一個參數(shù)代表一個集合并且自動的在運行時擴展它根據(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)數(shù)據(jù)發(fā)生改變喷市。為了實現(xiàn)這點,使用返回值類型為liveData在你的查詢方法描述中威恼。當(dāng)數(shù)據(jù)庫被更新,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ù)對象箫措。

RxJava

Room也能返回RxJava2 PublisherFlowable對象從你定義的查詢當(dāng)中腹备。為了使用這個功能,添加android.arch.persistence.room:rxjava2 到你的build Gradle依賴斤蔓。你能夠返回Rxjava2定義的對象植酥,如下所示:

@Dao
public interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    public Flowable<User> loadUserById(int id);
}

有關(guān)更多細節(jié),請參見谷歌開發(fā)者Room and RxJava文章

Direct cursor access(直接游標訪問)

如果你的應(yīng)用邏輯直接訪問返回的行附迷,你可以返回一個Cursor對象從你的查詢當(dāng)中惧互,如下所示:

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

注意:非常不建議使用Cursor API 因為它不能保證行是否存在或者行包含什么值。使用這個功能僅僅是因為你已經(jīng)有期望返回一個cursor的代碼并且你不能輕易的重構(gòu)喇伯。

Querying multiple tables(查詢多張表)

你的一些查詢可能訪問多個表去計算結(jié)果。Room允許你寫任何查詢拨与,所以你也能連接表格稻据。還有,如果答復(fù)是一個observable數(shù)據(jù)類型买喧,例如Flowable或者LiveData捻悯,Room監(jiān)視所有被查詢中被引用的無效的表格。

如下代碼段展示如何執(zhí)行一個表格連接去聯(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)中今缚,例如,你可以寫一個查詢?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ù)據(jù)庫的自定義數(shù)據(jù)類型蔗蹋。為了添加這種類型的支持何荚,你可以提供一個把自定義類轉(zhuǎn)化為一個Room能夠持久化的已知類型的TypeConverter。

例如:如果我們想持久化日期的實例猪杭,我們可以寫如下TypeConverter去存儲相等的Unix時間戳在數(shù)據(jù)庫中:

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

之前的例子定義了兩個函數(shù)餐塘,一個把Date對象轉(zhuǎn)換為Long對象。另一個逆向轉(zhuǎn)換皂吮,從Long到Date戒傻。因為Room已經(jīng)知道了如何持久化Long對象税手,它能使用轉(zhuǎn)換器持久化Date類型。

接著需纳,你增加@TypeConverters注解到AppDatabase類為了Room能夠使用你已經(jīng)為每個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)熙暴,包含單獨的entities,DAOs,和DAO methods慌盯。更多細節(jié)周霉,請參考@typeconverter
文檔

Database migration(數(shù)據(jù)庫遷移)

當(dāng)你添加或改變你app的特性,你需要修改你的entity類去反映這些改變亚皂。當(dāng)一個用戶更新你應(yīng)用到最近的版本俱箱,你不希望他們丟失已經(jīng)存在的數(shù)據(jù),特別是你無法從遠程服務(wù)器恢復(fù)數(shù)據(jù)灭必。

Room允許你使用Migration類保留用戶數(shù)據(jù)以這種方式狞谱。每個Migration類在運行時指明一個開始版本和一個結(jié)束版本,Room執(zhí)行每個Migration類的migrate()方法禁漓,使用正確的順序去遷移數(shù)據(jù)庫到一個最近版本跟衅。

注意:如果你不提供必需的migrations類,Room重建數(shù)據(jù)庫播歼,也就意味你將丟失數(shù)據(jù)庫中的所有數(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驗證schema去保證遷移成功。如果Room發(fā)現(xiàn)問題烁试,它將拋出不匹配異常雇初。

Testing migrations(測試遷移)

遷移并不是一件簡單的事情,如果不能正確編寫將會造成應(yīng)用崩潰减响。為了保證你應(yīng)用的穩(wěn)定性靖诗,你應(yīng)該在提交前測試你的遷移類。Room提供一個測試Maven組件去協(xié)助測試過程辩蛋。然而呻畸,為了讓這個組件工作,你需要到處你的數(shù)據(jù)庫schema悼院。

Exporting schemasI(導(dǎo)出 schemas)

根據(jù)編譯伤为,Room導(dǎo)出你的數(shù)據(jù)庫Schema到一個JSON文件中。為了導(dǎo)出schema,設(shè)置 注釋處理器的屬性room.schemaLocation在你的build.gradle文件中绞愚,如下所示:

build.gradle

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

你應(yīng)該存儲導(dǎo)出的JSON文件-代表了你數(shù)據(jù)庫schema的歷史-在你的版本控制系統(tǒng)中叙甸,正如它允許創(chuàng)建老版本的數(shù)據(jù)庫去測試。

為了測試這些migrations位衩,添加 android.arch.persistence.room:testing Maven artifac從Room當(dāng)中到你的測試依賴當(dāng)中裆蒸,并且把schema 位置當(dāng)做一個asset文件添加,如下所示:

build.gradle

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

測試package提供一個 可以讀取這些schema文件的MigrationTestHelper類糖驴。它也是Junit4 TestRule類僚祷,所以它能管理創(chuàng)建的數(shù)據(jù)庫。

如下代碼展示了一個測試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(測試你的數(shù)據(jù)庫)

當(dāng)運行你app的測試時贮缕,你不應(yīng)該創(chuàng)建一個完全的數(shù)據(jù)庫如果你不測試數(shù)據(jù)庫本身辙谜。Room允許你輕松的模仿數(shù)據(jù)訪問層在測試當(dāng)中。這個過程是可能的因為你的DAOs不暴漏任何你數(shù)據(jù)庫的細節(jié)感昼。當(dāng)測試你的應(yīng)用装哆,你應(yīng)該創(chuàng)建模仿你的DAO類的假的實例。

這兒有兩種方式去測試你的數(shù)據(jù)庫:

  • 在你的開發(fā)主機上
  • 在一個Android設(shè)備上

Testing on your host machine(在你的主機上測試)

Room使用SQLite支持庫定嗓,這個支持庫提供匹配這些Android Framework類的接口并且允許你通過自定義支持庫實現(xiàn)去測試你的數(shù)據(jù)庫查詢蜕琴。

即使這個裝置允許你的測試運行很快,它是不建議的因為用戶設(shè)備的SQLite版本和可能與host主機不匹配宵溅。

Testing on an Android device(在Android設(shè)備上測試)

測試你的數(shù)據(jù)庫推薦的方法實現(xiàn)是寫一個單元測試在Android設(shè)備上凌简。因為這些測試不需要創(chuàng)建一個activity,他講bicentennialUI單元測試快层玲。

當(dāng)裝置你的測試用例時号醉,你應(yīng)該創(chuàng)建一個數(shù)據(jù)庫的內(nèi)存版本好讓你的測試更密閉,如下所示:

@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)于測試數(shù)據(jù)庫migrations的信息參看 Migration Testing

附加:沒有實體鍵的對象引用

從數(shù)據(jù)庫到對象間關(guān)系的映射是一個很常見的實踐辛块,并且在服務(wù)端運行良好,在它們被訪問的時候進行高性能的惰性加載铅碍。

但是在客戶端润绵,惰性加載并不可行,這是因為很有可能發(fā)生在主線程胞谈,在主線程查詢磁盤信息會導(dǎo)致很嚴重的性能問題尘盼。主線程有大概16ms來計算并繪制一個Activity的界面更新,因此甚至一個查詢僅僅耗費5ms烦绳,你的app仍然會耗光繪制畫面的時間卿捎,導(dǎo)致顯著的Jank[1]問題。更糟的是径密,如果有個并發(fā)運行的數(shù)據(jù)庫事務(wù),或者如果設(shè)備正忙于處理其他磁盤相關(guān)的繁重工作,查詢會花費更多的時間完成底桂。如果你不使用惰性加載的方式植袍,app會獲取多余其所需要的數(shù)據(jù),從而導(dǎo)致內(nèi)存消耗的問題籽懦。

ORM通常將該問題交給開發(fā)者決定于个,使得他們可以根據(jù)自己的用例選擇最佳的方式。不幸地是暮顺,開發(fā)者通常終止模型和UI之間的共享厅篓。當(dāng)UI變更超時時,問題隨之發(fā)生并且很難預(yù)感和解決捶码。

舉個例子羽氮,UI界面讀取一組Book列表,每本書擁有一個Author對象宙项。你可能開始會設(shè)計你的查詢?nèi)ナ褂枚栊约虞d乏苦,從而Book實例使用getAuthor()方法查詢數(shù)據(jù)庫。過了一些時間尤筐,你意識到你需要在app的UI界面顯示作者名汇荐。你可以添加以下方法:

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

但是這種看似沒有問題的代碼會導(dǎo)致Author表在主線程被查詢。

如果你急于查詢作者信息盆繁,這會變得很難去改變數(shù)據(jù)是如何加載的掀淘,如果你不再需要這個數(shù)據(jù)的話,例如當(dāng)你app的UI不再需要顯示關(guān)于特定作者信息的時候油昂。于是你的app必須繼續(xù)加載不再顯示的信息革娄。這種方式更為糟糕,如果Author類引用了其他表冕碟,例如getBooks()方法拦惋。

由于這些原因,Room禁止實體間的對象引用安寺。作為替換厕妖,你必須顯式地請求你所需要的數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挑庶,一起剝皮案震驚了整個濱河市言秸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌迎捺,老刑警劉巖举畸,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異凳枝,居然都是意外死亡抄沮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來合是,“玉大人了罪,你說我怎么就攤上這事〈先” “怎么了泊藕?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長难礼。 經(jīng)常有香客問我娃圆,道長,這世上最難降的妖魔是什么蛾茉? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任讼呢,我火速辦了婚禮,結(jié)果婚禮上谦炬,老公的妹妹穿的比我還像新娘悦屏。我一直安慰自己,他們只是感情好键思,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布础爬。 她就那樣靜靜地躺著,像睡著了一般吼鳞。 火紅的嫁衣襯著肌膚如雪看蚜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天赔桌,我揣著相機與錄音供炎,去河邊找鬼。 笑死疾党,一個胖子當(dāng)著我的面吹牛音诫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雪位,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纽竣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了茧泪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤聋袋,失蹤者是張志新(化名)和其女友劉穎队伟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體幽勒,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡嗜侮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锈颗。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡顷霹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出击吱,到底是詐尸還是另有隱情淋淀,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布覆醇,位于F島的核電站朵纷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏永脓。R本人自食惡果不足惜袍辞,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望常摧。 院中可真熱鬧搅吁,春花似錦、人聲如沸落午。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽板甘。三九已至党瓮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盐类,已是汗流浹背寞奸。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工迄薄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留湃缎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓史飞,卻偏偏與公主長得像猫妙,于是被迫代替她去往敵國和親瓷翻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容