簡介
引言
谷歌在今年的I/O大會上發(fā)布了新的架構(gòu)庫Android architecture component雕凹,為了解決開發(fā)者遇到的一些常見問題殴俱,推薦遵從以下兩個原則構(gòu)建應(yīng)用:
- 關(guān)注點分離
盡量避免在Activity或Fragment中編寫所有的代碼,任何不是處理 UI 或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中枚抵。保持它們盡可能的精簡可以避免許多與生命周期有關(guān)的問題粱挡。 - model驅(qū)動UI
使用持久化的model,因為如果 OS 銷毀應(yīng)用釋放資源俄精,用戶不用擔(dān)心丟失數(shù)據(jù);而且即使網(wǎng)絡(luò)連接不可靠或者是斷開的榕堰,應(yīng)用仍將繼續(xù)運行竖慧。
為此,推出了新的架構(gòu)組件來幫助開發(fā)者快速搭建滿足上述要求的應(yīng)用逆屡,這就是四個新組件LiveDate,ViewModel,Room Persistence,Lifecycles的由來,而此次的重點就是Room圾旨。
Room
Room提供了一個SQLite之上的抽象層,使得在充分利用SQLite功能的前提下順暢的訪問數(shù)據(jù)庫魏蔗。Room中有三個主要的組件:Entity砍的,Database和Dao。
使用方法
gradle配置
打開整個項目的build.gradle莺治,然后添加:
allprojects {
repositories {
jcenter()
//因為GFW的原因廓鞠,這個網(wǎng)站不一定連的上,參考下文中的問題一
maven { url 'https://maven.google.com' }
}
}
然后在app的build.gradle里添加:
// App Toolkit
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"
這樣就把所需的庫添加到項目中的谣旁。
Entity
當(dāng)一個類用@Entity注解并且被@Database注解中的entities屬性所引用床佳,Room就會在數(shù)據(jù)庫中為那個entity創(chuàng)建一張表。
默認(rèn)Room會為entity中定義的每一個field都創(chuàng)建一個column榄审。如果一個entity中有你不想持久化的field砌们,那么你可以使用@Ignore來注釋它們,如下面的代碼所示:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
要持久化一個field,Room必須有獲取它的渠道浪感。你可以把field寫成public昔头,也可以為它提供一個setter和getter。如果你使用setter和getter的方式影兽,記住它們要基于Room的Java Bean規(guī)范揭斧。
Primary key
每個entity必須至少定義一個field作為主鍵(primary key)。即使只有一個field赢笨,你也必須用@PrimaryKey注釋這個field未蝌。如果你想讓Room為entity設(shè)置自增ID,你可以設(shè)置@PrimaryKey的autoGenerate屬性茧妒。如果你的entity有一個組合主鍵萧吠,你可以使用@Entity注解的primaryKeys屬性,具體用法如下:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
Room默認(rèn)把類名作為數(shù)據(jù)庫的表名桐筏。如果你想用其它的名稱纸型,使用@Entity注解的tableName屬性,如下:
//SQLite中的表名是大小寫敏感的梅忌。
@Entity(tableName = "users")
class User {
...
}
和tableName屬性類似狰腌,Room默認(rèn)把field名稱作為數(shù)據(jù)庫表的column名。如果你想讓column有不一樣的名稱牧氮,為field添加@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 和 uniqueness
為了提高查詢的效率,你可能想為特定的字段建立索引踱葛。要為一個entity添加索引丹莲,在@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;
}
有時候,某個字段或者幾個字段必須是唯一的常挚。你可以通過把@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;
}
關(guān)系
因為SQLite是關(guān)系數(shù)據(jù)庫洲赵,你可以指定對象之間的關(guān)聯(lián)。雖然大多數(shù)ORM庫允許entity對象相互引用商蕴,但是Room明確禁止了這種行為叠萍。后文中的注意事項中解釋了原因。
雖然不可以使用直接的關(guān)聯(lián)绪商,Room仍然允許你定義entity之間的外鍵(Foreign Key)約束俭令。
比如,假設(shè)有另外一個entity叫做calledBook部宿,你可以使用@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落蝙。
嵌套對象
有時你可能想把一個entity或者一個POJOs作為一個整體看待,即使這個對象包含幾個field暂幼。這種情況下筏勒,你可以使用@Embedded注解,表示你想把一個對象分解為表的子字段旺嬉。然后你就可以像其它獨立字段那樣查詢這些嵌入的字段管行。
比如,我們的User類可以包含一個類型為Address的field邪媳,Address代表street,city,state, 和postCode字段的組合捐顷。為了讓這些組合的字段單獨存放在這個表中,對User類中的Address字段使用@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;
}
那么現(xiàn)在代表一個User對象的表就有了如下的字段:id,firstName,street,state,city,以及post_code。
如果一個entity有多個嵌套字段是相同類型徽龟,你可以設(shè)置prefix屬性保持每個字段的唯一性叮姑。Room就會在嵌套對象中的每個字段名的前面添加上這個值。
Data Access Objects (DAOs)
Room中的主要組件是Dao類据悔。DAO抽象出了一種操作數(shù)據(jù)庫的簡便方法传透。
注:Room不允許通過主線程上訪問數(shù)據(jù)庫,除非您在構(gòu)建器上調(diào)用allowMainThreadQueries()屠尊,因為它可能會長時間地鎖定用戶界面。異步查詢(返回LiveData或RxJava Flowable的查詢)將免除此規(guī)則耕拷,因為它們在需要時異步地在后臺線程上運行查詢讼昆。
便利的方法
DAO提供了多種簡便的查詢方式,本文檔列出幾種常見的例子骚烧。
- insert
當(dāng)你創(chuàng)建了一個DAO方法并且添加了@Insert注解浸赫,Room生成一個實現(xiàn),將所有的參數(shù)在一次事務(wù)中插入數(shù)據(jù)庫赃绊。
下面的代碼片段演示了幾種查詢的例子:
@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,代表新插入元素的rowId碧查,如果參數(shù)是一個數(shù)組或者集合运敢,那么應(yīng)該返回long[]或者List校仑。
- update
Update是一個更新一系列entity的簡便方法。它根據(jù)每個entity的主鍵作為更新的依據(jù)传惠。下面的代碼演示了如何定義這個方法:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
你可以讓這個方法返回一個int類型的值迄沫,表示更新影響的行數(shù),雖然通常并沒有這個必要卦方。
- delete
這個API用于刪除一系列entity羊瘩。它使用主鍵找到要刪除的entity。下面的代碼演示了如何定義這個方法:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
你可以讓這個方法返回一個int類型的值盼砍,表示從數(shù)據(jù)庫中被刪除的行數(shù)尘吗,雖然通常并沒有這個必要。
使用@Query的方法
@Query是DAO類中主要被使用的注解浇坐。它允許你在數(shù)據(jù)庫中執(zhí)行讀寫操作睬捶。每個@Query方法都是在編譯時檢查,因此如果查詢存在問題吗跋,將出現(xiàn)編譯錯誤侧戴,而不是在運行時引起崩潰。
Room還會檢查查詢的返回值跌宛,如果返回的對象的字段名和查詢結(jié)果的相應(yīng)字段名不匹配酗宋,Room將以下面兩種方式提醒你:
如果某些字段名不匹配給出警告。
如果沒有匹配的字段名給出錯誤提示疆拘。
簡單的查詢:
@Dao
public interface MyDao {
@Query(“SELECT * FROM user”)
public User[] loadAllUsers();
}
這是一個非常簡單的查詢蜕猫,加載所有的user。在編譯時哎迄,Room知道它是查詢user表中的所有字段回右。如果query有語法錯誤,或者user表不存在漱挚,Room將在app編譯時顯示恰當(dāng)?shù)腻e誤信息翔烁。
向query傳遞參數(shù)
大多數(shù)時候,你需要向查詢傳遞參數(shù)來執(zhí)行過濾操作旨涝,比如只顯示大于某個年齡的user蹬屹。為此,在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ù)的名稱來匹配弧腥。如果有不匹配的情況厦取,app編譯的時候就會出現(xiàn)錯誤。
你也可以傳遞多個參數(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ù)時候虾攻,我們只需要一個entity的部分字段铡买。比如,你的界面也許只需顯示user的first name 和 last name台谢,而不是用戶的每個詳細(xì)信息寻狂。只獲取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)在你可以在query方法中使用這個POJO:
//注:這些POJO也可以使用@Embedded注解纠亚。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
Room知道這個查詢返回first_name和last_name字段的值,并且這些值可以被映射到NameTuple類的field中筋夏。因此Room可以生成正確的代碼蒂胞。如果查詢返回了太多的字段,或者某個字段不在NameTuple類中条篷,Room將顯示一個警告骗随。
傳入?yún)?shù)集合
一些查詢可能需要你傳入個數(shù)是一個變量的參數(shù),只有在運行時才知道具體的參數(shù)個數(shù)赴叹。比如鸿染,你可能想獲取一個區(qū)間的用戶信息。當(dāng)一個參數(shù)代表一個集合的時候Room是知道的乞巧,它在運行時自動根據(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);
}
可觀察的查詢
當(dāng)執(zhí)行查詢的時候,你通常希望app的UI能自動在數(shù)據(jù)更新的時候更新绽媒。為此蚕冬,在query方法中使用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);
}
注:對于version 1.0,Room使用query中的table列表來決定是否更新LiveData對象获三。
RxJava
Room還可以讓你定義的查詢返回RxJava2的Publisher和Flowable對象旁蔼。要使用這個功能,在Gradle dependencies中添加Android.arch.persistence.room:rxjava2石窑。然后你就可以返回RxJava2中定義的對象類型了牌芋,如下面的代碼所示:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
Direct cursor access
如果你的app需要獲得直接返回的行蚓炬,你可以在查詢中返回Cursor對象松逊,如下面的代碼所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
注:不推薦使用Cursor API,因為它無法保證行是否存在或者行中有哪些值肯夏。只有在當(dāng)前的代碼需要一個cursor经宏,而且你又不好重構(gòu)的時候才使用這個功能犀暑。
多表查詢
某些查詢可能需要根據(jù)多個表查詢出結(jié)果。Room允許你書寫任何查詢烁兰,因此表連接(join)也是可以的耐亏。而且如果響應(yīng)是一個可觀察的數(shù)據(jù)類型,比如Flowable或者LiveData沪斟,Room將觀察查詢中涉及到的所有表广辰。
下面的代碼演示了如何執(zhí)行一個表連接查詢來查出借閱圖書的user與被借出圖書之間的信息。
@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對象主之。比如你可以寫一個如下的查詢加載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;
}
}
使用類型轉(zhuǎn)換器
Room內(nèi)置了原始類型择吊。但是,有時你會希望使用自定義數(shù)據(jù)類型槽奕。 要為自定義類型添加這種支持几睛,可以提供一個TypeConverter,它將一個自定義類轉(zhuǎn)換為Room保留的已知類型粤攒。
比如所森,如果我們要保留Date的實例,我們可以編寫以下TypeConverter來存儲數(shù)據(jù)庫中的等效的Unix時間戳記:
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對象焕济,另一個將從Long到Date轉(zhuǎn)換為執(zhí)行逆轉(zhuǎn)換。 由于Room已經(jīng)知道如何持久化Long對象钻蹬,因此可以用此轉(zhuǎn)換器來持久保存Date類型的值吼蚁。
接下來,將@TypeConverters注釋添加到AppDatabase類问欠,以便Room可以使用你為該AppDatabase中的每個實體和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);
}
測試遷移
寫遷移不是一件簡單的事情旗国,如果寫法不恰當(dāng)可能導(dǎo)致app的進(jìn)入崩潰的惡性循環(huán)。為了保證app的穩(wěn)定性注整,你應(yīng)該先測試遷移能曾。Room提供了一個testing Maven artifact來幫助你完成這個測試過程。但是要讓這個artifact工作肿轨,需要導(dǎo)出數(shù)據(jù)庫的schema寿冕。
導(dǎo)出schemas
在編譯的時候,Room將database的schema信息導(dǎo)出到一個JSON文件中椒袍。為此驼唱,要在build.gradle 文件中設(shè)置room.schemaLocation注解處理器屬性,如下面的代碼所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
你應(yīng)該把導(dǎo)出的在這個JSON文件-它代表了你的數(shù)據(jù)庫的schema歷史-保存到你的版本管理系統(tǒng)中驹暑,這樣就可以讓Room創(chuàng)建舊版本的數(shù)據(jù)庫來測試玫恳。
測試遷移需要把Room的Maven artifact android.arch.persistence.room:testing 添加到你的test dependencies,并且把schema的位置作為 asset folder添加進(jìn)去辨赐,代碼如下:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
testing package提供了一個MigrationTestHelper類,它可以讀出這些schema文件京办。它同時也是一個 Junit4 TestRule類掀序,因此可以管理創(chuàng)建的數(shù)據(jù)庫。
下面的代碼是一個遷移測試的例子:
@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 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.
}
}
可能出現(xiàn)的問題
問題1
問題描述
編譯項目消耗時間長惭婿,而且報錯
compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"
解決辦法
純粹是因為greatWall的問題不恭,把app的build.gradle中的maven.google.com替換https://dl.google.com/dl/android/maven2/
問題2
問題描述
在測試數(shù)據(jù)庫遷移的時候,需要往build.gradle上添加一句
testCompile "android.arch.persistence.room:testing:1.0.0-alpha5"
然后編譯的時候可能會報錯:
解決辦法
把依賴修改為
compile "android.arch.persistence.room:testing:1.0.0-alpha5"
注意事項
事項1
兩個entity之間不能添加對象引用
官網(wǎng)翻譯:在數(shù)據(jù)庫和相關(guān)的對象模型之間建立映射關(guān)系是服務(wù)端非常常見且好用的辦法财饥,可以通過懶加載來提高性能县袱。
然而,在客戶端這邊佑力,懶加載的效果并不顯著因為它發(fā)生在UI線程式散,而在UI線程里面執(zhí)行對磁盤的查找會帶來顯著的性能問題。UI線程大概會花16ms的時間來計算與繪制頁面布局的升級打颤,所以就算查找只花5ms暴拄,app仍然可能在繪制框架的過程中耗盡時間并且造成可見的問題。更糟糕的是编饺,如果查找是多個事務(wù)并行執(zhí)行的話可能會花掉更多時間乖篷,或者設(shè)備同時在執(zhí)行其他磁盤耗費嚴(yán)重的任務(wù)。而且如果不使用懶加載的話透且,將會帶來更多內(nèi)存上的消耗撕蔼。
ORM通常將這個問題留給開發(fā)者自己根據(jù)應(yīng)用的實際情況做決定。不幸的是秽誊,開發(fā)者通常會在模型層與UI之間共享模型鲸沮。隨著UI不停變更,很難去分析與調(diào)試出現(xiàn)的問題锅论。
數(shù)據(jù)統(tǒng)計
操作 | greendao耗時 | room耗時 |
---|---|---|
循環(huán)插入10000條數(shù)據(jù) | 98238ms | 66821ms |
批量插入10000條數(shù)據(jù) | 623ms | 1051ms |
循環(huán)更新10000次 | 97840ms | 61348ms |
批量更新10000條數(shù)據(jù) | 739ms | 1366ms |
循環(huán)查詢10000條數(shù)據(jù) | 31923ms | 6932ms |
批量查詢10000條數(shù)據(jù) | 149ms | 不支持大量參數(shù)的同時查詢 |
greenDao 的數(shù)據(jù)來源于網(wǎng)上讼溺。
操作 | greendao耗時 | room耗時 |
---|---|---|
循環(huán)插入10000條數(shù)據(jù) | 98238ms | 66821ms |
批量插入10000條數(shù)據(jù) | 623ms | 1051ms |
循環(huán)更新10000次 | 97840ms | 61348ms |
批量更新10000條數(shù)據(jù) | 739ms | 1366ms |
循環(huán)查詢10000條數(shù)據(jù) | 31923ms | 6932ms |
批量查詢10000條數(shù)據(jù) | 149ms | 不支持大量參數(shù)的同時查詢 |