今まで3年くらいCore Dataを使っていましたが、JOIN USを引き継ぎ、バージョン3.0として作り直すタイミングで、初めてRealmを使ってみました。
色々思うところがあったので、それについて書きます。
特にRealmについては経験がまだ多くないので、もし間違えなどあればご容赦&ご指摘いただければと思います。
両者を使った評価を先に記すと、こんな感じでした。
Core Data | Realm | |
---|---|---|
導入のしやすさ | x | o |
書きやすさ | △ | o |
DB系ライブラリとしての機能 | o | △ |
困った時の対応 | o (ネット情報多い) | o (サポート良い) |
Core Dataを使ってきた印象
まずCore Dataについての記述が多くなります。
興味無い方はスキップしてください。
Core Dataじっくり使ったことないと分からない記述があると思います。
APIが複雑
Swift - iOSアプリでデータベースを使う時に必要な知識をまとめてみた - Qiitaの前半に良い感じにまとめられていますが、全貌を把握するのがちょっと大変です。
NSManagedObjectContext
の扱いにコツがいる
Core Dataはスレッド間でNSManagedObjectContext
(以下context
と表記)を使い回すことが出来ません。
(context
とはRealmオブジェクトのようなものです。)
つまりUIスレッドをブロックしないようにバックグラウンドスレッドで保存処理などしようとすると、それ用のcontext
を生成する必要があります。
さらにそれら別のコンテキストの変更内容をマージする必要があります。
このあたりの記述がけっこう面倒かつ学習コスト必要で、それゆえCore Dataの敷居が高めだったのでは、と思っています。
NSManagedObjectContextDidSaveNotification
を用いて各スレッドのcontext
の変更監視してマージ
iOS 4以前のけっこう面倒なやり方で、とても面倒です。
NSManagedObjectContext
のperformBlock:
メソッドで各スレッドのcontext
の変更をマージ
iOS 5で導入されたAPIで、このおかげでかなり楽になりました。
以下の記事が詳しいです。
Core DataのAPIを組み合わせて下回り作るのが大変
iOS 5でかなり実装しやすくなったとはいえ、context
をどう扱うかという設計は実装者に委ねられています。
ベストプラクティスは Multi-Context CoreData | Cocoanetics だと思っています。
ここに書いてあることを理解して実装に落とし込む必要があり、なかなか大変です。
端的に言うと、Asynchronous Saving
の項に書いてあるように、以下を用意して上記のiOS 5のAPIを用いてハンドリングする、ということです。
- 保存用の一番親の
context
(バックグラウンドスレッド)- これによってUIスレッドで永続化処理でのブロッキングを防ぐ
- 表示用の
context
(UIスレッド) - 保存用の
context
(バックグラウンドスレッド)
MagicalRecordなどライブラリの利用
というわけで実装するの大変かつ車輪の再発明なので、ラップしてくれるライブラリ使うと便利です。
最近は一番実績のあるMagicalRecordを使っていました。
これは実際にソースを追って、上記のベストプラクティス通りになっていることを確認しました。
ただ、MagicalRecordはObjective-C時代のライブラリなので、Swiftでも動くとはいえ、型も弱いしイケてないです。(あくまでSwiftぽい書き方が出来ないだけで今でも良いライブラリではあります。)
さらに、MagicalRecord使った場合も、 iOS - CoreDataでatomicにsaveする - Qiita のケアなど必要です。
Swiftではgitdoapp/SugarRecordあたりが良さそうかなと思いましたが、ベストプラクティス通りの実装になっているかの確認が面倒だったりで使っていないです。
マイグレーション実装が大変
これも正確な実装をするには、Core Dataのマイグレーションのリファアレンスに目を通さなければ行けなくて大変でした。
結論からいうと、大半のスキーマ変更は、良い感じにスキーマ弄って軽量マイグレーション実行すれば速くて楽に出来るのですが、その知識を得るのに時間かかったり、軽量マイグレーションで対処出来ない場合などの定義が面倒だったりします。
パフォーマンスはどうか?
遅いといわれていますが、各種ベンチマーク見ても、そこまでいうほど遅くないと思いますし、DBアクセスのパフォーマンスがネックになるようなアプリは少ないので、あまり問題視していませんでした。
苦労ポイント中心に書きましたが、これら乗り越えれば、良い感じのライブラリなので、気に入って使っていました。
Realmについて
ようやくRealmについての話題です( ´・‿・`)
学習コストがとても低い
Swift Docs - Realm is a mobile database: a replacement for SQLite & Core Dataを、ちょこっと実際にコード書いて試しながら1時間くらい読んだら、大体理解出来ました。
感覚的には、MagicalRecord経由でCore Dataを扱う感じでした。
さらにSwiftぽく型が強めに書ける(RealmSwift
使った場合)ので、SugarRecord
の方がさらに感覚が近そうです。
というわけで、実際に利用してみて感じたのは、上であげたCore Dataで苦労してきた点が、ほぼ解消された形で使いやすいAPIセットとして提供されているということでした。
逆に、すでにCore Data使いこなしたり、MagicalRecord
にラッパーメソッドで便利にした状態と比べたら、正直そこまで大きな違いは感じ無かったです。
なので、Core Data未経験でDB扱いたい場合の選択肢としては良いと思いますが、Core Dataから乗り換えるほどかと言われると迷いますね、という印象です。
モデル定義が簡単・差分分かりやすい
Core DataはGUIでスキーマ定義出来るのですが、それをコードとマッピングしたり、GUIが便利というよりむしろ手間が増えるし、差分も読みにくいし、みたいな状態でした。
反面、Realmは https://realm.io/jp/docs/swift/latest/#section-4 に記載の通りシンプルにクラスにプロパティ記述していけば良いので楽でした。
サポートが良い
-
RealmのSlackによるサポートが手厚い
- @kishikawakatsumi さんからご返答いただけます
- 日本語訳が早い
- 上記の日本語リファレンスやブログなどすぐ日本語で公開されます
個人的には英語読むのは困りませんが、日本語の方が楽に速く読めるのでありがたいです。
マイナス点
率直にマイナスに感じた点を上げていきます。
以下は、0.96時点のもので、現バージョンにおける制限事項に記載の通り、今後改善するものが多いと思います。
現在のRealmはベータバージョンであり、バージョン1.0のリリースに向けて、継続的に機能追加および不具合の修正が行われています。
まだ0系のバージョン
もうすぐ1.0に到達しそうですが、まだベータバージョン
の0.96です。
ただ、すでにRealmを採用してリリースされたアプリがけっこうあるので、そこまで困ったことにはならないと思いますが、多少リスクとしてとらえていた方が良いと思っています。
この記述をしっかり読んでそれを承知で使った方が良いかと思います。
Realmをプロダクション環境で使うことはできますか?
Realmは、2012年から商用のプロダクトで利用されています。
現在のRealmを利用する場合は、RealmのObjective-C、およびSwiftのAPIが、コミュニティのフィードバックを受けて変わりうるものだとしてお使いください。 機能追加、不具合の修正も同様に考えてください。
きめ細やかな変更通知が未サポート
Core Dataは、特にNSFetchedResultsControllerによる特定のコレクション監視が良く出来ていた一方、Realmの場合現状は「全体のどこかで変更があった」というすごく大ざっぱな単位でしか通知が来なくてかつその変更内容も分からないです。
なので、大ざっぱにUI更新するか、あるいは自前で細かい変更検知を出来る仕組みを整えるなど対応が必要になります。
リレーション周りが弱い
https://realm.io/jp/docs/swift/latest/#section-4 のリレーションシップ(関連)
に記載の通り、モデル定義としてはリレーションに対応しています。
しかし、Core Dataとは違い以下の状態なので、それをフレームワークがサポートしてくれることに慣れている感覚では面倒かつそれに起因するミス・バグなどに多少悩まされました。
- リレーションは自分で管理する必要がある
- コレクションの値の追加・削除は手動で行う
- カスケード削除未対応
別スレッドのrealm
オブジェクトがコミットした内容が伝播するのが遅れることがある
こちらは、個人的にかなりハマりました。
書き込み用のスレッドでそのrealm
オブジェクトでコミット完了後、すぐにUIスレッドのrealm
オブジェクトでデータアクセスすると内容が更新前のものだったりしました。
クライアントコードのバグを疑ってけっこう苦労しましたが、以下の制約のようです。
refresh()
を呼ぶようにして対応しましたが、コミットのオプションとして他のrealm
に全て変更が伝わるまで待つようなオプションが欲しいなと思いました。
https://realm-public.slack.com/archives/general/p1439446195000912 (RealmのSlackに登録していないと見られないです)
原理的には別スレッドの変更はトランザクションがコミットされた時点で、他の同じRealmに変更が通知されます。
ただ、それが遅れる場合がある、というか
今回だと、メインスレッドは制御を戻さないので、いったんこのメソッドを抜けるか、ランループに制御を戻さないと、
変更を受けられないと思います。
なので、タイミングの問題のときはあえてrefresh()
を呼んで、古いデータが返ってくるのを防ぐことができます。
毎回refresh()
を呼ぶようなラッパーやエクステンションを作ってるひともいたと思いますね。
マイグレーション時にデフォルト値が無視される
昨日これに起因するエンバグを見つけて気づいたばかりなのですが、
dynamic var isFoo: Bool = true
のようなプロパティを増やした時、新規インストールでは問題無いのですが、アップデートの場合 false
になってしまいました。
よくドキュメント読んだりIssue検索したら、以下のように現状の制限とのことでした。
注意することとして、初期値の設定はマイグレーションの最中には適用されません。 この問題は不具合としてこちらのIssue #1793で管理されています。
Use default property values when adding a new column in a migration · Issue #1793 · realm/realm-cocoaが対応されるのを待ちましょう。
デフォルト値に関する記述をdynamic var isFoo: Bool = true
と二重に書かなくてはいけなくて微妙ですが、とりあえず以下のようにマイグレーションコードを書けば対応出来ます。
ただ、Core Dataではこれは自動マイグレーションで面倒見てくれるので、それと同じ感覚で実装していると落とし穴になるかと思いますので注意です。
let config = Realm.Configuration(
schemaVersion: nextVersion,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < nextVersion) {
migration.enumerate(User.className()) { old, new in
guard let new = new else {
assert(false)
return
}
new["isFoo"] = true // newオブジェクトをUser型にキャスト出来なかったのでプロパティは文字列で扱う
}
}
})
Realm.Configuration.defaultConfiguration = config
というわけで、以下のような良い評判の多いRealm
ですが、実利用するとけっこうハマったり、マイナス点もあるなと思いました。
- 使いやすい
- パフォーマンスが良い
もちろん、Realm
についてネガティブな意見を述べたいわけではなく、こういうこと踏まえつつ適切に利用していけば良いなと思います。
僕は、アプリの要件次第ではありますが、継続してRealm
使っていこうかなと思っています。
特に、現バージョンにおける制限事項が解消されていくことを願っております