13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FlutterAdvent Calendar 2019

Day 20

Firebase + FlutterでWhereNotInを考える

Last updated at Posted at 2019-12-20

やりたいこと

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脳ではできないことが多く、結果としてクライアントに吸収する形になった :cry: が、なぜそうなったか?そこに至ったまでに色々学びがあったので共有したい。案が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あるやんいけるやん!」と思うだろう、詳細は下記へ :runner:

各アイデアの調査

一通り検討していく。

案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.)

クラッシュします。 :clap:

案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. フロントでどうにかする

:lifter: おまたせ :lifter:

結局こうなりましたね・・・どんな設計にするにせよ実現はできる。すでに有識者が検証とアイデアも公開してくれていますね、ありがたい。

手法としては 特定件数取得してフィルターする N+1回リクエストする などの何パターン化が考えられるかと思います。ページングするか、readしている必要はあるかなどのユースケースによって分ければ良いと思います。

メリデメは後述。

案4. Algoliaでどうにかする

感謝。この案があったかという感じ。
確かにクエリを見ていると OR があり複数キーのrangeにも対応している。メリデメは後述。

案5. RESTのAPI作っちゃう

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に徐々に変えていこうと思います。それでは電子レンジにチキンを潜影蛇手(せんえいじゃしゅ) :snake: してくるのでこの辺で。

13
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?