Realm Java 3.0: コレクション通知、スナップショット、関連によるソート
本日、Realm Java 3.0をリリースしました。このリリースでは、関連をつかった RealmResults
のソートや、RealmResults
、RealmList
などのコレクションが更新された際の通知で更新内容の詳細(追加/削除/更新)を受け取ることができる機能を追加しています。これらの機能によりRealmが提供するライブなコレクションの活用の幅が大きく広がります。
コレクションの詳細な変更通知(Fine-grained Collection Notification)
これまでRealmでは、Realm
、RealmObject
、RealmResults
クラスに対してRealmChangeListener
インターフェイスをつかった通知機能を提供してきました。このインターフェイスには一つ大きな制限があります。それは、呼び出されることにより 何か が変わったということはわかるのですが、何がどのように変わったかについてはわからないという点です。変更があったことがわかれば十分という場合も多いのですが、もっと詳しい情報が必要になるケースもあります。たとえば、RecyclerView
で変更内容に応じたアニメーションを行いたい場合などです。これまでは、realm.copyFromRealm()
とAndroid Support LibraryのDiffUtil
を組み合わせることで実現することもできましたが、望ましいものではありませんでした。
本リリースでは、コレクションの詳細な変更通知(Fine-grained Collection Notification)機能を追加しました。RealmResults
に登録するための新たなインターフェイスにより実現されるもので、どの要素が追加、削除、変更がされたかについての情報を受け取ることができます。これにより、実際に変更があった要素についてのみ更新を行うことができるようになります。
併せて、Android AdaptersライブラリについてもRealmRecyclerViewAdapter
が詳細な変更通知を利用するように更新し、スムーズなアニメーションを行うようになっています。次の動画で、実際にどのような違いが出るかを見ることができます。
private final OrderedRealmCollectionChangeListener<RealmResults<Person>> changeListener = new OrderedRealmCollectionChangeListener() {
@Override
public void onChange(RealmResults<Person> collection, OrderedCollectionChangeSet changeSet) {
// `null` means the async query returns the first time.
if (changeSet == null) {
notifyDataSetChanged();
return;
}
// For deletions, the adapter has to be notified in reverse order.
OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges();
for (int i = deletions.length - 1; i >= 0; i--) {
OrderedCollectionChangeSet.Range range = deletions[i];
notifyItemRangeRemoved(range.startIndex, range.length);
}
OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges();
for (OrderedCollectionChangeSet.Range range : insertions) {
notifyItemRangeInserted(range.startIndex, range.length);
}
OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges();
for (OrderedCollectionChangeSet.Range range : modifications) {
notifyItemRangeChanged(range.startIndex, range.length);
}
}
};
コレクションの詳細な変更通知は、従来の変更通知のためのリスナーと同じように動作します。たとえばfindAllAsync()
などにより非同期クエリが実行された場合変更内容を計算する処理はバックグラウンドスレッドで実行され、その後呼び出したスレッドで通知が行われます。DiffUtil
を使った場合のような余分なメモリのオーバーヘッドはありません。
コレクションスナップショット
Realmの重要なデザインコンセプトの一つに、オブジェクトやコレクションがライブであるというものがあります。これは、たとえば検索結果を保持するRealmResults
は、データベース内のデータが変更されるとそれが自動的に反映されるということです。これはリアクティブアーキテクチャにとってとても効果的で、RealmChangeListener
を登録するだけで検索結果に影響する変更に対する通知を受け取ることができます。
しかし、このライブであるという特性が問題を引き起こす場合もあります。たとえばライブなコレクションに対してループで順番に要素にアクセスする場合です。次にそのような場合のコード例を示します。
RealmResults<Guest> uninvitedGuests = realm.where(Guests.class)
.equalTo("inviteSent", false)
.findAll();
for (i = 0; i < uninvitedGuests.size(); i++) {
realm.beginTransaction()
uninvitedGuests.get(i).sendInvite();
realm.commitTransaction();
}
ここでは、招待されていない全てのゲストに対して招待を行うため未招待のゲストを取得しています。ここでuninvitedGuests
が通常のArrayList
であれば、期待通り動作し全てのゲストに招待が行われます。
ところが、RealmResults
の場合は変更が自動的に反映されるため、ループの中でトランザクションを実行するとループが回る度にuninvitedGuests
に変更が反映されてしまいます。これにより招待が行われたゲストはリストから取り除かれ、要素が1つずつずれます。今回のコードでfor
文はルーブのたびにインデックスを増やしているため、意図しない要素へアクセスすることになってしまします。結果として、全体の半分の人にしか招待が行われず、あきらかに意図した動作とは違います。
これに対しては2つの解決策が存在します。
// 1. ループ自体を一つのトランザクションの中で実行する
realm.beginTransaction()
for (i = 0; i < uninvitedGuests.size(); i++) {
uninvitedGuests.get(i).sendInvite();
}
realm.commitTransaction();
// 2. リストに対して後ろから逆順にアクセスする
for (int i = uninvitedGuests.size() - 1; i >= 0; i--) {
realm.beginTransaction()
uninvitedGuests.get(i).sendInvite();
realm.commitTransaction();
}
これらの方法はわかりやすいものではないため、Realm Java 0.89のリリースの際にRealmResults
に対するすべての更新を次のイベントループまで遅延させることを決めました(Handler.postAtFrontOfQueue
を用いて実装されています)。
これはシンプルなfor
ループが意図通りに動作するという利点をもたらしましたが、RealmResult
が最新ではない状態が発生し、たとえばオブジェクトを削除した場合にもRealmResult
中に無効なオブジェクトとして次のイベントループまで残り続けてしまうという欠点ももたらしました。
realm.beginTransaction();
guests.get(0).deleteFromRealm(); // Delete the object from Realm
realm.commitTransaction();
guests.get(0).isValid() == false; // You could now get a reference to a deleted object.
3.0においてこの判断について再度検討を行い、RealmResults
を以前のように完全にライブなものに戻すことを決めました。ただし、コレクションに対して通常通りfor
ループを使えるようにするため、createSnapshot()
メソッドを追加しています。このメソッドが返すOrderedRealmCollectionSnapshot
はこれまで通りの変更の影響を受けないコレクションとして使用することができます。for
文の中で要素を変更する場合は、このメソッドを用いて作成したスナップショットを使用してください(ただしループの中でトランザクションを作成することはほとんどの場合においてアンチパターンであることに変わりはありません)。
RealmResults<Person> uninvitedGuests = realm.where(Person.class).equalTo("inviteSent", false).findAll();
OrderedRealmCollectionSnapshot<Person> uninvitedGuestsSnapshot = uninvitedGuests.createSnapshot();
for (int i = 0; uninvitedGuestsSnapshot.size(); i++) {
realm.beginTransaction();
uninvitedGuestsSnapshot.get(i).setInvited(true);
realm.commitTransaction();
}
イテレーターはこのスナップショットを内部で使用しているためfor-each
は意図通りに動作します。
realm.beginTransaction();
RealmResults<Person> uninvitedGuests = realm.where(Person.class).equalTo("inviteSent", false).findAll();
for (Person guest : guests) {
realm.beginTransaction();
uninvitedGuests.setInvited(true);
realm.commitTransaction();
}
この変更を行った理由はいくつかありますが、そのうちの一つはコレクションの詳細な変更通知を実装するにあたって内部のリファクタリングが必要だったということがあります。他には、RealmResults
とRealmList
の両方が完全にライブなものになるため、Realmの全てのクラスおいて完全に同じセマンティクスを提供できるというものがあります。これにより、ドキュメントも理解しやすいものにできます。以前の動作は混乱を招く場合もありましたが、完全にライブであることとそれに対するドキュメントの改善により、今まで以上に強力なAPIを提供できると考えています。
今回の変更が既存のコードベースに影響する場合があることは理解してますが、その影響はfor
ループの中でトランザクションを作成している場合に限られるはずです。それ以外のコードについてはこれまで通り動作するので、長い目で見ればよい変更であったと思っていただけることを期待します。
関連プロパティを用いたソート
これまで、RealmResults
の要素のソートは対象クラスが直接持つプロパティでのみ行うことができました。Realm Java 3.0では、対象クラスが関連 関連として持っているクラスのプロパティを用いてソートできるようになりました。
たとえば以下のように、Person
のコレクションをそれぞれが関連として持っているdogオブジェクトの年齢プロパティによってソートできるようになります。
RealmResults<Person> persons = realm.where(Person.class).findAllSorted(“dog.age”, Sort.ASCENDING);
これら以外についても、直近のリリースでさまざまなバグフィックスを行っています。変更の完全なリストについては、changelogをご確認ください。
お読みいただきありがとうございます。 Realm で素晴らしいアプリケーションを作りましょう!お困りの際はStack Overflow(日本語)、 Slack(日本語)、Twitter(日本語)、GitHub(英語)でご相談ください。