Help us understand the problem. What is going on with this article?

Realmの隠れた重要ポイント

More than 3 years have passed since last update.

About

Realmは、SQLiteやCoreDataを置き換えるべく開発されているDBで、主な特徴はこんな感じです。

  • NoSQL的なアーキテクチャ
  • 高速
  • JavaとCocoaをサポート
    • iOSとAndroidで設計を共有できる
    • DBファイル(*.realm)がプラットフォーム非依存なので、データの共有が容易

公式のドキュメントやサンプルコードがそれなりに充実しているので、とりあえず使い始めるには情報に不足はありません。

しかし、ある程度開発を進めていくと、ドキュメントで言及されていなくても、事前に知っておかないと辛い部分も見えてきたので、メモとしてまとめておきます。

サンプルコードは主にJava(Android)となりますが、Cocoaな人は適宜Swift/Objective-Cに読み替えてください。

隠れた重要ポイント

  • [共通] StandaloneObjectという概念
  • [Java] StandaloneObjectとManagedObjectの区別が重要
  • [Cocoa] Standaloneは破壊的にManagedになる
  • [共通] StandaloneObjectでaddOrUpdateすると、既存のRelationが消える

[共通] StandaloneObjectという概念

RealmObjectには、Standaloneかどうかという重要な概念があります。
具体的には、オブジェクトが特定のRealmインスタンスの管理下に置かれているかどうか、ということなのですが、サンプルコードで見れば一目瞭然です。

// Standalone
Item item = new Item();

// Managed (Not Standalone)
Item item = Realm.getInstance(getContext()).createObject(Item.class);

Standaloneという概念が何故重要なのかというと、スレッド間の受け渡しが可能になるからです。

Realmの公式ドキュメントには以下のように書いてあったので「キツイなー」と思っていたのですが、これはRealmインスタンスが絡んだ場合の話のようです。

スレッド間での使用方法として注意することは、Realm、RealmObject、RealmResults インスタンスは、スレッド間での受け渡しができないということです。

Standaloneを積極的に活用することで、例えばWebAPIの通信処理では、以下のような設計も可能になります。

  1. バックグラウンドの通信処理でJSONのParseとStandaloneObjectの構築を行う
  2. メインスレッド側でStandaloneObjectを受け取る
  3. バックグラウンドでStandaloneObjectをRealmに保存する。

なお、Standaloneという言葉はRealmのドキュメントにも登場するのですが、「Standaloneではない」に対応する表現については、言及されていませんでした。

本記事では便宜的に、Standaloneではない状態をManagedと呼ぶことにします。

[Java] StandaloneObjectとManagedObjectの区別が重要

StandaloneObjectは値の入れ物に過ぎないので、最終的にRealmに保存をすればManagedObjectになり、また、Realmの機能(Relationなど)を適用するにはManagedObjectで操作をする必要があります。

つまり、実装のなかで、そのオブジェクトはStandaloneObjectなのかManagedObjectなのか区別することが、非常に重要になります。

まず、Javaのサンプルコードで確認します。

final Item standaloneItem = new Item();
standaloneItem.setName("なまえ");

Realm.getInstance(getContext()).executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // copyToRealm() で ManagedObjectが返される.
        // managedItem と standaloneItem は別インスタンス.
        final Item managedItem = realm.copyToRealm(standaloneItem);
    }
});

Realm#copyToRealm()メソッドは、引数がStandaloneObjectだった場合は、戻り値としてManagedObjectを新しいインスタンスとして返すようになっています。このManagedObjectのインスタンスは、copyToRealm()を呼び出したRealmインスタンスに紐付いています。

続けて、保存したItemをCategoryとの1対多Relationに追加する処理を実装してみます。

final Item standaloneItem = new Item();
standaloneItem.setName("なまえ");

Realm.getInstance(getContext()).executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // copyToRealm() で ManagedObjectが返される.
        // managedItem と standaloneItem は別インスタンス.
        final Item managedItem = realm.copyToRealm(standaloneItem);

        // Relationへの追加は、必ずManagedObjectで行う.
        final Category category = realm.where(Category.class).equalTo("id", 1).findFirst();        
        category.getItems().add(managedItem);
        // NG: StandaloneObjectでRelationに追加しても保存されない.
        category.getItems().add(standaloneItem);
    }
});

