相關(guān)文章:
- 【翻譯】安卓架構(gòu)組件(1)-App架構(gòu)指導(dǎo)
- 【翻譯】安卓架構(gòu)組件(2)-添加組件到你的項目中
- 【翻譯】安卓架構(gòu)組件(3)-處理生命周期
- 【翻譯】安卓架構(gòu)組件(4)-LiveData
- 【翻譯】安卓架構(gòu)組件(5)-ViewModel
- 【翻譯】安卓架構(gòu)組件(7)-分頁庫
Room為SQLite提供了一個抽象層,使得可以流暢使用SQLite的所有功能淮野。
處理大量結(jié)構(gòu)化數(shù)據(jù)的app可以從本地數(shù)據(jù)持久化中獲取巨大利益蠢沿。最常見的用例是緩存相關(guān)的數(shù)據(jù)。在這種情況下刚陡,當(dāng)設(shè)備無法訪問網(wǎng)絡(luò)的時候,用戶仍然可以在離線時瀏覽內(nèi)容株汉。任何用戶原始數(shù)據(jù)的變化都會在連接網(wǎng)絡(luò)后同步筐乳。
核心框架提供了原生SQL的支持。盡管這些API很強大乔妈,但是比較底層并且需要花費大量的時間和努力去使用:
- 沒有原生SQL查詢語句的編譯時驗證蝙云。當(dāng)你的數(shù)據(jù)結(jié)構(gòu)變化時,你需要手動更新受影響的SQL褒翰。這個過程會花費大量的時間并且很容易錯誤頻出贮懈。
Room考慮到了這些匀泊,提供了SQLite的抽象層优训。
Room有三個主要的組件:
- 數(shù)據(jù)庫(Database):你可以使用該組件創(chuàng)建數(shù)據(jù)庫的持有者。該注解定義了實體列表各聘,該類的內(nèi)容定義了數(shù)據(jù)庫中的DAO列表揣非。這也是訪問底層連接的主要入口點。注解類應(yīng)該是抽象的并且擴展自
RoomDatabase
躲因。在運行時早敬,你可以通過調(diào)用Room.databaseBuilder()
或者Room.inMemoryDatabaseBuilder()
獲取實例。 - 實體(Entity):這個組件代表了持有數(shù)據(jù)庫表記錄的類大脉。對每種實體來說搞监,創(chuàng)建了一個數(shù)據(jù)庫表來持有所有項。你必須通過
Database
中的entities
數(shù)組來引用實體類镰矿。實體的每個成員變量都被持久化在數(shù)據(jù)庫中琐驴,除非你注解其為@Ignore
。
實體類可以擁有無參數(shù)構(gòu)造函數(shù)(如果DAO類可以訪問每個持久化成員變量)或者擁有和實體類成員變量匹配參數(shù)的構(gòu)造函數(shù)。Room也可以使用全部或者部分構(gòu)造函數(shù)绝淡,例如只接收部分成員變量的構(gòu)造函數(shù)宙刘。
- 數(shù)據(jù)訪問對象(DAO):這個組件代表了作為DAO的類或者接口。DAO是Room的主要組件牢酵,負(fù)責(zé)定義訪問數(shù)據(jù)庫的方法悬包。被注解
@Database
的類必須包含一個無參數(shù)的抽象方法并返回被@Dao
注解的類型。當(dāng)編譯時生成代碼時馍乙,Room會創(chuàng)建該類的實現(xiàn)布近。
通過使用DAO類訪問數(shù)據(jù)庫而不是查詢構(gòu)建器或直接查詢,你可以將數(shù)據(jù)庫架構(gòu)的不同組件分離丝格。此外吊输,DAO允許你在測試時很容易地模擬數(shù)據(jù)訪問。
這些組件和app的其他部分關(guān)系圖如下:
(img)
下面的代碼片段包含了簡單的數(shù)據(jù)庫配置铁追,含有1個實體和一個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 Setters(實際代碼中不可省略)
}
//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
對象時,你可以遵循單例設(shè)計模式琅束,因為每個RoomDatabase
實例代價是非常昂貴的扭屁,并且你幾乎不需要訪問多個實例。
實體
當(dāng)一個類被@Entity
注解涩禀,并被@Database
注解的entities
屬性引用時料滥,Room為這個實體在數(shù)據(jù)庫中創(chuàng)建一個表。
默認(rèn)情況艾船,Room為實體類的每個成員變量創(chuàng)建一個列葵腹。如果一個實體類的某個成員變量不想被持久化,你可以使用Ignore
注解標(biāo)記屿岂,如:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;//不進行持久化
}
為了持久化成員變量践宴,Room必須可以訪問它。你可以使成員變量是公共的爷怀,或者提供getter和setter方法阻肩。如果你使用getter/setter方法,請記住它們在Room中遵循Java Beans的概念运授。
主鍵
每個實體必須至少定義一個成員變量作為主鍵烤惊。甚至僅僅有一個成員變量,也要標(biāo)記其為@PrimaryKey
吁朦。同時柒室,如果你想要Room指定ID自增,你可以設(shè)置@Primary
的autoGenerate
屬性逗宜。如果實體的主鍵是綜合的雄右,你可以使用@Entity
的primaryKeys
屬性剥啤,如:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
默認(rèn)情況下,Room使用類名作為數(shù)據(jù)庫表的表名不脯。如果你想要數(shù)據(jù)庫表有一個其他的名字府怯,設(shè)置@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;
}
索引與唯一
取決于你如何訪問數(shù)據(jù)复局,你可能想要索引確切的字段以加速數(shù)據(jù)的查詢冲簿。為了向?qū)嶓w添加索引,在@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è)置@Index
的屬性unique
為true
:
@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)系
因為SQLite是一個關(guān)系型數(shù)據(jù)庫,你可以指定對象間的關(guān)系角钩。即使大多數(shù)ORM類庫允許實體對象互相引用吝沫,Room則顯式地禁止了這一點。
即使你不能直接使用關(guān)系映射递礼,Room仍然允許你去定義實體鍵的外鍵約束惨险。
例如,有另一個叫做Book
的實體脊髓,你可以通過使用@ForeignKey
注解定義其和User
實體的關(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;
}
外鍵是非常強大的,因為它們允許你指定引用實體更新時會發(fā)生什么将硝、例如恭朗,你可以告訴SQLite去刪除所有的書籍,如果這些書所對應(yīng)的User
被刪除并且指定了@ForeignKey
的屬性onDelete=CASCADE
依疼。
SQLite將
@Insert(OnConfilct=REPLACE)
處理為REMOVE
和REPLACE
的集合而不僅僅是更新操作痰腮。這個替換沖突值的方法可能會對你的外鍵約束起作用。
內(nèi)嵌對象
有些時候你想要一個實體類或POJO類作為數(shù)據(jù)庫邏輯的一部分涛贯。這種情況下诽嘉,你可以使用@Embedded
注解來蔚出。你可以查詢內(nèi)嵌成員弟翘,就像你可能查詢其他字段一樣。
例如骄酗,我們的User
類包含一個Address
類型的成員稀余,代表了street
、city
趋翻、state
和postCode
睛琳。為了分別存儲這些字段,在User
類中包含一個Address
成員并標(biāo)記為@Embedded
,如:
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
寒屯。
內(nèi)嵌成員也可以含有其他內(nèi)嵌成員
如果一個實體含有多種相同類型的內(nèi)嵌成員,你可以通過設(shè)置prefix
屬性保持每個字段的唯一性黍少。Room之后添加提供的值到每個內(nèi)嵌對象的起始位置寡夹。
數(shù)據(jù)訪問對象(DAO)
Room的主要組件是Dao
類。DAO以清晰的方式抽象除了訪問數(shù)據(jù)庫的行為厂置。
Room不允許在主線程方位數(shù)據(jù)庫菩掏,除非你在Builder調(diào)用
allowMainThreadQueries()
,因為這可能會導(dǎo)致UI被鎖住昵济。而異步查詢則不受此約束智绸,因為異步調(diào)用在后臺線程運行查詢工作。
便捷方法
有很多可以使用DAO類的便捷查詢方法访忿,例如:
Insert
當(dāng)你創(chuàng)建一個DAO方法并標(biāo)記其為@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ù),它會返回long醉顽,表示新插入項的row Id沼溜。如果參數(shù)是數(shù)組或集合,它會返回long[]
或者List<Long>
游添。
Update
Update是更新一組實體的便捷方法系草。它查詢匹配主鍵的記錄然后更新。如:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
盡管通常并不需要如此唆涝,你可以讓該方法返回一個int
值找都,表示更新至數(shù)據(jù)庫的行號。
Delete
Delete是刪除一組實體的便捷方法廊酣。它使用主鍵去尋找記錄并刪除:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
同上能耻,你可以讓該方法返回一個int
值表示被刪除的行號。
使用@Query
@Query
是用于DAO類的主要注解亡驰。它允許你在數(shù)據(jù)庫上執(zhí)行讀寫操作晓猛。每個Query
方法都會在編譯時驗證,因此如果查詢語句有問題凡辱,那么編譯時就會報錯戒职,而不是在運行時發(fā)生。
Room同樣驗證查詢的返回值透乾,如果返回對象的成員名和字段名不一致洪燥,Room會以以下兩種方式警告:
- 如果僅僅部分成員名相符磕秤,則發(fā)出警告
- 如果沒有成員名相符,則發(fā)出錯誤
簡單查詢
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
這是一個加載所有用戶的簡單查詢捧韵。在編譯時市咆,Room知道這是查詢用戶表的所有字段。如果查詢語句含有語法錯誤再来,或者用戶表在數(shù)據(jù)庫中并不存在床绪,Room會顯示相應(yīng)的錯誤。
查詢中傳遞參數(shù)
大多數(shù)時候其弊,你需要在查詢中傳遞參數(shù)來執(zhí)行過濾操作癞己,例如僅僅顯示具體年齡的用戶。為了完成這個任務(wù)梭伐,在你的Room注解中使用方法參數(shù)痹雅,如:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
當(dāng)編譯時處理這個查詢時,糊识,Room將:minAge
和minAge
匹配在一起绩社。Room使用參數(shù)名進行匹配,如果匹配不成功赂苗,會在編譯時報錯愉耙。
你也可以傳遞多個參數(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ù)時候拌滋,你可能需要一個實體的一部分成員變量朴沿,例如你的UI可能只顯示用戶的名和姓,而不是用戶的所有細(xì)節(jié)败砂。通過僅僅獲取出現(xiàn)在你UI中的字段赌渣,你可以存儲很多資源,并且你的查詢完成地更快昌犹。
Room允許你從查詢中返回任何對象坚芜,只要結(jié)果字段集可以被映射到返回的對象上。例如斜姥,你可以創(chuàng)建下面的POJO類來獲取用戶的姓和名:
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_name
和last_name
字段的查詢鸿竖,并可以映射到NameTuple
類。這樣铸敏,Room就能生成正確的代碼缚忧。如果查詢返回太多的字段,或者某個字段并不存在于NameTuple
中搞坝,Room會顯示一個警告搔谴。
傳遞參數(shù)集合
你的某些查詢可能會傳遞大量的參數(shù),而且直到運行時才知道具體的參數(shù)桩撮。例如敦第,你可能會獲取關(guān)于用戶所屬區(qū)域的信息。當(dāng)參數(shù)為集合時店量,Room能夠理解并自動根據(jù)當(dāng)前提供的參數(shù)進行擴展:
@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)運行查詢時芜果,你通常想要在數(shù)據(jù)變化的時候你的app界面自動更新。為了做到這一點融师,在查詢方法中使用LiveData
類型的返回值右钾。Room會生成所有必要的代碼,當(dāng)數(shù)據(jù)更新時旱爆,會自動更新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);
}
RxJava
Room也可以從你定義的查詢中直接返回RxJava2的Publisher
和Flowable
對象。為了使用這個功能怀伦,添加android.arch.persistence.room:rxjava2
到你的Gradle構(gòu)建依賴脆烟。你可以隨后返回RxJava2定義的類型,如:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
查詢多張表
你的一些查詢可能需要訪問多張表來計算結(jié)果房待。Room允許你寫任何的查詢邢羔,因此你可以使用連接表。另外桑孩,如果結(jié)果是可觀察數(shù)據(jù)類型拜鹤,例如Flowable
或者LiveData
,Room會驗證所有SQL查詢語句流椒。
下面的代碼片段展示了如何連接兩張表敏簿,一張表是包含用戶借書的信息,另一張包含當(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);
}
你也可以從這些查詢中返回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();
// 你也可以在單獨的文件中定義該類极谊,只要你添加了public修飾符
static class UserPet {
public String userName;
public String petName;
}
}
使用類型轉(zhuǎn)換
Room提供內(nèi)置工具用于基本類型和其封裝類型的裝換。但是有些時候你可能使用了使用了自定義數(shù)據(jù)類型安岂,而想在數(shù)據(jù)庫表中始終單個字段轻猖。為了添加這類自定義類型支持,你需要提供一個TypeConverter
域那,將自定義類轉(zhuǎn)換到Room已知可以持久化的類型咙边。
例如,如果我們想要持久化Date
實例次员,我們可以這樣寫:
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)換淑蔚。
接下來市殷,在AppDataBase
添加@TypeConverters
注解,使得Room可以使用你定義的轉(zhuǎn)換器:
@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
的作用范圍搞挣,包括單獨實體,DAO以及DAO方法音羞。
數(shù)據(jù)庫遷移
當(dāng)你在app添加以及修改功能時囱桨,你需要修改你的實體類以響應(yīng)這些變化。當(dāng)一個用戶更新到最新版本的app時嗅绰,你不想讓他們丟掉所有已經(jīng)存在的數(shù)據(jù)舍肠,特別是不能再從遠(yuǎn)程服務(wù)器獲取的數(shù)據(jù)。
Room允許你編寫Migration
類來保護用戶數(shù)據(jù)窘面。每個Migration
類指定一個startVersion
和endVersion
翠语。在運行時,Room運行每個Migration
類的migrate()
方法财边,使用正確的順序遷移至數(shù)據(jù)庫的更新版本肌括。
如果你沒有提供必要的遷移,Room會重新構(gòu)建數(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");
}
};
為了保持你的遷移功能的正確性,使用完整的查詢語句而不是詢而不是引用表示查詢的常量鲸鹦。
在遷移過程完成后慧库,Room驗證當(dāng)前的表以確保遷移的正確性。如果Room找到問題馋嗜,會拋出未匹配的異常信息齐板。
測試遷移
遷移是很重要的事情,錯誤的編寫會導(dǎo)致你app的崩潰循環(huán)葛菇。為了保證app的穩(wěn)定性甘磨,你應(yīng)該測試你的遷移工作。Room提供了一個測試的Maven構(gòu)件來幫助測試眯停。但是济舆,為了該構(gòu)件可以工作,你需要導(dǎo)出你的數(shù)據(jù)庫表莺债。
導(dǎo)出數(shù)據(jù)庫表
//build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
Room會將你數(shù)據(jù)庫的表信息導(dǎo)出為一個json文件滋觉。你應(yīng)該在版本控制系統(tǒng)中保存該文件,該文件代表了你的數(shù)據(jù)庫表歷史記錄齐邦,這樣允許Room創(chuàng)建舊版本的數(shù)據(jù)庫用于測試椎侠。
為了測試遷移,添加android.arch.persistence.room:testing
到你的測試依賴措拇,以及添加模式表的位置至asset文件夾我纪,如:
//build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
測試包提供了一個MigrationTestHelper
類,可以讀取這些模式表文件。
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db 版本為1. 使用SQL添加一些數(shù)據(jù)
// 你不能使用DAO浅悉,因為它表示的是最新的數(shù)據(jù)庫
db.execSQL(...);
// 準(zhǔn)備下個版本
db.close();
// 重新打開數(shù)據(jù)庫版本2
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
}
}
測試你的數(shù)據(jù)庫
當(dāng)運行測試你的app時趟据,如果沒有測試數(shù)據(jù)庫本身,你不需要創(chuàng)建全部的數(shù)據(jù)庫仇冯。Room允許在你的測試中模擬數(shù)據(jù)訪問層之宿。這個過程是可能的族操,因為你的DAO并沒有泄漏任何數(shù)據(jù)庫的細(xì)節(jié)苛坚。當(dāng)測試剩下的app部分時,你應(yīng)該創(chuàng)建模擬你的DAP類色难。
這里有兩種測試數(shù)據(jù)庫的方式:
- 在你的開發(fā)主機上
- 在Android設(shè)備上
在你的主機上測試
Room使用SQLite支持庫泼舱,提供了匹配安卓框架類的接口。這種支持允許你傳遞支持類庫的自定義實現(xiàn)以測試你的數(shù)據(jù)庫枷莉。
即使這種方案允許你測試非辰筷迹快捷,但是并不值得推薦笤妙,這是因為你設(shè)備上以及你用戶設(shè)備上運行的SQLite版本可能和你主機上運行的版本并不匹配冒掌。
在Android設(shè)備上測試
這種推薦的測試數(shù)據(jù)庫方法是編寫運行在安卓設(shè)備上的JUnit測試。因為這些測試并不需要創(chuàng)建Activity
蹲盘,它們應(yīng)該會比在UI上測試要快股毫。
當(dāng)設(shè)置你的測試時,你應(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));
}
}
附加::沒有實體鍵的對象引用
從數(shù)據(jù)庫到對象間關(guān)系的映射是一個很常見的實踐铃诬,并且在服務(wù)端運行良好,在它們被訪問的時候進行高性能的惰性加載苍凛。
但是在客戶端趣席,惰性加載并不可行,這是因為很有可能發(fā)生在主線程醇蝴,在主線程查詢磁盤信息會導(dǎo)致很嚴(yán)重的性能問題宣肚。主線程有大概16ms來計算并繪制一個Activity
的界面更新,因此甚至一個查詢僅僅耗費5ms悠栓,你的app仍然會耗光繪制畫面的時間霉涨,導(dǎo)致顯著的Jank問題。更糟的是闸迷,如果有個并發(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ù)砂碉。
簡單通俗地解釋一下Jank:第2幀畫面同步信號已經(jīng)到來,由于第2幀數(shù)據(jù)還沒有準(zhǔn)備就緒刻两,顯示的還是第1幀增蹭。這種情況被Android開發(fā)組命名為“Jank”