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

系列文章導(dǎo)航

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

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

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

注意:將Room導(dǎo)入工程识樱,請(qǐng)參考將Architecture Components引入工程

需要處理大量結(jié)構(gòu)化數(shù)據(jù)的應(yīng)用能從本地持久化數(shù)據(jù)中受益匪淺执赡。最常見的使用場(chǎng)景是緩存相關(guān)的數(shù)據(jù)芭商。比如语盈,當(dāng)設(shè)備無法訪問網(wǎng)絡(luò)時(shí),用戶仍然可以在離線時(shí)瀏覽內(nèi)容赘方。當(dāng)設(shè)備重新聯(lián)網(wǎng)后烧颖,任何用戶發(fā)起的內(nèi)容更改將同步到服務(wù)器。

核心框架提供了操作原始SQL內(nèi)容的內(nèi)置支持窄陡。盡管這些API很強(qiáng)大炕淮,但它們相對(duì)較低層,需要大量的時(shí)間和精力才能使用:

  • 沒有對(duì)原始SQL查詢語句的編譯時(shí)驗(yàn)證泳梆。 當(dāng)你的數(shù)據(jù)圖變化時(shí),你需要手動(dòng)更新受影響的SQL查詢語句榜掌。這個(gè)過程可能很耗時(shí)优妙,而且容易出錯(cuò)。
  • 你需要使用大量模板代碼來進(jìn)行SQL語句和Java數(shù)據(jù)對(duì)象的轉(zhuǎn)換憎账。

RoomSQLite之上提供一個(gè)抽象層套硼,來幫助你處理這些問題。

Room包含三大組件:

  • Database:利用這個(gè)組件來創(chuàng)建一個(gè)數(shù)據(jù)庫持有者胞皱。注解定義一系列實(shí)體邪意,類的內(nèi)容定義一系列DAO。它也是底層連接的主入口點(diǎn)反砌。

    注解類應(yīng)該是繼承RoomDatabase的抽象類雾鬼。在運(yùn)行期間,你可以通過調(diào)用Room.databaseBuilder()Room.inMemoryDatabaseBuilder()方法獲取其實(shí)例宴树。

  • Entity:這個(gè)組件表示持有數(shù)據(jù)庫行的類策菜。對(duì)于每個(gè)實(shí)體,將會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫表來持有他們酒贬。你必須通過Database類的entities數(shù)組來引用實(shí)體類又憨。實(shí)體類的中的每個(gè)字段除了添加有@Ignore注解外的,都會(huì)存放到數(shù)據(jù)庫中锭吨。

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

  • DAO: 該組件表示作為數(shù)據(jù)訪問對(duì)象(DAO)的類或接口锄弱。DAORoom的主要組件,負(fù)責(zé)定義訪問數(shù)據(jù)庫的方法耸携。由@Database注解標(biāo)注的類必須包含一個(gè)無參數(shù)且返回使用@Dao注解的類的抽象方法棵癣。當(dāng)在編譯生成代碼時(shí),Room創(chuàng)建該類的實(shí)現(xiàn)夺衍。

注意:通過使用DAO類代替查詢構(gòu)建器或者直接查詢來訪問數(shù)據(jù)庫狈谊,你可以分離數(shù)據(jù)庫架構(gòu)的不同組件。此外沟沙,DAO允許你在測(cè)試應(yīng)用時(shí)輕松地模擬數(shù)據(jù)庫訪問河劝。

這些組件,以及與應(yīng)用程序其他部分的關(guān)系矛紫,如圖所示:

room_architecture.png

以下代碼片段包含一個(gè)數(shù)據(jù)庫配置樣例赎瞎,其包含一個(gè)實(shí)體和一個(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)建的數(shù)據(jù)庫的實(shí)例:

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

注意:實(shí)例化AppDatabase對(duì)象時(shí)务甥,應(yīng)該遵循單例模式,因?yàn)槊總€(gè)RoomDatabase實(shí)例都相當(dāng)昂貴喳篇,而且很少需要訪問多個(gè)實(shí)例敞临。

