Android Realm數據庫使用指南

nabi 7年前發布 | 8K 次閱讀 Realm 安卓開發 Android開發 移動開發

Android Realm數據庫使用指南

Realm數據庫, 目前有Java, Objective?C, React Native, Swift, Xamarin的幾種實現, 是一套用來取代SQLite的解決方案.

本文面向Android開發, 所以只討論Java實現.

目前Realm Java的最新版本是2.3.1.

官方文檔在此: realm java doc , 花一個下午就可以基本過一遍, 之后隨時查用.

我寫了一個小程序 TodoRealm , 使用Realm做數據庫實現的一個To-do應用, 在實際使用的過程中也有一些發現.

本文是我自己看文檔的時候的一些記錄, 有一些實際使用時的發現也穿插在對應的章節了.

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; }

}</code></pre>

字段類型

Model類中可以包含的字段類型包括基本數據類型(及它們的裝箱類型)和Date類, 另外也可以包含 RealmObject 的子類或者是 RealmList<? extends RealmObject> .

字段性質

在字段上加注解可以定義字段的性質:

<p>@Required 表明字段非null.</p>

原生類型和 RealmList 類型默認是非null的.

RealmObject 字段永遠是可以為null的.

<p>@Ignore 表示字段不會被存儲.</p> <p>@Index 加索引.</p> <p>@PrimaryKey 加主鍵, 主鍵只能有一個, 主鍵默認加索引.</p>

但是注意主鍵默認沒有加 @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() 方法中根據新舊版本號進行一步一步地升級.

開發的時候為了方便我用的是 .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();</code></pre>

這里創建對象可以用 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("john@corporation.com");
    }
});

異步

因為transactions之間是互相阻塞的.

異步執行可以用這個方法:

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() {
                // 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();</code></pre>

更新操作:

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();
}

}</code></pre>

注意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.

資源

 

 

來自:http://www.cnblogs.com/mengdd/p/android-realm-database-guides.html

 

 本文由用戶 nabi 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!