Realm(Java)的那些事
什么是Realm?
在Android平臺上,有很多基于SQLite的ORM框架,例如 GreenDAO , OrmLite , SugarORM , LitePal 等等,對于寫習慣了SQL語句的小伙伴們來說,看到SQLite這樣的數據庫肯定是倍感親切了,有了這些框架更是如虎添翼。但是,在我們日常的開發中,數據量并不會特別的大,表的結構也不會特別復雜,用SQL語句有種大(過)材(于)小(繁)用(瑣)的感覺,我們需要做的事情可能僅僅是把用戶生成的數據對象快速的緩存起來。這個時候NoSQL就派上了用場,以Mongodb,Redis為代表的NoSQL都引入了一些相對現代化的方式存儲數據,比如支持Json,Document的概念,流式api,數據變更通知等等,極大程度的降低了我們學習的成本提高了我們的開發效率。而Realm作為一款移動端的NoSQL框架,官方定位就是替代SQLite等關系型數據庫。
Realm是一個由Y Combinator孵化的創業團隊開源出來的MVCC(多版本并發控制)的數據庫,支持運行在手機,平板和可穿戴設備等嵌入式設備上。
Realm的優點
簡單易用
上面我們已經說過,Realm并不是基于SQLite上的ORM,它有自己的數據庫引擎,使用也非常簡單。我們先來看看一段簡單的代碼。
// 通過繼承定義我們自己的model類
public class Dog extends RealmObject {
private String name;
private int age;
// ... 生成 getter 和 setter ...
}
public class Person extends RealmObject {
@PrimaryKey
private long id;
private String name;
private RealmList<Dog> dogs; // 生命一對多的關系
// ... 生成 getter 和 setter ...
}
// 像普通的Java對象一樣使用他們
Dog dog = new Dog();
dog.setName("Rex");
dog.setAge(1);
// 初始化Realm
Realm.init(context);
// 在當前線程下獲取Realm實例
Realm realm = Realm.getDefaultInstance();
// 查詢Realm中所有年齡小于2歲的狗狗
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0 因為現在還沒有狗狗添加到Realm中
// 在事物中持久化你的數據
realm.beginTransaction();
final Dog managedDog = realm.copyToRealm(dog);
Person person = realm.createObject(Person.class);
person.getDogs().add(managedDog);
realm.commitTransaction();
// 當數據改變后,Listener會被通知
puppies.addChangeListener(new RealmChangeListener<RealmResults<Dog>>() {
@Override
public void onChange(RealmResults<Dog> results) {
// 查詢結果會被實時更新
puppies.size(); // => 1
}
});
// 在后臺線程中異步地更新對象
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
Dog dog = bgRealm.where(Dog.class).equalTo("age", 1).findFirst();
dog.setAge(3);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// Realm對象會被自動地更新
puppies.size(); // => 0 因為沒有小狗狗的年齡小于兩歲
managedDog.getAge(); // => 3 狗狗的年齡已經被更新了
}
});
速度快
我們直接看Realm官方的對比測試數據吧。
每秒能在200K數據記錄中進行查詢后count的次數: Realm能夠達到30.9次,而SQLite只能達到13.6次,Core Data只能達到可憐的一次。
在200K條數據記錄進行一次遍歷查詢,數據和前面的count相似: Realm能夠達到每秒遍歷200K數據記錄31次,SQLite只能達到14次,而Core Data只有可憐的2次。
在一次事物中每秒插入數據量的對比,SQLite可以達到178K條記錄,性能最好,Realm可以達到94K,而Core Data再次墊底,只有18K。
我自己也進行一次簡單的測試,以JSON格式插入641條記錄(好吧,我知道數據量比較小,僅僅只是一個參考,具體的數據可以參考 這里 )。
04-03 19:06:13.837 11090-11245/io.github.marktony.espresso D/TAG: 1491217573837
04-03 19:06:14.044 11090-11245/io.github.marktony.espresso D/TAG: 1491217574044
207毫秒(Android 7.1.1, Realm 3.0)。
跨平臺
Realm目前支持Objective-C(iOS), Swift(iOS), Java(Android), JavaScript, Xamarin等平臺。
現在很多應用都需要兼顧iOS和Android兩個平臺,使用Realm可以使用Realm提供的API,在數據持久化層實現兩個平臺的無差異化移植,無需對內部數據的架構進行改動。
高級功能
Realm支持加密,格式化查詢,流式API,JSON,數據變更通知等等。
可視化
Realm官方提供了一個名為「 Realm Browser 」輕量級的數據庫查看工具(目前還只支持macOS平臺),利用Realm Browser我們可以進行簡單的插入,刪除等基本操作。
第三方開發者也提供了一些移動端的數據庫查看工具,例如:
Realm Browser by Jonas Rottmann (Android)
ealm Browser by Max Baumbach (iOS)
開源
Realm 已經將 Realm Core , Realm Java , Realm Cocoa , Realm JS , Realm Dotnet 等等項目開源,這也就意味著,你可以向Realm團隊提bug,提建議等等,和Realm團隊一起見證Realm的成長。
另外,還有一點一定要說明的是,Realm團隊的 博客 ,干貨滿滿,而且都有中文翻譯哦~
Realm的不足
說完了優點,自然還要說說不足的地方。
體積
引入Realm之后,在不做任何處理的情況下,APK體積增大了一個非常恐怖的數字 5.6M ,你沒有看錯,是5.6兆
(5.6M是什么概念,四舍五入就是10M,在四舍五入就是100M啊)
。直接看圖吧。
我們可以通過配置 build.gradle 的split,根據不同的設備類型對APK進行拆分,從而達到縮減體積的目的。下面我配置了split之后,APK體積的變化。
splits {
// Split apks on build target ABI, view all options for the splits here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
abi {
enable true
reset()
include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
}
universalApk true
}
數據類型
- Realm要求當前Bean對象必須直接繼承RealmObject, 或者間接實現(目前已經不再推薦使用) ,侵入性非常強。
- Realm不支持內部類。
- Realm修改了部分數據類型,例如List --> RealmList。在內部實現上,RealmList與ArrayList等還是有比較大的區別的。
-
使用RealmList 時,泛型 T 類型必須是直接繼承了RealmObject的類型,例如,如果是RealmList 類型,那么不好意思,這是不支持的?。官方建議我們自定義一個 RealmString 繼承自RealmObject,例如:
public class RealmString extends RealmObject { private String string; public String getString() { return string; } public void setString(String string) { this.string = string; } }
-
Realm是不支持主鍵自增長的,所以,我們需要自己維護一個 PrimaryKey 來約束主鍵。例如:
@PrimaryKey private String number;
另外,如果沒有給RealmObject設置主鍵,insertOrUpdata的默認操作就是insert,這樣 就會導致重復插入數據記錄了。
- Intent傳值時,也會有一些坑。例如,我想要在a1中查詢數據,然后將查詢結果(RealmList)傳遞到a2中。不好意思,Realm不想和你說話,并向你丟了一個crash。這是因為,ArrayList實現了 Serializable 接口,而RealmList并沒有。再例如,如果不是RealmList,而是一個普通的繼承自RealmObject并實現了 Serializable 接口的實體類呢?也不行。這是因為,查詢出來的數據并不是我們想要的對象,而是Realm利用apt幫我們生成的實體類的子類,或者說實體類的代理類,而在Realm中,起作用的就是這個代理類。
那么,怎么解決呢?官方的建議不要傳遞整個RealmList或者RealmObject,而是傳遞對象的標識符,然后在接收方(Activity, Service, BroadcastReceiver等等)解析出這個標識符,然后利用Realm再次查詢獲得相應的結果。
線程限制
如果你在UI線程獲取到了當前Realm對象,在異步線程中使用當前Realm對象進行操作,就會拋出異常了。RealmObject也是如此。所以,異步很重要。
另外,在調用了 Realm.close() 方法之后,所獲取的對象就不能再訪問了,所以,在獲取到了RealmObject之后,官方提供了一個 copyFromRealm 來復制一份實例供我們使用。
在Android中使用Realm
看到這里的客官,應該對Realm是真愛了。下面我們就來正經討論一下,如何在Android中使用Realm數據庫。
先決條件
- Android Studio 1.5.1或以上版本
- JDK 1.7或以上版本
- 較新版本的Android SDK
- Android API Level 9 或者以上(即Android 2.3及以上)
什么,你想要在Eclipse上使用Realm?
安裝
Realm是作為一個Gradle插件安裝的。
-
第一步:在project級別的 build.gradle 文件下添加:
buildscript { repositories { jcenter() } dependencies { classpath "io.realm:realm-gradle-plugin:3.0.0" } }
下面是project級別的 build.gradle 文件的位置:
-
第二步:在application級別的 build.gradle 文件的頂部應用 realm-android 插件:
apply plugin: 'realm-android'
下面是application級別的 build.gradle 文件的位置:
添加完成后,刷新gradle依賴即可。
下面是兩個級別的 build.gradle 文件示例:
除了gradle外,Realm并不支持像Maven和Ant這樣的構建工具。如果你有需要的話,可以關注下面的兩個issue。
獲取Realm實例
一般需要先在Application中完成Realm的初始化工作。例如:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
Realm.init(this);
}
}
然后我們可以通過一個Configuration來指定Realm生成的數據庫的名字和版本等等。
Realm realm = Realm.getInstance(new RealmConfiguration.Builder()
.deleteRealmIfMigrationNeeded()
.name("MyAwsomeRealmName.realm")
.build());
創建Realm實體
Realm的實體類可以通過繼承 RealmObject 的方式創建:
public class User extends RealmObject {
private String name;
private int age;
@Ignore
private int sessionId;
// 通過IDE生成的標準getters和setters...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}
Realm實體類還是支持 public , protected , private 字段和方法的。
public class User extends RealmObject {
public String name;
public boolean hasLongName() {
return name.length() > 7;
}
@Override
public boolean equals(Object o) {
// 自定義equals操作
}
}
字段類型
Realm支持的字段類型:
boolean , byte , short , int , long , float , double , String , Date and byte[] ,其中integer類型 byte , short , int ,都被自動的包裝成了 long 類型。 RealmObject 和 RealmList<? extends RealmObject> 的子類支持實體類之間的關系(一對一,一對多,多對多等)。
裝箱類型 Boolean , Byte , Short , Integer , Long , Float 和 Double 等也可以在實體類中使用,不過需要注意的是這些字段的值有可能為 null 。
Required字段和null值
在有些情況下,字段值為 null 并不合適。在Realm中, @Required 注解就是用來強制檢查,不允許字段出現 null 值。只有 Boolean , Byte , Short , Integer , Long , Float 和 Double 等可以使用 @Required 注解,如果其他類型的字段使用了此注解,編譯時將會出現錯誤。原始字段類型和 RealmList 類型被隱含的標示為 Required ,而 RealmObject 類型字段是可以為nullable的。
屬性忽略
使用注解 @Ignore 意味著此字段可以不被存儲到數據庫中。
自動更新
對于底層數據而言, RealmObject 是實時的,自動更新的,這也就意味著我們獲取到的對象數據不需要我們手動的刷新。更改數據對查詢的影響會被立刻反應在查詢結果上。
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Dog myDog = realm.createObject(Dog.class);
myDog.setName("Fido");
myDog.setAge(1);
}
});
Dog myDog = realm.where(Dog.class).equalTo("age", 1).findFirst();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Dog myPuppy = realm.where(Dog.class).equalTo("age", 1).findFirst();
myPuppy.setAge(2);
}
});
myDog.getAge(); // => 2
屬性索引
使用注解 @Index 會給字段添加一個搜索索引。這會導致插入速度變慢和數據文件變大,但是查詢操作會更快。所以,Realm只推薦你在需要提高讀性能的時候添加索引。索引支持的字段類型包括: String , byte , short , int , long , boolean 和 Date 。
增刪改查
數據庫的使用,最常用的就是增刪改查(CRUD)四種操作了,我們一起來看看Realm是如何實現上述四種操作的。
寫操作
在討論具體的CRUD之前,我們要先了解一下寫操作。讀操作是隱式完成的,也就是說,任何時候你都可以對實體進行訪問和查詢。而所有的寫操作(添加,修改,刪除)都必須在寫事物中完成。寫事物能夠被提交和取消。寫操作同時也用于保證線程安全。
// 獲取Realm實例
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
//... 在這里添加或者升級對象 ...
User user = realm.createObject(User.class);
realm.commitTransaction();
// 取消寫操作
// realm.cancelTransaction();
需要注意的是,寫操作是互斥的。所以,如果我們同時在UI線程和后臺線程中創建寫操作就有可能導致ANR。當我們在UI線程創建寫事物時,可以使用 異步事物 來避免ANR的出現。
Realm是crash安全的,所以如果在事物中產生了一個異常,Realm本身是不會被破壞的。不過在當前事物中的數據被丟失,不過為了避免異常產生的一系列問題,取消事物就非常重要了。如果使用 executeTransaction() 這些操作都會被自動完成。
由于Realm采用的MVCC架構,在寫事物進行的同時,讀操作也是被允許的。這也就意味著,除非需要在許多的線程中,同時處理許多的并行事務,我們可以使用大型事物,完成許多細粒度的事物。當我們向Realm提交一個寫事物時,其他的Realm實例都會被通知,并且被 自動更新 。
讀和寫的操作在Realm中就是 ACID .
添加
我們可以使用下面的代碼將數據添加到Realm中:
realm.beginTransaction();
User user = realm.createObject(User.class); // 創建一個新的對象
user.setName("John");
user.setEmail("john@corporation.com");
realm.commitTransaction();
User user = new User("John");
user.setEmail("john@corporation.com");
// 將對象復制到Realm中,后面的操作必須在realmUser上進行。
realm.beginTransaction();
User realmUser = realm.copyToRealm(user);
realm.commitTransaction();
我們也可以使用 realm.executeTransaction() 方法替代手動的跟蹤 realm.beginTransaction() , realm.commitTransaction() 和 realm.cancelTransaction() ,這個方法自動地處理了begin/commit,和錯誤發生后的cancel。
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
});
異步事物可以幫助我們處理同步事物可能帶來的UI線程阻塞的問題。使用異步事物后,事物會在一個后臺線程上運行,事物完成后會進行結果通知。
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// 事物成功完成
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
// 事物失敗,自動取消
}
});
onSuccess 和 onError 回調都是可選的,但是如果提供了這些方法,它們會在事物成功完成或者失敗后被調用。
我們可以通過 RealmAsyncTask 獲取一個異步事物的對象,這個對象可以用在當事物未完成而Activity或者Fragment被銷毀時取消事物。如果在回調中進行了更新UI的操作,而又忘記了取消事物,就會造成crash。
public void onStop () {
if (transaction != null && !transaction.isCancelled()) {
transaction.cancel();
}
}
Realm還提供了一個和神奇的功能,直接通過JSON(String, JSONObject, InputStream)添加數據,并且Realm會自動忽略沒有在RealmObject中定義的字段。單個的對象可以通過 Realm.createObjectFromJson() 方法添加,對象表可以通過 Realm.createAllFromJson() 方法添加。
// 代表city的RealmObject
public class City extends RealmObject {
private String city;
private int id;
// getters and setters left out ...
}
// 通過String添加
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObjectFromJson(City.class, "{ city: "Copenhagen", id: 1 }");
}
});
// 通過一個InputStream添加多個對象
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
try {
InputStream is = new FileInputStream(new File("path_to_file"));
realm.createAllFromJson(City.class, is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
Realm解析JSON時遵循下面的規則:
- 使用包含null值的JSON創建對象
- 對于非必須字段,設置為默認值null
- 對于必須字段,直接拋出異常
- 使用包含null值的JSON更新對象
- 對于非必須字段,設置為null
- 對于必須字段,直接拋出異常
- JSON不包含字段
- 保持必須和非必須字段的值不變
查詢
首先定義一個 User 類:
public class User extends RealmObject {
@PrimaryKey
private String name;
private int age;
@Ignore
private int sessionId;
// 使用IDE生成的標準getters和setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}
查詢所有name為「John」或者「Peter」的User:
// 創建一個RealmQuery用于查找所有符合條件的user
RealmQuery<User> query = realm.where(User.class);
// 添加查詢條件
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");
// 執行查詢
RealmResults<User> result1 = query.findAll();
// 或者進行簡化
RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();
上面的代碼就可以獲取一個 RealmResults 類的實例,包含了名稱為John或者Peter的所有user。當 findAll() 方法被調用時,查詢便開始執行。 findAll() 是 findAll() 方法大家族的一員,類似的方法還有: findAllSorted() 返回一個排好序之后的結果集合, findAllAsync() 會在后臺線程中異步的完成查詢操作。
需要注意的是,查詢得到的結果是沒有被復制的。正如Realm的官方文檔所言:
All fetches (including queries) are lazy in Realm, and the data is never copied.
我們得到了一個符合查詢條件的對象列表的引用,但是如果我們直接操作,對象將會是原始的對象。所以,還是復制一份吧。
ArrayList array = realm. copyFromRealm(result2);
RealmResults 繼承自Java的 AbstractList ,在許多方面的操作類似。例如, RealmResults 是有序的,我們可以通過索引獲取特定的對象。
當查詢沒有符合條件的結果時,返回值 RealmResults 并不會為null,但是 size() 方法會返回0。
如果我們想要修改或者刪除 RealmResults 中的對象,也必須在寫事物中進行。
過濾
對于所有的數據類型,都有以下兩種查詢條件:
- equalTo()
- notEqualTo()
使用 in() 匹配某一特定字段而不是一個的值的列表。例如,查找名字 「Jill」, 「William」, 「Trillian」,我們可以使用 in("name", new String[]{"Jill", "William", "Trillian"}) 。 in() 方法接收String, 二進制數據和數值型字段。
數值數據類型包括 Data ,都允許進行下面的查詢條件:
- between() (包含邊界值)
- greaterThan() - 大于
- lessThan() - 小于
- greaterThanOrEqualTo() - 大于等于
- lessThanOrEqualTo() - 小于等于
String類型字段允許使用以下查詢條件:
- contains() - 包含
- beginsWith() - 以...開頭
- endsWith() - 以...結尾
- like() - 類似于
所有的String類型都支持添加第三個參數來控制大小寫的敏感類型。
- Case.INSENSITIVE -> 大小寫不敏感
- Case.SENSITIVE -> 大小寫敏感(默認值)
使用 like() 進行模糊匹配,匹配條件如下:
- * - 匹配0個或者多個Unicode字符
- ? - 匹配單個Unicode字符
舉個?,假設現在有一個RealmObject有一個 name 字段,其值有「William」, 「Bill」,「Jill」, 和 「Trillian」。 查詢條件 like("name", "?ill*") 會匹配開始的3個對象, 而 like("name", "*ia?") 會匹配第一個和最后一個對象。
二進制數據,String, RealmObject 的列表( RealmList )有可能為空,也就是長度為0,下面是檢測是否為空的查詢條件:
- isEmpty()
- isNotEmpty()
如果一個字段是非必須字段,那么它的值就有可能為 null ,我們可以用以下條件檢測:
- isNull()
- isNotNull()
邏輯運算符
每一個查詢條件都被隱式地使用 AND 連接。而邏輯運算符 OR 必須使用 or() 顯式地聲明。
仍然以上面 User 類為例,我們可以使用 beginGroup() 和 endGroup() 來聲明一組查詢條件。
RealmResults<User> r = realm.where(User.class)
.greaterThan("age", 10) // 隱式地AND
.beginGroup()
.equalTo("name", "Peter")
.or()
.contains("name", "Jo")
.endGroup()
.findAll();
查詢條件也可以使用 not() 進行否定, not() 也可以和 beginGroup() 和 endGroup() 用于否定一組子查詢條件。再舉個?,我們想要查詢所有名字不為「Peter」和「Jo」的User:
RealmResult<User> r = realm.where(User.class)
.not()
.beginGroup()
.equalTo("name", "Peter")
.or()
.contains("name", "Jo")
.endGroup()
.findAll();
當然,我們也可以用 in() 進行簡化:
RealmResult<User> r = realm.where(User.class)
.not()
.in("name", new String[]{"Peter", "Jo"})
finalAll();
排序
當我們的查詢完成后,可以使用下面的代碼對查詢結果進行排序:
RealmResults<User> result = realm.where(User.class).findAll();
result = result.sort("age"); // 升序排序
result = result.sort("age", Sort.DESCENDING);
默認采用的是按升序排序,如果需要改變的話,可以將 Sort.DESCENDING 作為可選參數傳入。
唯一值
我們可以使用 distinct() 來查詢某一字段共有多少類型的值。例如,我們要查詢在我們的數據庫中有多少不同的名字:
RealmResults<Person> unique = realm.where(Person.class).distinct("name");
此操作只支持integer和string類型的字段,對其他類型進行此操作會產生異常。我們也可以對多個字段進行排序。
鏈式查詢
由于查詢結果并不會進行復制和計算操作,我們可以一步一步的過濾我們的數據:
RealmResults<Person> teenagers = realm.where(Person.class).between("age", 13, 20).findAll();
Person firstJohn = teenagers.where().equalTo("name", "John").findFirst();
我們也可以對子對象進行鏈式查詢。假設上面的 Person 類還有一個 Dog 類型的list類型字段:
public class Dog extends RealmObject {
private int age;
// getters & setters ...
}
public class Person extends RealmObject {
private int age;
private RealmList<Dog> dogs;
// getters & setters ...
}
我們可以通過鏈式查詢,查找年齡在13至20之間,并且至少有一只年齡為1歲的狗狗的人:
RealmResults<Person> teensWithPups = realm.where(Person.class).between("age", 13, 20).equalTo("dogs.age", 1).findAll();
需要注意的是,鏈式查詢的基礎并不是 RealmQuery ,而是 RealmResults 。如果我們為一個已經存在的 RealmQuery 添加更多的查詢條件,修改的是query本身,而不是鏈。
OK,到這里,查詢的情況我們討論的也差不多了。由于查詢在增刪改查四種操作中的使用頻率最高,所以篇幅也最長,下面我們來討論「修改」的情況。
修改
事實上,我們在上面的內容中已經進行過修改的操作了,。我們在查詢到符合條件的對象后,開啟一個事物,在事物中進行修改,然后提交事物即可:
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Dog myPuppy = realm.where(Dog.class).equalTo("age", 1).findFirst();
myPuppy.setAge(2);
}
});
刪除
刪除操作和修改操作類似,基本思想都是先查詢,然后在事物中進行操作。我們可以通過下面的代碼進行刪除操作:
// 獲取查詢結果
final RealmResults<Dog> results = realm.where(Dog.class).findAll();
// 所有對數據的變更必須在事物中進行
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
// 移除符合條件的單個查詢結果
results.deleteFirstFromRealm();
results.deleteLastFromRealm();
// 移除單個對象
Dog dog = results.get(5);
dog.deleteFromRealm();
// 移除所有符合條件的查詢結果
results.deleteAllFromRealm();
}
});
高級用法
加密
Realm文件可以通過向 RealmConfiguration.Builder.encryptionKey() 傳遞一個512位(64字節)的密鑰進行加密后存儲在磁盤上。
byte[] key = new byte[64];
new SecureRandom().nextBytes(key);
RealmConfiguration config = new RealmConfiguration.Builder()
.encryptionKey(key)
.build();
Realm realm = Realm.getInstance(config);
這樣的措施保證了所有存儲在磁盤上的數據都是經過標準AES-256加密和解密的。當Realm文件創建后,每次創建Realm實例時都需要提供相同的密鑰。
點擊 examples/encryptionExample 查看完整的示例,示例展示了如何通過Android KeyStore安全地存儲密鑰以保證其他應用不能讀取此密鑰。
與Android系統結合
適配器
Realm提供了一些抽象的工具類來方便地將 OrderedRealmCollection ( RealmResults 和 RealmList 都實現了這個接口)展示到UI控件上。
- RealmBaseAdapter 可以和 ListView 搭配使用。參考 示例 。
- RealmRecyclerViewAdapter 可以和 RecyclerView 搭配使用。參見 示例 。
在使用這些適配器之前,我們需要在application級別的 build.gradle 文件中添加額外的依賴:
dependencies {
compile 'io.realm:android-adapters:2.0.0'
}
Intents
由于我們并不能直接在Intent之間傳遞 RealmObject ,所以Realm建議只傳遞 RealmObject 的標識符。舉個很簡單的?:如果一個一個對象擁有一個主鍵,那么我就可以通過 Intent 的 Bundle 傳遞這個值。
// 假設我們現在有一個person類,并且將其id字段設置為@PrimaryKey ...
Intent intent = new Intent(getActivity(), ReceivingService.class);
intent.putExtra("person_id", person.getId());
getActivity().startService(intent);
然后在接收組件中(Activity, Service, IntentService, BroadcastReceiver 等等)解析出傳遞的主鍵值并打開Realm,查詢到該主鍵對應的 RealmObject 。
// 在onCreate(), onHandleIntent()等方法中完成
String personId = intent.getStringExtra("person_id");
Realm realm = Realm.getDefaultInstance();
try {
Person person = realm.where(Person.class).equalTo("id", personId).findFirst();
// 對person進行一些操作 ...
} finally {
realm.close();
}
完整的示例可以在 threading example 的 Object Passing 部分找到。示例展示了在Android常用的如何傳遞id并得到對應的 RealmObject 。
AsyncTask & IntentService
Realm和 AsyncTask , IntentService 搭配使用時,需要特別留心, AsyncTask 類包含了一個在后臺線程執行的 doInBackground() 方法, IntentService 類包含了在工作線程執行的 onHandleIntent(Intent intent) 方法。如果我們需要在上述兩個方法中使用Realm,我們需要先打開Realm,完成工作,然后在退出之前關閉Realm。下面是一些示例:
AsyncTask:在 doInBackground() 方法中打開和關閉Realm。
private class DownloadOrders extends AsyncTask<Void, Void, Long> {
protected Long doInBackground(Void... voids) {
// 現在已經在后臺線程中了。
// 打開Realm
Realm realm = Realm.getDefaultInstance();
try {
// 使用Realm
realm.createAllFromJson(Order.class, api.getNewOrders());
Order firstOrder = realm.where(Order.class).findFirst();
long orderId = firstOrder.getId(); // order的id
return orderId;
} finally {
realm.close();
}
}
protected void onPostExecute(Long orderId) {
// 回到Android主線程
// 完成一些和orderId有關的操作例如在Realm中
// 查詢order并做一些操作。
}
}
IntentServie: 在 onHandleIntent() 方法中打開和關閉Realm。
public class OrdersIntentService extends IntentService {
public OrdersIntentService(String name) {
super("OrdersIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
// 現在已經在后臺線程中了。
// 打開Realm
Realm realm = Realm.getDefaultInstance();
try {
// 使用Realm
realm.createAllFromJson(Order.class, api.getNewOrders());
Order firstOrder = realm.where(Order.class).findFirst();
long orderId = firstOrder.getId(); // order的id
} finally {
realm.close();
}
}
}
需要特別注意的是:在 IntentService 中, ChangeListener 不能夠正常的工作。盡管它是一個 Looper 線程,每一次調用 onHandleIntent() 不「loop」的分離事件。這也就意味著我們可以注冊register listener,但是它永遠也不會被觸發。
與其他第三方庫結合使用
Realm與GSON
GSON 是Google開發的JSON處理庫,Realm和GSON可以無縫的配合使用。
// 使用User類
public class User extends RealmObject {
private String name;
private String email;
// getters and setters ...
}
Gson gson = new GsonBuilder().create();
String json = "{ name : 'John', email : 'john@corporation.com' }";
User user = gson.fromJson(json, User.class);
序列化(Serialization)
我們有時需要序列化與反序列化一個Realm對象以便與其它庫(比如 Retrofit )相配合。因為GSON使用 成員變量值而非getter和setter ,所以我們無法通過GSON的一般方法來序列化Realm對象。
我們需要為Realm模型對象自定義一個 JsonSerializer 并且將其注冊為一個 TypeAdapter 。
請參考這個 Gist 。
數組(Primitive lists)
某些JSON API會以數組的形式返回原始數據類型(例如String和integer), Realm暫時不支持對這種數組的處理 。但我們可以通過自定義 TypeAdapter 來處理這種情況。
這個 Gist 展示了如何將JSON中的整型數組存入Realm。類似地,我們可以用這個方法來處理其它原始數據類型數組。
Troubleshooting
Realm 對象屬性可能會包含循環引用。在這種情況下,GSON 會拋出 StackOverflowError。例如如下 Realm 對象擁有一個 Drawable 屬性:
public class Person extends RealmObject {
@Ignore
Drawable avatar;
// 其他字段
}
Person 類含有一個 Android Drawable 并且被 @Ignore 修飾。當 GSON 序列化時,Drawable 被讀取并且造成了堆棧溢出。添加如下代碼以避免類似問題:
public boolean shouldSkipField(FieldAttributes f) {
return f.getDeclaringClass().equals(RealmObject.class) || f.getDeclaringClass().equals(Drawable.class);
}
請注意對 Drawable.class 的判定語句,它告訴 GSON 跳過這個屬性的序列化以避免堆棧溢出錯誤。
Realm與Jackson-databind
Jackson-databind 是一個實現JSON數據和Java類之間綁定的庫。
Jackson使用反射實現了數據綁定,而這與Realm對RxJava的支持產生了沖突因為對class loader而言,RxJava不一定可用。所造成的異常如下所示:
java.lang.NoClassDefFoundError: rx.Observable
at libcore.reflect.InternalNames.getClass(InternalNames.java:55)
...
可以通過為項目引入RxJava或者在工程創建一個看起來包含下面的代碼的空文件來修改上面的問題。
package rx;
public class Observable {
// 為了支持Jackson-Databind,如果沒有引入RxJava依賴,
// 這樣的空文件就是必須的
}
issue已經被提交到Jackson項目了 。
Realm與Kotlin
Realm 完全兼容 Kotlin 語言,但有些地方需要注意:
- 我們的模型類需要是 開放的(open) 。
- 我們可能需要在某些情況下添加注解 @RealmCLass 以保證編譯通過。這是由于 當前 Kotlin 注解處理器的一個限制 。
- 很多 Realm API 引用了 Java 類。我們必須在編譯依賴中添加 org.jetbrains.kotlin:kotlin-reflect:${kotlin_version} 。
Realm與Retrofit
Retrofit 是一個由 Square 開發,保證類型安全(typesafe)的 REST API 處理工具。
Realm 可以與 Retrofit 1.x 和 2.x 無縫配合工作。但請注意 Retrofit 不會自動將對象存入 Realm。我們需要通過調用 Realm.copyToRealm() 或 Realm.copyToRealmOrUpdate() 來將它們存入 Realm。
GitHubService service = restAdapter.create(GitHubService.class);
List<Repo> repos = service.listRepos("octocat");
// 從Retrofit復制數據元素到Realm
realm.beginTransaction();
List<Repo> realmRepos = realm.copyToRealmOrUpdate(repos);
realm.commitTransaction();
Realm與RxJava
RxJava 是 Netflix 發布的一個 Reactive 的擴展 庫以支持 觀察者模式 。
Realm 包含了對 RxJava 的原生支持。如下類可以被暴露為一個 Observable : Realm , RealmResults , RealmObject , DynamicRealm 和 DynamicRealmObject 。
// 綜合使用Realm, Retrofit 和 RxJava(使用Retrolambda使語法更簡潔)
// 加載所有用戶并將它們的GitHub的最新stats合并(如果有的話)
Realm realm = Realm.getDefaultInstance();
GitHubService api = retrofit.create(GitHubService.class);
realm.where(Person.class).isNotNull("username").findAllAsync().asObservable()
.filter(persons.isLoaded)
.flatMap(persons -> Observable.from(persons))
.flatMap(person -> api.user(person.getGithubUserName())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> showUser(user));
請注意 異步查詢 不會阻塞當前線程,如上代碼會立即返回一個 RealmResults 實例。如果我們想確定該 RealmResults 已經加載完成請使用 filter operator 和 `RealmResults .isLoaded() 方法。通過判斷 RealmResults 是否已經加載可以得知查詢是否已經完成。
配置RxJava 是可選依賴,這意味著 Realm 不會自動包含它。這樣做的好處是我們可以選擇需要的 RxJava 版本以及防止過多的無用方法被打包。如果我們要使用相關功能,需要手動添加 RxJava 到 build.gradle 文件。
dependencies {
compile 'io.reactivex:rxjava:1.1.0'
}
我們也可以通過繼承 RxObservableFactory 來決定 Observable 的生成方式,然后通過 RealmConfiguration 進行配置。
RealmConfiguration config = new RealmConfiguration.Builder()
.rxFactory(new MyRxFactory())
.build()
如果沒有 RxObservableFactory 被定義, RealmObservableFactory 會被默認使用,它支持 RxJava <= 1.1.*(也就意味著目前在RxJava2.0上現在還沒有辦法使用...)。
版本遷移
當我們的數據結構發生了變化時,我們就需要對數據庫進行升級了。而在Realm上,數據庫的升級是通過遷移操作完成的,也就是把原來的數據遷移到具有新數據結構的數據庫。通常,這樣的操作可以分成兩部完成。
-
創建遷移類
// 遷移類示例 public class MyMigration implements RealmMigration{ @Override public void migrate(DynamicRealm realm, long oldVersion, long newVersion) { // DynamicRealm 暴露了一個可編輯的schema RealmSchema schema = realm.getSchema(); // 遷移到版本 1 : 添加一個新的類 // 示例: // public Person extends RealmObject { // private String name; // private int age; // // getters and setters left out for brevity // } if (oldVersion == 0) { schema.create("Person") .addField("name", String.class) .addField("age", int.class); oldVersion++; } // 遷移到版本 2 :添加一個primary key + 對象引用 // 示例: // public Person extends RealmObject { // private String name; // @PrimaryKey // private int age; // private Dog favoriteDog; // private RealmList<Dog> dogs; // // getters and setters left out for brevity // } if (oldVersion == 1) { schema.get("Person") .addField("id", long.class, FieldAttribute.PRIMARY_KEY) .addRealmObjectField("favoriteDog", schema.get("Dog")) .addRealmListField("dogs", schema.get("Dog")); oldVersion++; } } }
-
使用 Builder.migration 升級數據庫
將版本號改為2,當Realm發現新舊版本號不一致時,會自動使用該遷移類完成遷移操作。
RealmConfiguration config = new RealmConfiguration.Builder() .schemaVersion(2) // 在schema改變后,必須進行升級 .migration(new MyMigration()) // 開始遷移 .build()
其他
寫到這里,基本的內容就差不多討論完了,事實上,Realm還有很多其他的玩法,感興趣的話,可以戳 這里 ,詳細的了解。
參考資料
Realm:Create reactive mobile apps in a fraction of time
以上所有測試均基于Realm 3.0, 設備為OnePlus 3(Android 7.1.1), 環境為macOS 10.12.4.
來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0406/7791.html