實(shí)體

當(dāng)一個(gè)類由@Entity注解,并且由@Database注解的entities屬性引用麸澜,Room將在數(shù)據(jù)庫中為其創(chuàng)建一張數(shù)據(jù)庫表挺尿。

默認(rèn),Room會(huì)為實(shí)體類中的每個(gè)字段創(chuàng)建一列炊邦。如果實(shí)體類中包含你不想保存的字段编矾,你可以給他們加上@Ignore注解,如下面代碼片段所示:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

要持久化一個(gè)字段馁害,Room必須能夠訪問它窄俏。你可以將字段設(shè)置為public,或?yàn)樗峁?code>getter和setter碘菜。如果你使用settergetter裆操,請(qǐng)記住,它們基于RoomJava Bean約定炉媒。

主鍵

每個(gè)實(shí)體必須定義至少一個(gè)字段作為主鍵踪区。甚至當(dāng)僅僅只有一個(gè)字段時(shí),你仍然需要為該字段加上@PrimaryKey注解吊骤。另外缎岗,如果你想讓Room為實(shí)體分配自增ID,你可以設(shè)置@PrimaryKey注解的autoGenerate屬性白粉。如果實(shí)體包含組合主鍵传泊,你可以使用@Entity注解的primaryKeys屬性鼠渺,如下面的代碼片段所示:

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

    @Ignore
    Bitmap picture;
}

默認(rèn),Room使用類名作為數(shù)據(jù)庫表名眷细。如果你想讓表采用不同的名字拦盹,設(shè)置@Entity注解的tableName屬性,如下面的代碼片段所示:

@Entity(tableName = "users")
class User {
    ...
}

警告:SQLite中的表名不區(qū)分大小寫溪椎。

tableName屬性類似普舆,Room使用字段名作為數(shù)據(jù)庫中的列名。如果你想要一列采用不同的名字校读,添加@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;
}

索引和唯一約束

根據(jù)訪問數(shù)據(jù)的方式,你可能希望對(duì)數(shù)據(jù)庫中的某些字段進(jìn)行索引歉秫,以加快查詢速度蛾洛。要向?qū)嶓w添加索引,請(qǐng)?jiān)?code>@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;
}

有時(shí),數(shù)據(jù)庫中的某些字段或字段組合必須是唯一的兔甘。你可以通過設(shè)置@Index注解的unique屬性為true來強(qiáng)制滿足唯一屬性谎碍。下面代碼樣例阻止表含有對(duì)于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;
}

關(guān)系

因?yàn)?code>SQLite是關(guān)系型數(shù)據(jù)庫,你可以指定對(duì)象間的關(guān)系裂明。盡管大部分的ORM庫允許實(shí)體對(duì)象互相引用椿浓,但是Room明確禁止此操作太援。更多詳細(xì)信息闽晦,請(qǐng)參考附錄:實(shí)體間無對(duì)象引用

盡管你無法直接使用關(guān)系,Room仍然允許你定義實(shí)體間的外鍵約束提岔。

例如仙蛉,假如有另外一個(gè)叫做Book的實(shí)體,你可以使用@ForeignKey注解來定義它和User實(shí)體的關(guā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)引用的實(shí)體更新時(shí)應(yīng)該怎么處理。比如赛惩,你可以通過在@ForeignKey注解中包含onDelete=CASCADE哀墓,來告訴SQLite如果某個(gè)User實(shí)例被刪除,則刪除該用戶的所有書喷兼。

注意SQLite處理@Insert(onConfilict=REPLACE)作為一組REMOVEREPLACE操作篮绰,而不是單個(gè)UPDATE操作。這個(gè)替換沖突值的方法將會(huì)影響到你的外鍵約束季惯。更多詳細(xì)信息吠各,請(qǐng)參見SQLite文檔ON_CONFLICT語句臀突。

嵌套對(duì)象

