Android Realm資料庫使用指南
Android Realm資料庫使用指南
轉載於:https://www.cnblogs.com/mengdd/p/android-realm-database-guides.html
Realm資料庫, 目前有Java, Objective‑C, React Native, Swift, Xamarin的幾種實現, 是一套用來取代SQLite的解決方案.
本文面向Android開發, 所以只討論Java實現.
目前Realm Java的最新版本是2.3.1.
官方文件在此: realm java doc, 花一個下午就可以基本過一遍, 之後隨時查用.
我寫了一個小程式TodoRealm
本文是我自己看文件的時候的一些記錄, 有一些實際使用時的發現也穿插在對應的章節了.
Setup
在專案的根build.gradle的檔案中新增:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:2.3.0"
}
}
然後在app的build.gradle檔案中新增:
apply plugin: 'realm-android'
Done.
Models
Model類只要繼承RealmObject
即可.
public class User extends RealmObject { private String name; private int age; @Ignore private int sessionId; // Standard getters & setters generated by your IDE… 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; } }
欄位型別
Model類中可以包含的欄位型別包括基本資料型別(及它們的裝箱型別)和Date類, 另外也可以包含RealmObject
的子類或者是RealmList<? extends RealmObject>
.
欄位性質
在欄位上加註解可以定義欄位的性質:
@Required
表明欄位非null.
原生型別和RealmList
型別預設是非null的.RealmObject
欄位永遠是可以為null的.
@Ignore
表示欄位不會被儲存.
@Index
加索引.
@PrimaryKey
加主鍵, 主鍵只能有一個, 主鍵預設加索引.
但是注意主鍵預設沒有加@Required
, 如果主鍵要求非null, 需要顯式新增@Required
.
主鍵使用
有主鍵才能使用copyToRealmOrUpdate()
這個方法.
主鍵型別必須是String或者整型(byte, short, int, long)或者它們的裝箱型別(Byte, Short, Integer, Long).
有主鍵的物件建立的時候不能使用createObject(Class<E> clazz)
方法, 而應該使用createObject(Class<E> clazz, Object primaryKeyValue)
附上主鍵.
或者用copyToRealm(obj)
或copyToRealmOrUpdate(obj)
, 前者遇到主鍵衝突時會崩潰, 後者遇到主鍵衝突會更新已有物件.
自動更新的物件
Realm中的資料物件是自動更新(Auto-Updating)的, 物件一旦被查詢出來, 後續發生的任何資料改變也會立即反映在結果中, 不需要重新整理物件.
這是一個非常有用的特性, 結合資料變化的通知可以很方便地重新整理UI.
關係
Realm model物件間可以很方便地建立關係.
你可以在Model中儲存另一個物件的引用, 建立多對一的關係; 也可以儲存一組物件RealmList<T>
, 建立一對多或多對多的關係.
RealmList<T>
的getter永遠也不會返回null, 它只會返回一個為空的list.
把這個欄位設定為null可以清空這個list.
初始化
Realm在使用之前需要呼叫初始化:
Realm.init(context);
建議把它放在Application的onCreate()
裡.
配置
配置類: RealmConfiguration
定義了Realm的建立配置.
最基本的配置:
RealmConfiguration config = new RealmConfiguration.Builder().build();
它會建立一個叫default.realm
的檔案, 放在Context.getFilesDir()
的目錄下.
如果我們想自定義一個配置, 可以這樣寫:
// The RealmConfiguration is created using the builder pattern.
// The Realm file will be located in Context.getFilesDir() with name "myrealm.realm"
RealmConfiguration config = new RealmConfiguration.Builder()
.name("myrealm.realm")
.encryptionKey(getKey())
.schemaVersion(42)
.modules(new MySchemaModule())
.migration(new MyMigration())
.build();
// Use the config
Realm realm = Realm.getInstance(config);
所以我們是可以有多個配置, 訪問多個Realm例項的.
我們可以把配置設定為預設配置:
Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder().build();
Realm.setDefaultConfiguration(config);
之後用Realm.getDefaultInstance()
取到的就是這個預設配置對應的例項.
資料庫遷移
遷移的策略是通過config指定的:
RealmConfiguration config = new RealmConfiguration.Builder()
.schemaVersion(2) // Must be bumped when the schema changes
.migration(new MyMigration()) // Migration to run instead of throwing an exception
.build()
其中MyMigration
實現了RealmMigration
介面, 在migrate()
方法中根據新舊版本號進行一步一步地升級.
具體例子見Migration.
開發的時候為了方便我用的是.deleteRealmIfMigrationNeeded()
, 這樣在需要資料庫遷移的時候直接就刪了資料重新開始了.
關於Realm的close()
一個開啟的Realm例項會持有一些資源, 有一些是Java不能自動管理的, 所以就需要開啟例項的程式碼負責在不需要的時候將其關閉.
Realm的instance是引用計數的(reference counted cache), 在同一個執行緒中獲取後續例項是免費的, 但是底層的資源只有當所有例項被釋放了之後才能釋放. 也即你呼叫了多少次getInstance()
, 就需要呼叫相應次數的close()
方法.
比較建議的方法是在Activity或Fragment的生命週期中處理Realm例項的開啟和釋放:
- 在Activity的
onCreate()
中getInstance()
,onDestroy()
中close()
. - 在Fragment的
onCreateView()
中getInstance()
,onDestroyView()
中close()
.
如果多個Fragment相關的都是同一個資料庫例項, 那麼在Activity中處理更好一些.
寫
寫操作一般的流程是這樣:
// Obtain a Realm instance
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
//... add or update objects here ...
realm.commitTransaction();
這裡建立物件可以用createObject()
方法或者copyToRealm()
方法.
前者是先建立再set值, 後者是先new物件再更新資料庫.
如果不想自己處理beginTransaction()
, cancelTransaction()
和commitTransaction()
, 可以直接呼叫realm.executeTransaction()
方法:
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("[email protected]");
}
});
非同步
因為transactions之間是互相阻塞的.
非同步執行可以用這個方法:
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("[email protected]");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// Transaction was a success.
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
// Transaction failed and was automatically canceled.
}
});
這兩個回撥是Optional的, 它們只能在有Looper的執行緒呼叫.
注意: 這個方法的返回值物件可以用於在Activity/Fragment生命週期結束的時候取消未完的操作.
刪除和更新
所有的寫操作都要放在transaction中進行, 如上, 不同的操作只是其中具體方法不同.
刪除操作:
final RealmResults<User> users = getUsers();
// method 1:
users.get(0).deleteFromRealm();
// method 2:
users.deleteFromRealm(0);
// delete all
users.deleteAllFromRealm();
更新操作:
realm.copyToRealmOrUpdate(obj);
注意: 這個方法需要Model有主鍵, 會更新obj的主鍵對應的物件, 如果不存在則新建物件.
查詢
查詢可以流式地寫:
// Or alternatively do the same all at once (the "Fluent interface"):
RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();
查詢條件預設是and的關係, or則需要顯式指定.
這個RealmResults
是繼承Java的AbstractList
的, 是有序的集合, 可以通過索引訪問.RealmResults
永遠不會為null, 當查不到結果時, 它的size()
返回0.
查詢的執行緒
基本上所有的查詢都是很快進行的, 足夠在UI執行緒上同步進行.
所以絕大多數情況在UI執行緒上使用findAll()
是沒有問題的.
如果你要進行非常複雜的查詢, 或者你的查詢是在非常大的資料集上進行的, 你可以選擇非同步查詢, 使用findAllAsync()
.
查詢條件是一個集合 -> in()
如果想要查詢的某一個欄位的值是在一個集合中, 比如我有一個id的集合, 我現在想把id在這個集合中的專案全都查出來, 這就可以使用in操作符:
RealmResults<TodoList> toDeleteLists = realm.where(TodoList.class).in("id", ids).findAll();
鏈式查詢
查詢的時候可以利用link或關係來查詢, 比如一個Person類中含有一個RealmList<Dog> dogs
的欄位.
查詢的時候可以這樣:
RealmResults<Person> persons = realm.where(Person.class)
.equalTo("dogs.color", "Brown")
.findAll();
利用欄位名dogs.
來查詢一個dog的屬性, 再查出擁有這種特定屬性dog的人.
但是反向地, 我們能不能查詢主人是滿足特定屬性的人的所有dogs呢? 目前(2017.2.17)這種查詢仍是不支援的. 這裡有討論: realm-java-issue-607.
所以兩種解決辦法: 一是做兩次查詢; 二是在Dog類的model里加入對Person的引用.
Notifications
可以新增一個listener, 在資料改變的時候收到更新.
public class MyActivity extends Activity {
private Realm realm;
private RealmChangeListener realmListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
realmListener = new RealmChangeListener() {
@Override
public void onChange(Realm realm) {
// ... do something with the updates (UI, etc.) ...
}};
realm.addChangeListener(realmListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Remove the listener.
realm.removeChangeListener(realmListener);
// Close the Realm instance.
realm.close();
}
}
注意listener需要在不用的時候刪除掉.
可以用這樣刪除所有的listeners:
realm.removeAllChangeListeners();
Listener不一定要和Realm繫結, 也可以和具體的RealmObject
或者RealmResults
繫結.
當Listener被呼叫的時候, 它繫結的物件是自動更新的, 不需要手動重新整理.
檢視資料庫的工具
用Stetho不能直接檢視Realm的資料庫, 看不到.
需要用這個工具配置一下: stetho-realm.
之後就可以在瀏覽器中檢視Realm的資料庫了.
(但是感覺這個工具不是很好用, 有時候不顯示資料, 有時候顯示的是舊資料.)
也可以用官方提供的Realm Browser來檢視, 但是隻有Mac版.
如何檢視看這裡: StackOverflow answer.
實際使用的感想和遇到的問題
優點
- 建立Model之間的關係很方便也很直接, 查詢的時候自動關聯了其中的關係.
- 自動更新(Auto-Updating)的特性很有用, 不用再關心資料的重新整理, 只用關心UI的重新整理.
比如一旦給Adapter綁定了資料, 之後的資料更新只需要在onChange()裡面通知Adapter呼叫notifyDataSetChanged()
即可.
當然我並沒有用RealmBaseAdapter
和RealmRecyclerViewAdapter
, 估計這兩個更好用, 官方有例子, 這裡不再贅述.
缺點
這裡有的也不能說是缺點, 只是使用起來覺得不方便的地方.
- 限制了建立物件和操作物件必須在同一個執行緒.
違反了這條會報錯:java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
比如我們在UI執行緒查詢出來的物件, 想要非同步地刪除或者更新, 我們必須在新的執行緒重新查詢. - 沒有主鍵自增的功能, 見Issue #469, 需要自己控制主鍵自增.
- 從List中刪除了一項之後, 最後的一項會移動過來補到被刪除的那一項原來的位置. 這是因為人家就是這麼設計的stackoverflow. 預設情況下是沒有排序的, 資料按照新增的順序返回, 但是這並不是一種保證, 所以當刪除了中間的元素, 後面的會補上這個位置, 以保證底層的資料是放在一起的. 解決辦法就是指定一個排序規則.
- 查詢出來的物件不可以臨時改變其資料, 否則會報錯:
java.lang.IllegalStateException: Changing Realm data can only be done from inside a transaction.
- 不支援反向link的查詢. (見前面鏈式查詢部分的介紹).
- 不支援級聯刪除. 即從資料庫中刪除一個物件的時候, 不會刪除其中
RealmObject
子類或RealmList
型別的欄位在資料庫中對應的資料. Issue #1104, Issue #2717. 這點也可以理解, 因為model之間的關係可能是多對多的. 所以需要實現級聯刪除的地方需要手動處理. - 測試不方便:
RealmResults
物件即不能被mock也不能被new; 所有的Model物件也不能被mock. 因為Mockito can only mock non-private & non-final classes.
資源
我的練習Demo:
最後, 歡迎關注微信公眾號: 聖騎士Wind