iOSでParseと Parse Local Datastore最近2ヶ月ぐらい触ってるので、わかったことを書いてみます。間違いがわかれば嬉しいので、突っ込み歓迎。

さて、Local Datatoreは発表された時から僕はワクワクドキドキしていて、そのドキドキ感をブログで書いてきました。

Parse Local Datastore for iOSがやっと出た

しかし、実際には使ったレポートは書いてなかったんですね。なぜなら使ったことがなかったから。

しかし、最近はというと、2ヶ月近く前から毎日ParseとCoreDataの連携するコードを書きまくっていて、いろいろわかってきたことがあったので書いてみる。

基本的には、ParseのForum、Google Group、StackOverFlowで書かれている先人の知恵を借りながら、そこで書かれている現時点のベストプラクティスを参考にコード書いてる。

@目的
Taxnoteっていう経費記帳アプリに自動同期機能つける。

@背景
元々はCoreDataオンリーのアプリ。CoreData + Parse (+ Local Datastore)で自動同期機能をつけようとしてる途中。

@目指す動き
オフライン時はローカルにデータを保存し、オンラインな時にサーバと同期する。ようは、オフライン時でもキビキビ動いて、ユーザは意識しないまま、勝手に裏で同期が動く。

いろいろググったけど、Parse Local Datastoreをまとめたブログ記事は英語でもあんまりないので、参考になれば幸いです。

というわけで、まずは、Parse Local Datastoreの話を中心に書いていきまして、その後に、Parseの普通のメソッドの話を書いていく。

CoreDataとParse繋げるライブラリないの?

そもそも、僕はあんまり自分でコード書くより、もっとプログラミングが上手い人のライブラリを使いたいんですが、残念ながら良さげなのがありませんでした。

FTASyncは数年前に書かれてメンテはされてないし、すごくベーシックなことしかできないみたい。また、NSIncrementalStoreを使ったライブラリも試してみたんだけど、これも今はメンテされてなくて、上手く動かなかった。

Local Datastoreがでたから、みんなこっち使おうね。みたいな流れらしい。ただ、Local Datastoreはパフォーマンスの面で致命的な問題があり、使えず、しゃあないから自分で書かないといけなくなったと。

あとは、同期ってデリケートだから、自分で書いたほうがバグとかも対応できていいかなと思ったら、予想以上に大変で時間かかってる。

Local Datastoreのパフォーマンス

100個ぐらいのデータなら問題ないけど、1000個ぐらいセーブした時点で遅くなる。1500個ぐらいのデータを一気に取得しようとする時、iPhone6Plusで2秒近くかかっちゃう。CoreDataと同じことしようとしたら、雲底の速度差。

なので、Taxnoteのメインのデータのクラスには使えず(仕訳帳のクラス)、大幅に書き直す羽目に。

Taxnoteだと、メインの入力していくデータには使わず、勘定科目リストなどのデータのみに使うことに。

なぜかというと、科目リストのクラスと、メインの帳簿クラスはRelationで繋がっているので、ParseにSaveするたびに、Relationの呼び出しする時にLocal Datastor使うと便利だから。

Pinを分けたらパフォーマンス改善しないの?

Local DatastoreにはPinといって、それぞれ保存する領域を分けられる機能がある。

これを使って、カテゴリクラスのpin、エントリクラスのpinみたいに、Pinを分けたらパフォーマンス改善するかなと思って試してみた。

結果的には改善せず、Local Datastoreのパフォーマンスはこの時点で諦めた。なので、Local DatastoreをCoreDataとか、オフラインデータベースの代替品として使おうとするのは無理ゲーだと思う。

最初は自分もそういうふうに使えんじゃねえかと夢想したけど。

Local Datastoreをsyncするメソッドないの?

これ、Local Datastore使い始めたら思うんだけど、ParseをLocalに保存したものを、サーバ側と同期するメソッドがないんですよ。

Local Datastore Syncみたいな。

オフラインの時はLocal Datastoreに保存して、まだサーバに保存されてない部分だけ明示的に同期するとか。そういうメソッドがあれば便利なんだけど、残念ながらない。