有時(shí),你希望將一個(gè)實(shí)體或POJO表達(dá)作為數(shù)據(jù)庫邏輯中的一個(gè)整體贾漏,即使對(duì)象包含了多個(gè)字段候学。在這種情況下,你可以使用@Embeded注解來表示要在表中分為為子字段的對(duì)象纵散。然后梳码,你可以像其他單獨(dú)的列一樣查詢嵌入的字段。

例如困食,我們的User類可以包含一個(gè)類型為Address的字段边翁,其表示了一個(gè)字段組合,包含street硕盹、city符匾、statepostCode。為了將這些組合列單獨(dú)的存放到表中瘩例,將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對(duì)象的表將包含以下名字的列:idfirstName垛贤,street焰坪,statecitypost_code

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

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

數(shù)據(jù)訪問對(duì)象(DAO)

Room的主要組件是Dao類黔漂。DAO以簡(jiǎn)潔的方式抽象了對(duì)于數(shù)據(jù)庫的訪問。

Dao要么是一個(gè)接口禀酱,要么是一個(gè)抽象類炬守。如果它是抽象類,它可以有一個(gè)使用RoomDatabase作為唯一參數(shù)的可選構(gòu)造函數(shù)剂跟。

注意Room不允許在主線程中訪問數(shù)據(jù)庫减途,除非你可以builder上調(diào)用allowMainThreadQueries(),因?yàn)樗赡軙?huì)長(zhǎng)時(shí)間鎖住UI曹洽。異步查詢(返回LiveDataRxJava Flowable的查詢)則不受此影響鳍置,因?yàn)樗鼈冊(cè)谟行枰獣r(shí)異步運(yùn)行在后臺(tái)線程上。

方便的方法

可以使用DAO類來表示多個(gè)方便的查詢送淆。這篇文章包含幾個(gè)常用的例子税产。

插入

當(dāng)你創(chuàng)建一個(gè)DAO方法并用@Insert注解時(shí),Room會(huì)生成一個(gè)在在單獨(dú)事務(wù)中將所有參數(shù)插入到數(shù)據(jù)庫中的實(shí)現(xiàn)。

下面代碼展示幾個(gè)插入樣例:

@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è)long撤卢,表示插入項(xiàng)的新的rowId。如果參數(shù)是一個(gè)數(shù)組或集合梧兼,它應(yīng)該返回long []List<Long>放吩。

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

更新

Update是一個(gè)方便的方法渡紫,用于更新數(shù)據(jù)庫中以參數(shù)給出的一組實(shí)體。它使用與每個(gè)實(shí)體主鍵匹配的查詢考赛。下面代碼片段演示如何定義該方法:

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

雖然通常不是必須的惕澎,但你可以讓此方法返回一個(gè)int值,指示數(shù)據(jù)庫中更新的行數(shù)颜骤。

刪除

Delete是一個(gè)方便的方法唧喉,用于刪除數(shù)據(jù)庫中作為參數(shù)給出的實(shí)體集。使用主鍵來查找要?jiǎng)h除的實(shí)體忍抽。下面代碼演示如何定義此方法:

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

雖然通常不是必須的八孝,但你可以讓此方法返回一個(gè)int值,指示數(shù)據(jù)庫中刪除的行數(shù)鸠项。

使用@Query的方法

@QueryDAO類中使用的主要注解干跛。可以讓你執(zhí)行數(shù)據(jù)庫讀/寫操作祟绊。每個(gè)@Query方法會(huì)在編譯時(shí)驗(yàn)證楼入,因此如果查詢有問題,則會(huì)發(fā)生編譯錯(cuò)誤而不是運(yùn)行時(shí)故障牧抽。

Room還會(huì)驗(yàn)證查詢的返回值嘉熊,以便如果返回對(duì)象中的字段名與查詢相應(yīng)中的相應(yīng)列名不匹配,Room則會(huì)以下面兩種方式的一種提醒你:

  • 如果僅僅某些字段名匹配阎姥,則給出警告
  • 如果沒有字段匹配记舆,則給出錯(cuò)誤鸽捻。

