系列文章導(dǎo)航:
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(一)App架構(gòu)指南
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(二)將Architecture Components引入工程
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(三)處理生命周期
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(四)LiveData
- 【譯】Google官方推出的Android架構(gòu)組件系列文章(五)ViewModel
- 【譯】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)換憎账。
Room
在SQLite
之上提供一個(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
)的類或接口锄弱。DAO
是Room
的主要組件,負(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)系矛紫,如圖所示:
以下代碼片段包含一個(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
碘菜。如果你使用setter
和getter
裆操,請(qǐng)記住,它們基于Room
的Java 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ì)于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;
}
關(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)
作為一組REMOVE
和REPLACE
操作篮绰,而不是單個(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
符匾、state
和postCode
。為了將這些組合列單獨(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ì)象的表將包含以下名字的列:id
,firstName
垛贤,street
焰坪,state
,city
和post_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曹洽。異步查詢(返回LiveData
或RxJava 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
的方法
@Query是DAO
類中使用的主要注解干跛。可以讓你執(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 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
理解這個(gè)查詢是要返回first_name
和last_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
還能從你定義的查詢中返回RxJava2
的Publisher
和Flowable
對(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ù)類型漓摩,比如Flowable
或LiveData
裙士,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í)行從Long
到Date
的逆向轉(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í)體嘱朽,DAO
和DAO
方法蹭越。更多信息鞋拟,參見@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è)startVersion
和endVersion
。在運(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è)試這些遷移湃崩,添加Room
的android.arch.persistence.room:testing
組件到測(cè)試依賴荧降,然后添加模式位置作為一個(gè)asset文件夾,如下所示:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
測(cè)試包提供一個(gè)MigrationTestHelper類攒读,該類可以讀取這些模式文件誊抛。它也是一個(gè)JUnit4
的TestRule
類,因此它可以管理創(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ù)伍纫。