【问题标题】:Flutter Firestore pagination in abstract service class抽象服务类中的 Flutter Firestore 分页
【发布时间】:2021-09-12 09:52:54
【问题描述】:

我正在使用 Firestore 为我的 Flutter 应用程序实现分页,但遇到了设计问题。

我正在使用服务类通过数据模型类从我的应用程序的业务逻辑中抽象数据库操作,如下所示:

UI <- business logic (riverpod) <- data model class <- stateless firestore service

这很好用,因为它遵循关注点分离原则。

但是,在 Firestore 库中,实现分页的唯一方法是保存最后一个 DocumentSnapshot,以便在使用 startAfterDocument() 的下一个查询中引用它。这意味着,由于我的数据库服务是无状态的,我需要将这个 DocumentSnapshot 保存在我的业务逻辑代码中,原则上应该完全从 Firestore 中抽象出来。

我的第一个直觉是从服务内的数据模型类中重建 DocumentSnapshot 并将其用于分页,但我无法完全重建它,所以我想知道这是否足够。

有人遇到过这个问题吗?你是怎么解决的?

干杯!

【问题讨论】:

    标签: firebase flutter google-cloud-firestore


    【解决方案1】:

    我偶然发现了完全相同的问题,即使我使用的是 Bloc 而不是 Riverpod。 我为此写了整篇文章,以支持列表的实时更新并允许无限滚动:ARTICLE ON MEDIUM

    我的方法是按名称和 ID(例如)对查询进行排序,并使用 startAfter 而不是 startAfterDocument。 例如:

    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:infite_firestore_list/domain/list_item_entity.dart';
    import 'package:infite_firestore_list/domain/item_repository.dart';
    
    class FirebaseItemRepository implements ItemRepository {
      final _itemsCollection = FirebaseFirestore.instance.collection('items');
    
      @override
      Future<Stream<List<ListItem>>> getItems({
        String startAfterName = '',
        String startAfterId = '',
        int paginationSize = 10,
      }) async {
        return _itemsCollection
            .orderBy("name")
            .orderBy(FieldPath.documentId)
            .startAfter([startAfterName, startAfterId])
            .limit(paginationSize)
            .snapshots()
            .map((querySnapshot) => querySnapshot.docs.map((doc) {
                  return ListItemDataModel.fromFirestoreDocument(doc).toDomain();
                }).toList());
      }
    }
    

    在您的逻辑中,您只需使用 id 和 name 或您希望使用的任何字段,例如日期。 如果您使用多个orderBy 的组合,则在您第一次运行查询时,Firebase 可能会要求您使用将出现在日志中的链接来构建索引。

    这种方法的缺点是,它只有在您确定您在 orderBy 中使用的字段是唯一的情况下才有效。实际上,例如,如果您按日期排序,如果两个字段具有相同的日期并且您使用 startAfter 该日期(第一项),您可以跳过具有相同日期的第二项...

    在我的示例中,startAfterId 似乎没有用,但在我的用例中,它解决了我偶然发现的一些边缘情况。

    另类

    我认为但我个人不喜欢的另一种选择(因此我没有在我的文章中提到它)可能是将每个页面的最后一个文档的快照的数组存储在存储库本身中。 比使用逻辑域中的 id 请求新页面并在存储库本身中建立对应关系 id &lt;--&gt; snapshot

    如果您希望存储库单例中的页面数量有限,因此需要一个受控数组,那么这种方法可能会很有趣,否则它会闻到内存泄漏的味道,这就是为什么我个人不喜欢这种方法尽可能保持通用性。

    【讨论】:

    • 我已经探索过这两个选项。到目前为止,第一个是我确定的,唯一的缺点是它要求每个“时间戳索引”集合也必须在 firestore 中的 ID 字段上具有和索引
    【解决方案2】:

    如果您正在使用或可以使用任何orderBy 查询。您可以将 startAfter 与您的最后一个查询值一起使用。例如,如果您 orderBy date 您可以使用 last date 进行下一个分页查询。

    startAfter method reference

    【讨论】:

      【解决方案3】:

      分页的非常定义(你在一个页面;你转到下一页)是有状态的,所以尝试这样做它“无状态”没有任何意义。

      我不在 Flutter 中工作,但在 JS/React 中我构建了以下类,该类返回一个 OBJECT,该 OBJECT 具有 PageForward/PageBack 方法和保存所需数据/状态的属性:

      export class PaginateFetch {
        /**
         * constructs an object to paginate through large Firestore Tables
         * @param {string} table a properly formatted string representing the requested collection
         * - always an ODD number of elements
         * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
         * The array is assumed to be sorted in the correct order -
         * i.e. filterArray[0] is added first; filterArray[length-1] last
         * returns data as an array of objects (not dissimilar to Redux State objects)
         * with both the documentID and documentReference added as fields.
         * @param {array} sortArray a 2xn array of sort (i.e. "orderBy") conditions
         * @param {?string} refPath (optional) allows "table" parameter to reference a sub-collection
         * of an existing document reference (I use a LOT of structured collections)
         * @param {number} limit page size
         * @category Paginator
         */
        constructor(
          table,
          filterArray = null,
          sortArray = null,
          refPath = null,
          limit = PAGINATE_DEFAULT
        ) {
          const db = dbReference(refPath);
      
          /**
           * current limit of query results
           * @type {number}
           */
          this.limit = limit;
          /**
           * underlying query for fetch
           * @private
           * @type {Query}
           */
          this.Query = sortQuery(
            filterQuery(db.collection(table), filterArray),
            sortArray
          );
          /**
           * current status of pagination
           * @type {PagingStatus}
           * -1 pending; 0 uninitialized; 1 updated;
           */
          this.status = PAGINATE_INIT;
        }
      
        /**
         * executes the query again to fetch the next set of records
         * @async
         * @method
         * @returns {Promise<RecordArray>} returns an array of record - the next page
         */
        PageForward() {
          const runQuery = this.snapshot
            ? this.Query.startAfter(last(this.snapshot.docs))
            : this.Query;
      
          this.status = PAGINATE_PENDING;
      
          return runQuery
            .limit(this.limit)
            .get()
            .then((QuerySnapshot) => {
              this.status = PAGINATE_UPDATED;
              //*IF* documents (i.e. haven't gone beyond start)
              if (!QuerySnapshot.empty) {
                //then update document set, and execute callback
                //return Promise.resolve(QuerySnapshot);
                this.snapshot = QuerySnapshot;
              }
              return Promise.resolve(RecordsFromSnapshot(this.snapshot));
            });
        }
      
        /**
         * executes the query again to fetch the previous set of records
         * @async
         * @method
         * @returns {Promise<RecordArray>} returns an array of record - the next page
         */
        PageBack() {
          const runQuery = this.snapshot
            ? this.Query.endBefore(this.snapshot.docs[0])
            : this.Query;
      
          this.status = PAGINATE_PENDING;
      
          return runQuery
            .limitToLast(this.limit)
            .get()
            .then((QuerySnapshot) => {
              this.status = PAGINATE_UPDATED;
              //*IF* documents (i.e. haven't gone back ebfore start)
              if (!QuerySnapshot.empty) {
                //then update document set, and execute callback
                this.snapshot = QuerySnapshot;
              }
              return Promise.resolve(RecordsFromSnapshot(this.snapshot));
            });
        }
      }
      /**
       * @private
       * @typedef {Object} filterObject
       * @property {!String} fieldRef
       * @property {!String} opStr
       * @property {any} value
       */
      
      /**
       * ----------------------------------------------------------------------
       * @private
       * @function filterQuery
       * builds and returns a query built from an array of filter (i.e. "where")
       * conditions
       * @param {Query} query collectionReference or Query to build filter upong
       * @param {?filterObject} [filterArray] an (optional) 3xn array of filter(i.e. "where") conditions
       * @returns {Query} Firestore Query object
       */
      const filterQuery = (query, filterArray = null) => {
        return filterArray
          ? filterArray.reduce((accQuery, filter) => {
              return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
            }, query)
          : query;
      };
      
      /**
       * @private
       * @typedef {Object} sortObject
       * @property {!String} fieldRef
       * @property {!String} dirStr
       */
      
      /**
       * ----------------------------------------------------------------------
       * @private
       * @function sortQuery
       * builds and returns a query built from an array of filter (i.e. "where")
       * conditions
       * @param {Query} query collectionReference or Query to build filter upong
       * @param {?sortObject} [sortArray] an (optional) 2xn array of sort (i.e. "orderBy") conditions
       * @returns Firestore Query object
       */
      const sortQuery = (query, sortArray = null) => {
        return sortArray
          ? sortArray.reduce((accQuery, sortEntry) => {
              return accQuery.orderBy(sortEntry.fieldRef, sortEntry.dirStr || "asc");
              //note "||" - if dirStr is not present(i.e. falsy) default to "asc"
            }, query)
          : query;
      };
      

      【讨论】:

        猜你喜欢
        • 2019-01-13
        • 2022-06-20
        • 2020-05-28
        • 2021-10-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-10
        相关资源
        最近更新 更多