簡(jiǎn)單查詢

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

這是一條非常簡(jiǎn)單的用于加載所有用戶的查詢呼巴。在編譯時(shí),Room知道它是查詢user表的所有列御蒲。如果查詢包含語法錯(cuò)誤衣赶,或者如果user表不存在于數(shù)據(jù)庫,Room會(huì)在應(yīng)用編譯時(shí)厚满,展示相應(yīng)的錯(cuò)誤消息府瞄。

給查詢傳遞參數(shù)

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

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

當(dāng)查詢?cè)诰幾g時(shí)處理時(shí)货邓,Room匹配:minAge綁定參數(shù)和:minAge方法參數(shù)秆撮。Room采用參數(shù)名進(jìn)行匹配。如果沒有匹配成功换况,在應(yīng)用編譯時(shí)則發(fā)生錯(cuò)誤职辨。

你還可以在查詢中傳遞多個(gè)參數(shù)或引用她們多次,如下面代碼所示:

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

返回列的子集

大部分時(shí)間戈二,你僅僅需要獲取實(shí)體的幾個(gè)字段舒裤。比如,你的UI可能展示僅僅是用戶的first name和last name觉吭,而不是用戶的每個(gè)詳細(xì)信息腾供。通過僅獲取應(yīng)用UI上顯示的幾列,你可以節(jié)省寶貴的資源鲜滩,并且更快完成查詢台腥。

Room允許你從查詢中返回任意的java對(duì)象,只要結(jié)果列集能被映射到返回的對(duì)象绒北。比如黎侈,你可以創(chuàng)建下面的POJO來拉取用戶的first namelast 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理解這個(gè)查詢是要返回first_namelast_name列的值闷游,并且這些值可以映射成NameTuple類的字段峻汉。因此,Room可以生成正確的代碼脐往。如果查詢返回太多列休吠,或者有列不存在NameTuple類,Room則顯示一個(gè)警告业簿。

注意:這些POJO也可以使用@Embedded注解

傳遞參數(shù)集合

一些查詢可能要求傳遞一組個(gè)數(shù)變化的參數(shù)瘤礁,指導(dǎo)運(yùn)行時(shí)才知道確切的參數(shù)個(gè)數(shù)。比如梅尤,你可能想要獲取關(guān)于一個(gè)區(qū)域集里面所有用戶的信息柜思。Room理解當(dāng)參數(shù)表示為集合時(shí),會(huì)在運(yùn)行時(shí)基于提供的參數(shù)個(gè)數(shù)自動(dòng)進(jìn)行展開巷燥。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

可觀察的查詢

當(dāng)執(zhí)行查詢時(shí)赡盘,你經(jīng)常希望應(yīng)用程序的UI在數(shù)據(jù)更改時(shí)自動(dòng)更新。為達(dá)到這個(gè)目的缰揪,在查詢方法描述中使用返回LiveData類型的值陨享。Room生成所有必要的代碼,來達(dá)到當(dāng)數(shù)據(jù)更新時(shí)更新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對(duì)象赞厕。

RxJava

Room還能從你定義的查詢中返回RxJava2PublisherFlowable對(duì)象。要使用此功能定硝,請(qǐng)將Room組中的android.arch.persistence.room:rxjava2添加到構(gòu)建Gradle依賴中坑傅。然后,你可以返回RxJava2中定義的類型喷斋,如下面代碼所示:

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

直接光標(biāo)訪問

如果你的應(yīng)用邏輯需要直接訪問返回行唁毒,你可以從查詢中返回一個(gè)Cursor對(duì)象,如下面代碼所示:

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

警告:非常不鼓勵(lì)使用Cursor API星爪,因?yàn)樗鼰o法保證是否行存在浆西,或者行包含什么值。僅當(dāng)你已經(jīng)具有期望使用Cursor的代碼顽腾,并且不能輕易重構(gòu)時(shí)使用近零。

