Firestoreで既存機能の一部をリプレースしましたTL;DRこんにちは、スタメンの津田です。スタメンが提供しているサービス、TUNAGには、チャット機能があります。Reactで構築されていたのですが、特にモバイルアプリケーションのユーザー体験を向上させたいということで、昨年末にモバイルアプリケーションチャット機能のネイティブ化と、それに伴うサーバー側の一部再構築を行いました。Firestoreをクラウド上のキャッシュデータのような形で利用したため、その利用方法について紹介させていただきます。iOSアプリのチャット機能をVIPERアーキテクチャで開発した話」で紹介しています。データベースで、SDKを通してモバイルアプリケーションから直接アクセスできます。DBを用意せずにモバイルアプリケーションのみで他ユーザーとのデータ共有を含むアプリケーションを作成できます。Firestoreには以下のメリットがあると考え、採用しました。Firestoreからの引用です。データ同期を使用して、すべての接続端末のデータを更新します。回限りの取得クエリを効率的に実行するようにも設計されています。アプリでアクティブに使用されるデータをキャッシュします。これによりアプリは、端末がオフラインになっている場合でもデータの書き込み、読み取り、聞き取り、クエリを実行できます。ローカルに行われた変更がすべて同期されます。Firestoreをモバイル端末に対するデータの配信・同期の仕組みとして利用することにしました。具体的には、以下の図のような構成になります。API経由で行います。Firestoreへ直接書き込むことはありません。Firestoreを経由して伝達されます。Firestoreへのデータ同期は、非同期処理のGem、sidekiqを利用しました。Firestoreを利用しない場合、オフライン時キャッシュや、リアルタイムのデータ取得などの機能をモバイルアプリケーション側で実装する必要があるのはもちろん、サーバー側でもデータ取得の効率等を考えてAPIを追加実装する必要があります。Firestoreにも制限はありますが、メリットがデメリットを上回ると考えました。同期するデータ同期するデータは、基本的にはRDBにあるもののコピーなのですが、「セキュリティルール」と、「平坦化」に留意する必要がありました。Firestoreは単体で特定のユーザーに対して公開可能な情報を判断できる必要があります。ルールを使ってみるで説明されているセキュリティルールでデータに対するアクセス制御が表現できるように設計する必要がありました。Firestoreのデータはファイルシステムのようなツリー状になっているので、「あるパスに対しての権限をカスタム関数で表現できるか?」を考えると判断しやすかったです。平坦化データの内容に関しては、どこまでデータの平坦化をするかを考える必要がありました。FirestoreはNoSQLデータベースなので、RDBのようなデータのジョインは得意ではありません。Firestoreへ同期する際は全てを結合して、一つのドキュメントとして同期する必要があります。Firestoreのドキュメントとしては、以下のように平坦化。Firestoreの複数のドキュメント更新の引き金となるケースが出てきます。Firestoreへ同期し直す必要が発生します。このようなケースの洗い出しは、データの構造によってはかなり大変になります。基本的にデータは平坦化するのですが、5つの部屋に入室しているユーザーがいたとして、その5室すべてに、ユーザーの情報を登録していると、データの重複が多くなってしまい、また同期も大変になります。いったケースではデータの平坦化をせず、クライアント側でジョインしてもらうような選択も行いました。Firestoreへ認証させる必要があります。トークンを作成する」には、以下のように記載されています。Token(JWT)を使用したユーザーまたはデバイスの認証が可能であるため、認証に対する完全な制御が得られます。メソッドで認証するためにこのトークンを使用します。エンドポイントを作成する必要があります。API)を追加しました。トークンの作成」の例を参考にして実装しました。同期の制御データの同期に関しては、各モデルで若干の違いはあるのですが、概ね以下のような方針で共通の処理を作り、利用しました。対象のドキュメントを更新する必要があるかどうかを、RDB側で判断できる情報を持たせるリトライを前提とし、何度実行しても問題ないように作成する複数のドキュメント更新が必要となるケースがほとんどなので、一括書き込みを使用する1.同期判定sidekiqのワーカー自体、ドキュメント更新のきっかけとなりうる処理が発生したために起動しているのですが、その中でも、「どのドキュメントが実際に同期する必要があるのか?」は個別にレコードをみて判断するようにしました。これは、同じレコードに対して複数の同期処理が呼び出されるケースがあるのと、リトライを前提としているために、リトライ時、既に同期が終わっているドキュメントを再度処理しないようにするためです。Firestoreに同期された際のupdated_at)を持たせ、一致しているかどうかを見ることにしました。Firestoreへ同期を行い、完了後にfirestored_atをupdated_atの値で更新します。Recordのtouchを利用)のどちらかで行いました。ここが一番面倒だったので、もっといい方法はないかなー、と思っています。FirestoreのAPIは結構エラーを返します。リトライをすれば成功することがほとんどなので、リトライ自体はsidekiqの仕組みを利用し、更新の処理は何度実行しても問題ないように作成を行いました。Firestoreには、トランザクションと一括書き込みという機能があります。処理が失敗した場合は単純にリトライするため、トランザクション機能は必須ではなかったのですが、一つ一つ書き込むのに比べてパフォーマンスが出るため、基本的にこちらを利用して書き込みを行いました。件という制限があります。前述の注意点も含め、以下のような処理を行う共通機能を作成しました。Firestoreへデータが反映されるタイミングが遅くなる(他のレコードも含めてコミットされた後になる)ため、特に更新を早くしたいドキュメントがある場合は、トランザクションを分けるなどの対応もしています。FirestoreはFirebaseプロジェクト一つにつき、一つしか存在しません。そのため、開発環境や、ステージング、テスト環境用に独立したものをいくつも用意するのが困難です。/developer-B/のように分けて、環境変数で指定するようにしました。開発環境では、しばしばRDBの中身を入れ替えたりします。その場合は、全データの再同期をしない限り、どうしてもRDBのデータとは差が出てしまいます。これについて、現在は諦めています。Firestoreでの全文検索について、以下のように説明されています。フィールドの検索をサポートしていません。コレクション全体をダウンロードして、クライアント側でフィールドを検索することは現実的ではありません。Firestoreではテキストの検索は実現できません。Serviceへ同期し、検索に関してはそちらで行うようにしました。Elasticsearchをモバイルアプリケーションへ直接公開するのは難しいので、検索リクエストはRailsから行うようにしました。Firestoreに格納されている実際のデータを参照することになります。課金について」のデータをもとに試算し、試験運用で確認しました。動いているアプリケーションなので書き込み数は計算しやすかったのですが、読み取り数に関しては実際に動かしてみないとどれほど発生するのかが推測しづらかったです。Firestoreを使用した新しいモバイルアプリケーションでは、大きくユーザー体験を向上させることが出来ました。モバイルアプリケーションからは直接更新しないなど、若干、変則的な使い方ではあると思いますが、利用して正解だったと思っています。こんにちは...2019-10-21モバイルアプリエンジニアによる最近の登壇レポートこんにちは。株式会社スタメンでは、エンジニア採用を強化しています。ご興味のある方はぜひエンジニア採用情報をご覧ください。引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。