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の通信処理では、以下のような設計も可能になります。
- バックグラウンドの通信処理でJSONのParseとStandaloneObjectの構築を行う
- メインスレッド側でStandaloneObjectを受け取る
- バックグラウンドで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
を実行してくれるところです。
{
"id": 1,
"name": "項目1",
"category": {
"id": 1,
"name": "カテゴリ1"
}
}
WebAPIで取得して生成したStandaloneObjectを、copyToRealmOrUpdate
に流し込んでローカルデータの同期を行う、というのがよくある使い方です。
追加については問題はないのですが、更新については、良くも悪くもStandaloneObjectのフィールドで全てを更新してしまうため、既存のRelationが消えてしまうという問題に注意を払う必要があります。
例えば次のようなアプリを考えてみます。
- トップ画面でItemのリストをWebAPI経由で取得し、copyToRealmOrUpdateする。
- トップ画面でItemがリスト表示される。(ニュース記事の一覧みたいなイメージ)
- ユーザが任意のItemを選択すると、Itemの詳細画面に遷移する。
- Itemの詳細画面では、ユーザがItemに関するコメント(メモ)を複数保存出来る。
- このコメントは、アプリローカルのRealmに保存されるのみとし、サーバ側への保存は行われないものとする。
- リスト画面を再表示すると、ItemのリストをWebAPI経由で取得し、copyToRealmOrUpdateする。
Itemは、次のようにCommentを1対多のRelationとして保持しています。
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());
}
}
}
});
}
});