查詢多張表

一些查詢可能要求查詢多張表來計(jì)算結(jié)果。Room允許你寫任何查詢抄肖,因此你還可以連接表久信。此外,如果響應(yīng)是一個(gè)可觀察的數(shù)據(jù)類型漓摩,比如FlowableLiveData裙士,Room會(huì)監(jiān)視查詢中引用的所有無效的表。(Furthermore, if the response is an observable data type, such as Flowable or LiveData, Room watches all tables referenced in the query for invalidation)

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

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

使用類型轉(zhuǎn)換器

Room提供對(duì)于基本類型和其包裝類的內(nèi)置支持。然后卓舵,你有時(shí)候使用打算以單一列存放到數(shù)據(jù)庫中的自定義數(shù)據(jù)類型南用。為了添加對(duì)于這種自定義類型的支持,你可以提供一個(gè)TypeConverter掏湾,它將負(fù)責(zé)處理自定義類和Romm可以保存的已知類型之間的轉(zhuǎn)換裹虫。

比如,如果我們想要保存Date實(shí)例忘巧,我們可以寫下面的TypeConverter來將等價(jià)的Unix時(shí)間戳存放到數(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í)例定義了兩個(gè)函數(shù)恒界,一個(gè)將Date對(duì)象轉(zhuǎn)換成Long對(duì)象睦刃,另一個(gè)則執(zhí)行從LongDate的逆向轉(zhuǎn)換砚嘴。由于Room已經(jīng)知道了如何持久化Long對(duì)象,因此它可以使用這個(gè)轉(zhuǎn)換器來持久化保存Date類型的值。

接下來际长,你將@TypeConverters注解添加到AppDatabase類耸采,以便Room可以使用你在AppDatabase中為每個(gè)實(shí)體和DAO定義的轉(zhuǎn)換器。

AppDatabase.java

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.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);
}

你還可以限制@TypeConverters到不同的作用域如绸,包括單獨(dú)的實(shí)體嘱朽,DAODAO方法蹭越。更多信息鞋拟,參見@TypeConverters的引用文檔。

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

當(dāng)你添加和更改App功能時(shí)姿现,你需要修改實(shí)體類來反映這些更改扼脐。當(dāng)用戶更新到你的應(yīng)用最新版本時(shí)岸军,你不想要他們丟失所有存在的數(shù)據(jù),尤其是你無法從遠(yuǎn)端服務(wù)器恢復(fù)數(shù)據(jù)時(shí)瓦侮。

Room允許你編寫Migration類來保留用戶數(shù)據(jù)艰赞。每個(gè)Migration類指明一個(gè)startVersionendVersion。在運(yùn)行時(shí)肚吏,Room運(yùn)行每個(gè)Migration類的migrate()方法方妖,使用正確的順序來遷移數(shù)據(jù)庫到最新版本。

警告:如果你沒有提供需要的遷移類罚攀,Room將會(huì)重建數(shù)據(jù)庫吁断,也就意味著你會(huì)丟掉數(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ùn)行坞生,請(qǐng)使用完整查詢仔役,而不是引用代表查詢的常量。

在遷移過程完成后是己,Room會(huì)驗(yàn)證模式以確保遷移正確又兵。如果Room發(fā)現(xiàn)問題,將還會(huì)拋出包含不匹配信息的異常卒废。

測(cè)試遷移

遷移并不是簡(jiǎn)單的寫入沛厨,并且一旦無法正確寫入,可能導(dǎo)致應(yīng)用程序循環(huán)崩潰摔认。為了保持應(yīng)用程序的穩(wěn)定性逆皮,你應(yīng)該事先測(cè)試遷移。Room提供了一個(gè)測(cè)試Maven組件來輔助測(cè)試過程参袱。然而电谣,要使這個(gè)組件工作秽梅,你需要導(dǎo)出數(shù)據(jù)庫的模式。

導(dǎo)出數(shù)據(jù)庫模式

