Realmの隠れた重要ポイント

# About

[Realm](https://realm.io/)は、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インスタンスの管理下に置かれているかどうか**、ということなのですが、サンプルコードで見れば一目瞭然です。

```java
// 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のサンプルコードで確認します。

```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に追加する処理を実装してみます。

```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);

        // 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の状態が破壊的に書き換えられます。

```swift
// この時点では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`を実行してくれるところです。

```json: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として保持しています。

```java: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から解除されてしまう)

```java
// トップ画面表示時の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`に相当する処理を実装するしか無さそうです。

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