どうすりゃいいかというと、結局、Parseのモデルクラスで、needSyncみたいな項目を作って、自分で needSync = NOのやつを後から呼び出して、syncされてないやつだけちゃんと保存しておくとか。

こういう面倒なことをしないといけない。こういうメソッドは必要だろみたいなことは、Parseのフォーラムでも書かれてます。まじで必要。

ここまで、Local Datastoreの話で、以下は普通のParseのメソッドについての話。

SaveEventuallyの弱点

Parseには、SaveEventuallyっていう、データを保存した時、もしオフラインだったら、あとでオンラインなった時に勝手にサーバに保存しときますよっていう素敵なメソッドがある。

これは、ドキュメント読む限りでは神メソッドだ。

ただ、オフライン時に何回か連発で呼んだり、呼んだ後にアプリをタスクから強制終了させたりしたら、Parseサーバに反映されないデータが出たりする。

いろいろテストして、信頼性が低いので結局使わないことにしました。CoreData側でneedSaveとか、needSyncとかのattribute作って、自分でオフライン対応することに。

SaveInBackGroundもSaveEventuallyのようにオンラインなったら自動saveされる動きするので注意。

まあ、すごく厳密にデータSaveされなかった漏れがないようにしたかったから、Taxnoteでは使わないことにしたけど、そこまで厳密にする意味ないような時はガンガン使ってよいと思う。

たいていの状況では普通にあとからsaveされるし。

複数データを一回でsaveAllした時のリミット制限は?

Parseには、1秒間に何リクエストまでという制限がある。無料プランだと少なくて、有料で段階的に上げていける。

例えば、100個ぐらいのデータを PFObject saveみたいな感じで100回繰り返せば、100リクエストになっちゃうのはわかるとして、100個のデータをまとめて、PFObject saveAllで保存したらリミット制限にはどう影響するか?

これは、saveAllでまとめて保存しても、保存するデータを個別にカウントする。だから、無料プランで、1800個ぐらいのデータを一発のsaveAllで保存しようとすると、リミットにひっかかりましたのエラーが出る。

まあ、そんなことは起きないアプリなら問題ないんだけど、Taxnoteは二年分の仕訳帳データが既にあったら、全部で1800件ぐらいなっちゃうんですよ。それを自動同期使う時に一気にアップロードしようとすると、エラーメッセージでてわかった。

解決策は、有料プランでリミットを上げるか、複数回にリトライするか。今、ここやってる途中。

データ削除の設計

これが厄介なんだけど、デバイスAとデバイスBでのデータを同期する時、片方で一つのデータを削除したら、もう片方でもデータを削除しないといけない。

例えば、デバイスAである一つのデータを削除した時、サーバ側でそのデータを本当に削除しちゃうと、デバイスBで同期する時に、そのモデルクラスにある全てのデータをシンクロしないといけなくなるんです。

これはパフォーマンス的によろしくない。

というわけで、実際にサーバ上ではデータを削除せず、isDeletedみたいなカラムを作って、ソフトデリートというデータベース設計の手法を使うらしい。

これなら、デバイスBはisDeleted = YESとなっているのだけQueryで検索かけて同期し、デバイスAで削除されたデータをデバイスBでも同期した時に削除するという動きができる。

まあ、これがベストなやり方だとは思うんだけど、定期的にサーバ側でisDeleted = YESになっているデータをクリーンアップする必要があるんですよね。Cronとか使って。このタイミングが悩み中。

これはParseは直接関係ないけど、同期アプリ作る時はぶち当たる壁かも。

まとめ

Parse Local Datastoreは1000件ぐらい保存したら遅くなるので、この遅さが問題になるアプリなら使いにくい。結局、CoreDataとParseの連携部分のロジックを書かないといけない。

でも、局所的に使うには便利です。

Taxnoteだと、仕訳帳一覧のモデルクラスと、科目のモデルクラスでRelationsつなげないといけないので、科目の部分だけLocal Datasore使うのは意味あった。

ちなみに、TaxnoteはもともとCoreDataで作ってたけど、新しいアプリなら、Realm + Parseで作ってたかも。Realm評判いいし。


*確定申告を楽にするTaxnoteなど作ってます。自己紹介はこちら。プログラマもゆる募