匯編后剿牺,Room將你的數(shù)據(jù)庫模式信息導(dǎo)出到一個(gè)JSON文件中企垦。為了導(dǎo)出模式,在build.gradle文件中設(shè)置room.schemaLocation注解處理器屬性晒来,如下所示:

build.gradle

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

你可以將導(dǎo)出的JSON文件(代表了你的數(shù)據(jù)庫模式歷史)保存到你的版本控制系統(tǒng)中钞诡,因?yàn)樗梢宰?code>Room創(chuàng)建舊版本的數(shù)據(jù)庫以進(jìn)行測(cè)試。

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

build.gradle

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

測(cè)試包提供一個(gè)MigrationTestHelper類攒读,該類可以讀取這些模式文件誊抛。它也是一個(gè)JUnit4TestRule類,因此它可以管理創(chuàng)建的數(shù)據(jù)庫整陌。

遷移測(cè)試示例如下所示:

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

測(cè)試數(shù)據(jù)庫

當(dāng)應(yīng)用程序運(yùn)行測(cè)試時(shí)拗窃,如果你沒有測(cè)試數(shù)據(jù)庫本身,則不需要?jiǎng)?chuàng)建一個(gè)完整的數(shù)據(jù)庫泌辫。Room可以讓你在測(cè)試過程中輕松模擬數(shù)據(jù)訪問層随夸。這個(gè)過程是可能的,因?yàn)槟愕腄AO不會(huì)泄漏任何數(shù)據(jù)庫的細(xì)節(jié)震放。當(dāng)測(cè)試應(yīng)用的其余部分時(shí)宾毒,你應(yīng)該創(chuàng)建DAO類的模擬或假的實(shí)例。

有兩種方式測(cè)試數(shù)據(jù)庫:

  • 在你的宿主開發(fā)機(jī)上
  • 在一臺(tái)Android設(shè)備上

在宿主機(jī)上測(cè)試

Room使用SQLite支持庫殿遂,它提供了與Android Framework類相匹配的接口诈铛。該支持允許你傳遞自定義的支持庫實(shí)現(xiàn)來測(cè)試數(shù)據(jù)庫查詢。

即使這些設(shè)置能讓你的測(cè)試運(yùn)行非衬福快幢竹,也不推薦。因?yàn)檫\(yùn)行在你的設(shè)備上的SQLite版本以及用戶設(shè)備上的恩静,可能和你宿主機(jī)上的版本并不匹配焕毫。

在Android設(shè)備上測(cè)試

推薦的測(cè)試數(shù)據(jù)庫實(shí)現(xiàn)的方法是編寫運(yùn)行在Android設(shè)備上的JUnit測(cè)試。因?yàn)檫@些測(cè)試并不需要?jiǎng)?chuàng)建activity驶乾,它們相比UI測(cè)試應(yīng)該是更快執(zhí)行邑飒。

設(shè)置測(cè)試時(shí),你應(yīng)該創(chuàng)建數(shù)據(jù)庫的內(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ù)庫遷移疙咸,參見測(cè)試遷移

附錄:實(shí)體間無對(duì)象引用

將數(shù)據(jù)庫的關(guān)系映射到相應(yīng)的對(duì)象模型是一種常見的做法,在服務(wù)端可以很好地運(yùn)行风科。在服務(wù)端當(dāng)訪問時(shí)撒轮,使用高性能的延遲加載字段乞旦。

然而,在客戶端腔召,延遲加載是不可行的杆查,因?yàn)樗赡馨l(fā)生在UI線程上扮惦,并且在UI線程上查詢磁盤信息會(huì)產(chǎn)生顯著的性能問題臀蛛。UI線程有大約16ms的時(shí)間來計(jì)算以及繪制activity的更新布局,因此即使一個(gè)查詢僅僅耗費(fèi)5ms崖蜜,仍然有可能你的應(yīng)用會(huì)沒有時(shí)間繪制幀浊仆,引發(fā)可見的卡頓。更糟糕的是豫领,如果并行運(yùn)行一個(gè)單獨(dú)的事務(wù)抡柿,或者設(shè)備忙于其他磁盤重任務(wù),則查詢可能需要更多時(shí)間完成等恐。但是洲劣,如果你不使用延遲加載,應(yīng)用獲取比其需要的更多數(shù)據(jù)课蔬,從而造成內(nèi)存消耗問題囱稽。

