【问题标题】:Firestore pagination - Is there any query compatible with firebase's limitToLast?Firestore 分页 - 是否有任何与 firebase 的 limitToLast 兼容的查询?
【发布时间】:2018-06-28 18:41:25
【问题描述】:

有没有办法使用 Firestore 实现反向分页? 我正在努力使用 firestore 实现分页,并且对它的 firestore 查询有限。可以通过startAtlimit方法进行前向分页,就可以了。但是反向分页不容易做到,因为我们只有 endBeforeendAt 方法,我们如何从其中获取最后 n 个元素给定文件?我知道实时数据库有方法 limitToLast。 Firestore有这样的查询吗? (另外我需要实现多重排序,所以用“ASC”或“DESC”排序获取最后一个文档是行不通的) 非常感谢您的帮助。

谢谢!

【问题讨论】:

    标签: sorting firebase pagination google-cloud-firestore


    【解决方案1】:

    相当于 Cloud Firestore 中 Firebase 实时数据库中的 limitToLast(...) 操作是对数据进行降序排序(这在 Firestore 中是可能的),然后是 limit(...)。如果您在执行此操作时遇到问题,请更新您的问题以显示您所做的工作。

    我同意这是用于反向分页的次优 API,因为您以相反的顺序接收项目。

    【讨论】:

    • 感谢您的回复。问题是我需要像 afs.collection('data', ref => ref.orderBy('field1', 'asc').orderBy(''field2', 'desc') 那样实现排序和分页。 ..)。有没有办法让这些排序结果保持在分页中?我颠倒了这个查询顺序 (field1 => 'desc', field2 => 'asc', ...) ,但没有工作。谢谢。
    • 最后我决定和 Algolia 一起搬家,现在效果很好。
    【解决方案2】:

    更简单的答案:Firestore 现在有 .limitToLast(),它的工作原理与您想象的完全一样。在我自己的(我想我需要尽快发布)Firestore Wrapper 中使用:

    //////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////
    // *** Paginate API ***
    
    export const PAGINATE_INIT = 0;
    export const PAGINATE_PENDING = -1;
    export const PAGINATE_UPDATED = 1;
    export const PAGINATE_DEFAULT = 10;
    export const PAGINATE_CHOICES = [10, 25, 50, 100, 250, 500];
    
    /**
     * @classdesc
     * An object to allow for paginating a table read from Firestore. REQUIRES a sorting choice
     * @property {Query} Query that forms basis for the table read
     * @property {number} limit page size
     * @property {QuerySnapshot} snapshot last successful snapshot/page fetched
     * @property {enum} status status of pagination object
     * @method PageForward pages the fetch forward
     * @method PageBack pages the fetch backward
     */
    
    export class PaginateFetch {
      Query = null;
      limit = PAGINATE_DEFAULT;
      snapshot = null;
      status = null; // -1 pending; 0 uninitialize; 1 updated;
      /**
       * ----------------------------------------------------------------------
       * @constructs 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
       * @param {array} sortArray a 2xn array of sort (i.e. "orderBy") conditions
       * @param {ref} ref (optional) allows "table" parameter to reference a sub-collection
       * of an existing document reference (I use a LOT of structered collections)
       *
       * 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 {number} limit (optional)
       * @returns {PaginateFetchObject}
       **********************************************************************/
    
      constructor(
        table,
        filterArray = null,
        sortArray = null,
        ref = null,
        limit = PAGINATE_DEFAULT
      ) {
        const db = ref ? ref : fdb;
    
        this.limit = limit;
        this.Query = sortQuery(
          filterQuery(db.collection(table), filterArray),
          sortArray
        );
        this.status = PAGINATE_INIT;
      }
    
      /**
       * @method Page
       * @returns Promise of a QuerySnapshot
       */
      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 this.snapshot.docs.map((doc) => {
              return {
                ...doc.data(),
                Id: doc.id,
                ref: doc.ref
              };
            });
          });
      };
    
      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 this.snapshot.docs.map((doc) => {
              return {
                ...doc.data(),
                Id: doc.id,
                ref: doc.ref
              };
            });
          });
      };
    }
    
    /**
     * ----------------------------------------------------------------------
     * @function filterQuery
     * builds and returns a query built from an array of filter (i.e. "where")
     * consitions
     * @param {Query} query collectionReference or Query to build filter upong
     * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
     * @returns Firestor Query object
     */
    export const filterQuery = (query, filterArray = null) => {
      return filterArray
        ? filterArray.reduce((accQuery, filter) => {
            return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
          }, query)
        : query;
    };
    
    /**
     * ----------------------------------------------------------------------
     * @function sortQuery
     * builds and returns a query built from an array of filter (i.e. "where")
     * consitions
     * @param {Query} query collectionReference or Query to build filter upong
     * @param {array} sortArray an (optional) 2xn array of sort (i.e. "orderBy") conditions
     * @returns Firestor Query object
     */
    export 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;
    };
    
    

    我也有 CollectionGroup 查询的等效项,以及每个查询的侦听器。

    【讨论】:

      【解决方案3】:

      我遇到了同样的问题,不明白为什么使用 limitendAt 没有返回我想要的结果。我试图实现一个列表,您可以在其中双向分页,首先向前,然后向后返回到列表的开头。

      为了纠正这种情况,我决定为每一页缓存startAfterDocumentSnapshot,以便可以双向移动,这样我就不必使用endAt。唯一会成为问题的情况是,当用户在第一页以外的页面上时文档集合发生移动或更改,但通过返回第一页,它将重置为集合的开头。

      【讨论】:

      • 唯一的问题是,如果你在17页之后开始,你必须缓存17个文档,例如......
      • 您有更好的解决方法吗?对于我自己的用例,为每一页缓存一个文档的要求是一个有效的权衡
      • 阅读下面我的评论,或者在云函数中使用 offset()。
      【解决方案4】:

      是的。以弗兰克的回答为基础......

      在您的查询中有这样的内容...

          if (this.next) {
            // if next, orderBy field descending, start after last field
            q.orderBy('field', 'desc');
            q.startAfter(this.marker);
          } else if (this.prev) {
            // if prev, orderBy field ascending, start after first field
            q.orderBy('field', 'asc');
            q.startAfter(this.marker);
          } else {
            // otherwise just display first page results normally
            q.orderBy('field', 'desc');
          }
          q.limit(this.pageSize);
      

      然后当你得到查询时反转它......

          this.testsCollection
                  .valueChanges({ idField: 'id' })
                  .pipe(
                    tap(results => {
                      if (this.prev) {
                        // if previous, need to reverse the results...
                        results.reverse();
                      }
                    })
                  )
      

      【讨论】:

        【解决方案5】:

        我只想分享我的 Firestore 分页代码。
        我正在使用带有 NextJS 的反应钩子。

        您需要有“useFirestoreQuery”钩子,可以在这里找到。
        https://usehooks.com/useFirestoreQuery/

        这是我的设置。

        /* Context User */
        const {user} = useUser()
        
        /* States */
        const [query, setQuery] = useState(null)
        const [ref, setRef] = useState(null)
        const [reverse, setReverse] = useState(false)
        const [limit, setLimit] = useState(2)
        const [lastID, setLastID] = useState(null)
        const [firstID, setFirstID] = useState(null)
        const [page, setPage] = useState(1)
        
        /* Query Hook */
        const fireCollection = useFirestoreQuery(query)
        
        /* Set Ref, **When firebase initialized** */
        useEffect(() => {
          user?.uid &&
            setRef(
              firebase
                .firestore()
                .collection('products')
                .where('type', '==', 'vaporizers')
            )
        }, [user])
        
        /* Initial Query, **When ref set** */
        useEffect(() => {
          ref && setQuery(ref.orderBy('id', 'asc').limit(limit))
        }, [ref])
        
        /* Next Page */
        const nextPage = useCallback(() => {
          setPage((p) => parseInt(p) + 1)
          setReverse(false)
          setQuery(ref.orderBy('id', 'asc').startAfter(lastID).limit(limit))
        }, [lastID, limit])
        
        /* Prev Page */
        const prevPage = useCallback(() => {
          setPage((p) => parseInt(p) - 1)
          setReverse(true)
          setQuery(ref.orderBy('id', 'desc').startAfter(firstID).limit(limit))
        }, [firstID, limit])
        
        /* Product List */
        const ProductList = ({fireCollection}) => {
          const [products, setProducts] = useState([])
        
          useEffect(() => {
            let tempProducts = []
            let tempIDs = []
            const {data} = fireCollection
            for (const key in data) {
              const product = data[key]
              tempIDs.push(product.id)
              tempProducts.push(<ProductRow {...{product}} key={key} />)
            }
            if (reverse) {
              tempProducts.reverse()
              tempIDs.reverse()
            }
            setFirstID(tempIDs[0])
            setLastID(tempIDs.pop())
            setProducts(tempProducts)
          }, [fireCollection])
        
          return products
        }
        

        我使用上下文提供程序将“ProductList”移到了组件之外,但这就是它的要点。


        注意。 如果您正在寻找产品的总数。我建议您跟上这些云功能的总数。您需要将总数存储在单独的集合中。我把我的称为“捷径”。

        exports.incrementProducts = functions.firestore
          .document('products/{id}')
          .onCreate(async (snap, context) => {
            const createdProduct = snap.data()
            /* Increment a shortcut collection that holds the totals to your products */
          })
        
        exports.decrementProducts = functions.firestore
          .document('products/{id}')
          .onDelete((snap, context) => {
            const deletedProduct = snap.data()
            /* Decrement a shortcut collection that holds the totals to your products */
          })
        



        别忘了
        确保为所有这些设置了索引。这是我的样子。

        【讨论】:

          猜你喜欢
          • 2017-06-28
          • 2017-02-09
          • 2018-07-10
          • 1970-01-01
          • 1970-01-01
          • 2013-06-02
          • 1970-01-01
          • 2016-12-02
          • 1970-01-01
          相关资源
          最近更新 更多