💻

[Flutter/Firestore] OR検索しつつ変更検知したい

2020/08/20に公開

cloud_firestore を使ってFlutterアプリからCloud Firestoreを使う際に、OR検索しつつ変更検知する方法について色々検討したので備忘録として残します。

FirestoreではOR検索はできない

通常は、以下のような感じで where() メソッドを使ってクエリを組み立てて、 QuerySnapshot のストリームをlistenすることで変更検知を行います。

final Query query = Firestore.instance.collection('foo')
    .where('bar', isEqualTo: 'baz')
    .orderBy('createdAt');

query.snapshots().listen((qss) {
    final List<DocumentSnapshot> docs = qss.documents;
    final List<DocumentSnapshot> changedDocs = qss.documentChanges.map((docChange) => docChange.document).toList();
    
    // 変更を検知した際に実行したい処理
});

しかし、OR検索を含む条件でドキュメントを絞り込みたい場合はやっかいです。

Firestoreの仕様としてOR検索はできず、OR条件ごとに独立したクエリを作成してアプリ側で結果を結合する必要があります。

しかし、そうすると通常のように Query::snapshots() をlistenするということができません🤔

whereIn とか arrayContains とか arrayContainsAny とかあるけど?

where() メソッドの比較演算子として whereIn arrayContains arrayContainsAny といったものは用意されていて、これらはまさにOR検索の用途なのですが、残念ながら条件として渡せる配列は最大で要素数10件までしか対応していないようです。

また、空の配列を渡すこともできません。

// 動く
final Query query = Firestore.instance.collection('foo')
    .where('number', whereIn: [1,2,3,4,5,6,7,8,9,10]);

// エラーになる
// Invalid Query. 'arrayContainsAny' filters support a maximum of 10 elements in the value array.
final Query query = Firestore.instance.collection('foo')
    .where('number', whereIn: [1,2,3,4,5,6,7,8,9,10,11]);

// エラーになる
// Invalid Query. A non-empty array is required for 'whereIn' filters.
final Query query = Firestore.instance.collection('foo')
    .where('number', whereIn: []);

なので、動的な配列を使ったOR検索としては実質使えません😓

参考:FirestoreのArrayContainAnyクエリでタイムラインを実装してみた - Qiita

解決策:アプリ側でドキュメントを絞り込んで、ドキュメントごとにlistenする

クエリをlistenすることができないのであれば、アプリ側でドキュメントを絞り込んでおいて、それらのドキュメントをそれぞれ個別にlistenすれば一応やりたいことができます。

List<Foo> foos = (await Firestore.instance.collection('foo').getDocuments())
    .documents
    .map((doc) => _fromDoc(doc)) // DocumentSnapshotをFooモデルに変換するユーティリティを作っておくイメージ
    .toList();

// OR検索はアプリ側で行う
List<Foo> filteredFoos = foos.where((foo) => targetIds.contains(foo.id)).toList();

filteredFoos.forEach((foo) {
    Firestore.instance.collection('foo')
        .document(foo.id)
        .snapshots()
        .listen((DocumentSnapshot doc) {
            // 変更を検知した際に実行したい処理
        });
});

ただ、これだと

  • 条件に一致するドキュメントが新たに追加されたとき
  • 条件に一致していたドキュメントが削除されたとき
  • 条件に一致していたドキュメントが更新されたことにより条件に一致しなくなったとき

に適切に検知することができません😓

どうしてもそれが必要な場合は、コレクション全体をlistenしておいて、変更されたドキュメントの内容に応じて何かしらの処理を実施したり無視したりするような実装にするしかないのかなと思います。

Firestore.instance.collection('foo').snapshots().listen((qss) async {
    qss.documentChanges.map((docChange) => docChange.document).forEach((DocumentSnapshot doc) {
        if (_isTarget(doc)) {
            // 変更を検知した際に実行したい処理
        }
    });
});

全然詳しく理解できてないので、もっといい方法をご存知の方いたらぜひ 教えてください 🙇

GitHubで編集を提案

Discussion