ORM通常將此決定留給開發(fā)人員,以便他們可以基于應(yīng)用的使用場(chǎng)景來做最好的事情二跋。不幸的是战惊,開發(fā)人員通常最終在他們的應(yīng)用和UI之間共享模型,隨著UI隨著時(shí)間的推移而變化扎即,難以預(yù)料和調(diào)試的問題出現(xiàn)吞获。

舉個(gè)例子,使用加載Book對(duì)象列表的UI谚鄙,每個(gè)Book對(duì)象都有一個(gè)Author對(duì)象各拷。你可能最初設(shè)計(jì)你的查詢使用延遲加載,這樣的話Book實(shí)例使用getAuthor()方法來返回作者闷营。第一次調(diào)用getAuthor()會(huì)調(diào)用數(shù)據(jù)庫查詢撤逢。一段時(shí)間后,你會(huì)意識(shí)到你需要在應(yīng)用UI上顯示作者名字粮坞,你可以輕松添加方法調(diào)用蚊荣,如以下代碼片段所示:

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

然而,這個(gè)看起來無害的修改莫杈,會(huì)導(dǎo)致Author表在主線程被查詢互例。

如果你頻繁的查詢作者信息,如果你不再需要數(shù)據(jù)筝闹,后續(xù)將會(huì)很難更改數(shù)據(jù)的加載方式媳叨,比如你的應(yīng)用UI不再需要展示有關(guān)特定作者的信息的情況腥光。因此,你的應(yīng)用必須繼續(xù)加載并不需要顯示的數(shù)據(jù)糊秆。如果作者類引用另一個(gè)表武福,例如使用getBooks()方法,這種情況會(huì)更糟痘番。

由于這些原因捉片,Room禁止實(shí)體類之間的對(duì)象引用。相反汞舱,你必須顯式請(qǐng)求你的應(yīng)用程序需要的數(shù)據(jù)伍纫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市昂芜,隨后出現(xiàn)的幾起案子莹规,更是在濱河造成了極大的恐慌,老刑警劉巖泌神,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件良漱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡欢际,警方通過查閱死者的電腦和手機(jī)母市,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幼苛,“玉大人窒篱,你說我怎么就攤上這事〔把兀” “怎么了墙杯?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)括荡。 經(jīng)常有香客問我高镐,道長(zhǎng),這世上最難降的妖魔是什么畸冲? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任嫉髓,我火速辦了婚禮,結(jié)果婚禮上邑闲,老公的妹妹穿的比我還像新娘算行。我一直安慰自己,他們只是感情好苫耸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布州邢。 她就那樣靜靜地躺著,像睡著了一般褪子。 火紅的嫁衣襯著肌膚如雪量淌。 梳的紋絲不亂的頭發(fā)上骗村,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音呀枢,去河邊找鬼胚股。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裙秋,可吹牛的內(nèi)容都是我干的琅拌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼残吩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼财忽!你這毒婦竟也來了倘核?” 一聲冷哼從身側(cè)響起泣侮,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎紧唱,沒想到半個(gè)月后活尊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漏益,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年蛹锰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绰疤。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铜犬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轻庆,到底是詐尸還是另有隱情癣猾,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布余爆,位于F島的核電站纷宇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蛾方。R本人自食惡果不足惜像捶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桩砰。 院中可真熱鬧拓春,春花似錦、人聲如沸亚隅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枢步。三九已至沉删,卻和暖如春渐尿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矾瑰。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工砖茸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人殴穴。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓凉夯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親采幌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劲够,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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