ポイントは、category.getItems().add(managedItem)の部分で、ManagedObjectを引数に与えている点です。ここで間違えてStandaloneObjectなインスタンスをaddしても、追加されないので注意が必要です。

[Cocoa] StandaloneObjectは破壊的にManagedObjectになる

先ほどのJavaの例で、Realm#copyToRealm()がManagedObjectを新しいインスタンスとして返すと説明しました。

Cocoa版では、StandaloneObjectの状態が破壊的に書き換えられます。

// この時点ではStandaloneObject
let item = Item()
item.name = "なまえ"

let realm = Realm()
realm.beginWriteTransaction()
// addObject()の戻り値はVoid.
// addObject()の処理の中で、itemはManagedObjectになる.
realm.addObject(item)
realm.commitWriteTransaction()

途中でManagedObjectに変わり得るStandaloneObjectをスレッド間で受け渡す場合には、注意が必要です。

途中でManagedObjectに変わったインスタンスに対して、別スレッドから操作しようとするとRealmは例外を発生させます。

[共通] StandaloneObjectでaddOrUpdateすると、既存のRelationが消える

Java版にはcopyToRealmOrUpdate、Swift版にはaddOrUpdateObjectという便利なメソッドがあります。

これは、その名の通り、PrimaryKeyを持つモデルに対して、「既存オブジェクトが存在しなければ追加、存在すれば更新」という処理を行ってくれます。

これが便利なのは、例えば次のようなネストされたオブジェクトであっても、Itemに対するcopyToRealmOrUpdateを行えば、関連するCategoryについてもcopyToRealmOrUpdateを実行してくれるところです。

item.json
{
  "id": 1,
  "name": "項目1",
  "category": {
    "id": 1,
    "name": "カテゴリ1"
  }
}

WebAPIで取得して生成したStandaloneObjectを、copyToRealmOrUpdateに流し込んでローカルデータの同期を行う、というのがよくある使い方です。

追加については問題はないのですが、更新については、良くも悪くもStandaloneObjectのフィールドで全てを更新してしまうため、既存のRelationが消えてしまうという問題に注意を払う必要があります。

例えば次のようなアプリを考えてみます。

  1. トップ画面でItemのリストをWebAPI経由で取得し、copyToRealmOrUpdateする。
  2. トップ画面でItemがリスト表示される。(ニュース記事の一覧みたいなイメージ)
  3. ユーザが任意のItemを選択すると、Itemの詳細画面に遷移する。
  4. Itemの詳細画面では、ユーザがItemに関するコメント(メモ)を複数保存出来る。
    • このコメントは、アプリローカルのRealmに保存されるのみとし、サーバ側への保存は行われないものとする。
  5. リスト画面を再表示すると、ItemのリストをWebAPI経由で取得し、copyToRealmOrUpdateする。

Itemは、次のようにCommentを1対多のRelationとして保持しています。

Item.java
class Item extends RealmObject {
    ...
    private RealmList<Comments> comments;
    ...

    public RealmList<Comments> getComments() {
        return comments;
    }

    public void setComments(RealmList<Comments> comments) {
        this.comments = comments;
    }
    ...
}

以下のように素朴にcopyToRealmOrUpdateをしてしまうと、4で追加したはずのcommentsが、5の再読み込みで空になってしまいます。
(Comment自体は残っているが、Relationから解除されてしまう)

// トップ画面表示時のWebAPI読み込み処理
WebAPIClient.getItems(new Callback() {
    @Override
    public void onResponse(Item[] items) {
        Realm.getInstance(getContext()).executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                realm.copyToRealmOrUpdate(Arrays.asList(items));
            }
        });
    }
});

Realmからすると、与えられたStandaloneObjectで更新するしか無い(更新の差分を伝える仕組みがない)ので、仕方ない気もします。

これを回避するには、次のように手動でcopyToRealmOrUpdateに相当する処理を実装するしか無さそうです。

WebAPIClient.getItems(new Callback() {
    @Override
    public void onResponse(Item[] items) {
        Realm.getInstance(getContext()).executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                for(Item item : items) {
                    final Room managedItem = realm.where(Item.class).equalTo(
                    "id", item.getId()).findFirst();
                    if (managedItem == null) {
                        // 存在しなければ追加
                        realm.copyToRealm(room);
                    } else {
                        // 存在すれば更新
                        // StandaloneObjectから手動でManagedObjectに値をコピー
                        managedItem.setName(item.getName());
                    }
                }
            }
        });
    }
});
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away