やりたいこと
Firebase + Flutterのアプリケーション内でFireStore上にあるデータの 特定のグループ内の特定のユーザーの投稿は表示しない
のような仕様を満たしたい場合を考える。
- groups [collection]
- group 1
- comments [collection]
- comment 1
- text: xxxx
- uid: A
- created_at: 2019-10-11 10:00:00
- comment 2
- text: xxxx
- uid: B
- created_at: 2019-10-11 10:00:00
- comment 3
- text: xxxx
- uid: C
- created_at: 2019-10-11 10:00:00
- ...
- group 2
- ...
例えば上記のような構造があったとする。
comments
には膨大な量のデータが有り、uid: B のコメント以外のコメントがほしい
ケースを考える。これがなかなかに厄介で、MySQL的なRDB脳ではできないことが多く、結果としてクライアントに吸収する形になった が、なぜそうなったか?そこに至ったまでに色々学びがあったので共有したい。案が5つあるうちで1,2は検証するも失敗、3を導入し、4,5はメリデメを整理した結果調査のみとした。
全体的にFireStore強めの記事になりすいません・・・
まず確認したこと
MySQL的な WhereNotIn
的な概念はFireStoreには存在しない。
FireStore Flutter SDK
Query where(
dynamic field, {
dynamic isEqualTo,
dynamic isLessThan,
dynamic isLessThanOrEqualTo,
dynamic isGreaterThan,
dynamic isGreaterThanOrEqualTo,
dynamic arrayContains,
List<dynamic> arrayContainsAny,
List<dynamic> whereIn,
bool isNull,
}) {...
を見るとわかるように条件として設定できるQueryのレパートリーは少なく、かつOR条件というものは存在しない。
これは公式のドキュメントでも言及されている。
論理 OR クエリ。この場合は、OR 条件ごとに独立したクエリを作成し、アプリでクエリ結果を結合する必要があります。
という前提知識の元書きを読み進めてほしい。
(賢いあなたは「オッ!whereInあるやんいけるやん!」と思うだろう、詳細は下記へ )
各アイデアの調査
一通り検討していく。
案1. uid検索用のテーブルを作り必要なuidだけwhereで絞り込む
- groups [collection]
- group 1
- users: [A, B, C] <--- ここを追加する
- comments [collection]
- comment 1
- text: xxxx
- uid: A
- created_at: 2019-10-11 10:00:00
- comment 2
- text: xxxx
- uid: B
- created_at: 2019-10-11 10:00:00
- comment 3
- text: xxxx
- uid: C
- created_at: 2019-10-11 10:00:00
- ...
- group 2
- ...
というケースを最初にメンバーと相談して調べ始めました。
実装としては修正も軽微だし、割と使われそうなコレクションだしまぁあってもいいなと思ったんですが、問題は OR条件が存在しない
ということ。
これによりこの案はお蔵入り。
whereIn
の問題点
上述の通り、whereIn
が存在してこれを使えばいけるように見えます。最新のSDKよりサポートされているため、0.12.11以上へ上げましょう。(この際、podに関するエラーが出ることがありこちらのissueで対策が議論されています。)
ブログでもハイテンションで語られており、全ユーザーが歓喜したんですが
As we mentioned earlier, you're currently limited to a maximum of 10 different values in your queries.
という成約があります。つまりクエリは10件までしか指定できません。
今回のケースでなければ便利に使えそうですが、groupのuidは全然10件超えるので厳しい・・・
では仮にFlutterで10件以上渡してやろうとするとどうなるか?
[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: PlatformException(invalid_query, FIRInvalidArgumentException, Invalid Query. ‘in’ filters support a maximum of 10 elements in the value array.)
クラッシュします。
案2. 連番のuseridを発行して、And条件でフィルターできるようにする
「なればAnd条件もりもりでやったらー!!!」という気持ちのアイデアが以下。
- users [collection]
- user 1
- uuid: A
- suid: 0 <- ここに連番のuseridを新しく定義して入れて
- user 2
- uuid: B
- suid: 1
- user 2
- uuid: B
- suid: 2
...
- groups [collection]
- group 1
- users: [A, B, C]
- comments [collection]
- comment 1
- text: xxxx
- uid: A
- suid: 0 <- commentのドキュメントにも追加
- created_at: 2019-10-11 10:00:00
- comment 2
- text: xxxx
- uid: B
- suid: 1
- created_at: 2019-10-11 10:00:00
- comment 3
- text: xxxx
- uid: C
- suid: 2
- created_at: 2019-10-11 10:00:00
- ...
- group 2
- ...
これで何ができるかというと。公式の特定の範囲の数値型の条件を取得しない方法にあるような
!= 句が含まれるクエリ。この場合は、「より大きい」クエリと「より小さい」クエリに、クエリを分割する必要があります。たとえば、クエリ句 where("age", "!=", "30") はサポートされませんが、2 つのクエリ(句 where("age", "<", "30") が含まれるクエリと句 where("age", ">", 30) が含まれるクエリ)を結合することで、同じ結果セットが得られます。
を踏襲して、ユーザーを識別する Authenticate から発行される文字列の uuid ではなく連番で数値型になるような値をもたせてこれを条件とします。つまり
B以外のコメント = suidが2以上 AND 1未満
のようにクエリを発行できるのでANDで表現できます。ただこれもやはりAND条件の限界があり複数条件に対応できません。連番でuidを発行するのも RDS でいうところの Auto Incrementはないのでcloud functionでuserを生成する時に毎回コレクション数をとってくる必要があります。
他のクエリがあるとそもそも実現できない
この条件のみでクエリを入れるならいいんですが、これに加えて created_atがxxx以上のもの
という成約を入れてしまうと複合クエリの制約条件 にひっかかりクエリの生成ができません。
// 公式のダメな例
citiesRef.where("state", ">=", "CA").where("population", ">", 100000)
で書かれるような範囲条件を指定するクエリは複数のキーを指定できません。
案3. フロントでどうにかする
おまたせ
結局こうなりましたね・・・どんな設計にするにせよ実現はできる。すでに有識者が検証とアイデアも公開してくれていますね、ありがたい。
手法としては 特定件数取得してフィルターする
N+1回リクエストする
などの何パターン化が考えられるかと思います。ページングするか、readしている必要はあるかなどのユースケースによって分ければ良いと思います。
メリデメは後述。
案4. Algoliaでどうにかする
Algoliaというサービスにfirestoreのデータをシンクすると検索機能を強化出来ますよ。
— 高松智明 (@t14i_) December 8, 2019
たしか not in も出来たはず。
(すでにご存知だったらすみません)https://t.co/oNURo9GvjYhttps://t.co/rZzHHW0mR9
感謝。この案があったかという感じ。
確かにクエリを見ていると OR
があり複数キーのrangeにも対応している。メリデメは後述。
案5. RESTのAPI作っちゃう
フロントで弾くことにすると、もし多量のデータになった時に通信料含め負担大きくなる+Firestoreのキャッシュ無駄に食いつぶすので、listenでなくてgetで取得するような感じならCloud Functionsを挟んであげた方がまだよいかもしれないです。少数程度のドキュメントであれば誤差ではありますが。
— const su- = エンジニア | 論理無職 (@_sgr_ksmt) December 9, 2019
cloud functionsでRESTのエンドポイントを建てるのではどうか?というのもありかなという話をtwitterでしました。インターネッツは困っていると助けてくれる人もいてありがたい・・・
メリデメは後述。
全体としてのメリデメの比較とまとめ
かんたんにメリデメをまとめます。
案 | メリット | デメリット |
---|---|---|
案1. uid検索用のテーブルを作り必要なuidだけwhereで絞り込む | 条件が10件以下なら可 | 条件が11件以上だと無理 |
案2. 連番のuseridを発行して、And条件でフィルターできるようにする | 条件が他にないなら可 | 複合条件に対応しない |
案3. フロントでどうにかする | 思想としてシンプルではある | 余計なデータを取得する必要があり、メモリ・キャッシュ的に非効率。 |
案4. Algoliaでどうにかする | 柔軟に対応できるし必要なデータのみリクエストできる | コスト |
案5. RESTのAPI作っちゃう | 余計なデータをリクエストしないでいい | 一部だけRESTになってフロントに実装差異が生まれる、CFでやるが結局そこでリクエストはすることになる |
ケースによっては1,2もありではありますね。今回は3で進めつつ、4か5に徐々に変えていこうと思います。それでは電子レンジにチキンを潜影蛇手(せんえいじゃしゅ) してくるのでこの辺で。