AplosというTwitterクライアントでCoreDataを使用していたのですが、いくつかの問題がありRealmに移行しました。詳しい経緯は前回のCoreDataとRealmのベンチマークで性能を比較をご覧ください。
今回は更にRealmについて掘り下げて行きたいと思います。
※この記事はRealmのバージョン0.88.0について記載しています。Relamの開発は活発なので試す場合はバージョンに注意してください。
#Realmのメリット/デメリット
##メリット
- SQLiteより速い。
- DBが肥大化してもパフォーマンスへの影響が少ない。
- PrimaryKeyがある。
- コンテキスト(NSManagedObjectContext)なしでオブジェクトが生成できる。
- RLMResultsが素敵。
- NSFetchedResultsControllerのモデル版みたいな挙動で、データベースが更新されたらフェッチしなくても保持しているRLMResultsに自動で反映される。
- KVOは未対応なので、更新タイミングは通知を利用する。
- フェッチを連続したクエリで実行できる。(もちろんNSPredicate、NSCompoundPredicateなどの利用も可能)
- 専用のデータベースブラウザ
- Android対応
##デメリット
- 超貧弱なFetchRequest
- Limitすらない。
- 制限付きなNSPredicate(後述)
- CoreDataのObjectIDの様な仕組みがPublicメソッドにないので、シンプルにスレッド間でオブジェクトを移動する方法がない。(後述)
- Delete Ruleがない(後述)
- テーブル定義を分けられない。(CoreDataみたいにmomdを分けて別定義ができない)
##その他の特徴
- 例外投げまくり。Realmを少し使えばすぐわかると思いますが、些細なエラーでも即例外が投げられますw わかりやすいと言えばわかりやすいのですが、仕様が把握できていないうちはガンガン例外投げられるので最初はちょっと戸惑いました。
- 開発は比較的速く1,2ヶ月ペースでメジャーバージョンアップされています。ChangeLogがしっかり書かれているので対応しやすいと思います。
#Tips
##制限付きNSPredicate
初めドキュメントを見たときNSPredicateがサポートされているということで喜びましたが、実際はかなり制限が多いです。現状だとNSPredicateのフォーマットが使えて今後もこの方向性で行きますという意思表示くらいに個人的には思います。ざっとした仕様はこちらをどうぞ。
ドキュメントに掲載されていない制限として、例えば配列に対するクエリがまだサポートされていないとかがあります。
// Realm(0.88.0)ではUnsupported operator. Multi-level object equality link queries are not supported.
[Tweet objectsWhere:@"retweeted_status.user != nil"];
そういう場合でもメッセージ付きで例外を投げてくれるので助かります。ここらへんの仕様はドキュメント化されていないので、コードを見るか実際に試して例外が起きるかを確認するのが早いと思います。
また@countなどの関数系も使えません。例えば配列で1つ以上要素を持っているものをフェッチする場合は以下のように、現状使えるNSPredicateの使用を組み合わせて対応するしかなさそうです。
// hastagsでいずれかの要素が空要素でない => 1つ以上要素を持つ
ANY hashtags.text != ''
制限付きですが、現在使用可能なNSPredicateフォーマットの組み合わせでたいがいは乗り切れるレベルではあります。
##スレッド間のオブジェクトの移動
###RLMRealm
RLMRealmは異なるスレッド間の移動ができず例外が投げられます。対処法は各スレッドごとにRLMRealmへのpathを利用して生成します。(RLMRealmはスレッドごとに内部的にCacheされている。)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
RLMRealm *realm = [RLMRealm realmWithPath:realmPath];
};
###RLMObject
RLMObjectは異なるスレッド間の移動ができず例外投げられます。
CoreDataのNSManagedObjectはスレッドセーフなobjectIDを利用してスレッド間でのオブジェクトの移動が可能ですが、RealmではobjectIDの様な仕組みがPublicメソッドにはないので、代替案としてprimaryKeyを利用する必要がありそうです。
実装例としてYSRealmStoreから抜粋します。
primaryKeyが必須なので、primaryKeyがないRLMObjectはメインスレッドでフェッチするしかなさそうです。
- (void)fetchOperationWithObjectsBlock:(YSRealmOperationObjectsBlock)objectsBlock
completion:(YSRealmOperationFetchCompletion)completion
{
__weak typeof(self) wself = self;
dispatch_async([[self class] operationQueue], ^{
NSMutableArray *values;
Class resultClass;
NSString *primaryKey;
// RLMObjectを取得。バックグラウンドスレッドで重いフェッチを実行
id results = objectsBlock ? objectsBlock(wself, [wself realm]) : nil;
if (!wself.isCancelled && results) {
if (![results conformsToProtocol:@protocol(NSFastEnumeration)]) {
results = @[results];
}
NSParameterAssert([results isKindOfClass:[NSArray class]]
|| [results isKindOfClass:[RLMArray class]]
|| [results isKindOfClass:[RLMResults class]]);
values = [NSMutableArray arrayWithCapacity:[results count]];
RLMObject *result = [results firstObject];
resultClass = [result class];
primaryKey = [resultClass primaryKey];
if (result) {
if (resultClass && primaryKey) {
// primaryKeyに対応していれば全オブジェクトのvalueを取得
for (RLMObject *obj in results) {
NSParameterAssert([obj isKindOfClass:[RLMObject class]]);
[values addObject:[obj valueForKey:primaryKey]];
}
} else {
DDLogWarn(@"%s; Primary key is required; class = %@, primaryKey = %@", __func__, NSStringFromClass(resultClass), primaryKey);
}
}
}
dispatch_async(dispatch_get_main_queue(), ^{
RLMResults *results;
if (!wself.isCancelled && [values count] > 0 && resultClass && primaryKey) {
// valuesを元にフェッチ
results = [resultClass objectsInRealm:[wself realm]
where:@"%K IN %@", primaryKey, values];
}
if (completion) completion(wself, results);
});
});
}
##Delete Ruleがない
-
Delete Ruleがないため子オブジェクトが削除されずゴミデータが増え続ける。対処法は親オブジェクトを削除する際に子オブジェクトも手動で削除するしかない。
-
オブジェクトの追加時にも問題が発生する場合がある。例えば親オブジェクトをUpdateし子オブジェクトが新しく追加された場合に、古い子オブジェクトはどこともリレーションがないデータとなってしまいます。これは親オブジェクトをUpdateする前に子オブジェクトを削除するか、子オブジェクトにPrimaryKeyを定義することで防ぐことができます。
これらは将来的に改善されそうですが、現状は手動で対応する必要があります。
##モデルオブジェクトの生成について
- NSNullはサポートされておらずオブジェクトに含まれていると例外が投げらます。APIなどで取得したJSONにnullが含まれている場合、それをどう扱うかを考える必要があります。
- Aplosではこのnullや他にもいくつか問題が考えられたので、手動で初期化しています。
参考: iOSでパース後のJSONオブジェクトにNSNullが含まれている場合の各種対処方法まとめ
#永続化の比較
##NSUserDefaults
###選択理由
- データ量が少ない。
- 複雑なフェッチ条件がない。
- 永続化のコード量が簡単で少ない。
###デメリット
- メモリに展開できないデータ量は使用不可。
- 特定のデータが欲しい場合に自前で実装が必要。(NSPredicateも制限付きだしコード量がどうしても多くなる)
##FMDB(SQLite)
###選択理由
- SQLに慣れている。
###メリット
- PrimaryKeyやAutoincrementなどデータベースが備えている機能が使える。
###デメリット
- マルチスレッドの処理は自前。
- コア部分のコード量が多い。
- 素でSQL書くのか・・・
##CoreData
###選択理由
- Frameworkの手厚いサポートが欲しい。
- NSFetchRequestでSQLでできることはほぼ可能
- Delete Rule
- Undo/Redo
- iCloud同期
- テーブル定義をGUIで設計したい
###デメリット
- 処理は重め。
- DBが肥大化するとパフォーマンスへの影響が著しい。
- 比較の中で一番学習コストが高い。
- マルチスレッドの処理は自前。
- コア部分のコード量が多い。(ミドルウェアである程度軽減できそう)
- MagicalRecord
- NLCoreData (シンプル)
- DBAccess (使ったことないけど気になる)
- メインスレッド上のコンテキストへのマージコストを考慮する必要がある。
- マルチスレッド処理についてはこのような方法でバックグラウンドでデータベースを更新する方法がありますが、メインスレッドへの影響を完全になくすことは無理です。
##Realm
###選択理由
###デメリット
- バージョン1.0に到達してない。
- きちんと使おうとすると学習コストはそこまで低いわけではない。
- 前述のデメリットのようなRealm特有の仕様に気を使う必要がある。
#まとめ
- FMDBとCoreDataの2つにはない速さとモダンな設計をどう感じるかで、Realmを採用するか決まるかなって思います。個人的にはiCloud同期やUndo/Redoが必要でない限りCoreDataを使う気にはなれないです。
- Realmがまだ発展途上というリスクも十分に理解する必要もあります。メジャーバージョンアップで破壊的な変更もされたりするので、ちゃんと追ってないとツラいかもしれません。
#最後に
- 一連のオペレーションをまとめて使いやすくしたYSRealmStoreを作りました。
以下の様なBlocksベースで実装してあります。
/* Sync */
[store writeTransactionWithWriteBlock:^(YSRealmWriteTransaction *transaction, RLMRealm *realm) {
// Can be operated for the realm. (Main thread)
[realm addOrUpdateObject:[[Tweet alloc] initWithObject:obj]];
}];
/* Async */
[store writeTransactionWithWriteBlock:^(YSRealmWriteTransaction *transaction, RLMRealm *realm) {
// Can be operated for the realm. (Background thread)
[realm addOrUpdateObject:[[Tweet alloc] initWithObject:obj]];
} completion:^(YSRealmStore *store, YSRealmWriteTransaction *transaction) {
}];
- 本家のExamplesとは違うアプローチを載せていますので、本家と合わせてご覧ください。
- YSRealmStoreはRealmがバージョン1.0に達するまでは、最新更新に合わせた最善な方法を下位互換を気にせず対応していきますので了承ください。