Firestore 分页 - 是不是有任何与 firebase 的 limitToLast 兼容的查询?

Posted

技术标签:

【中文标题】Firestore 分页 - 是不是有任何与 firebase 的 limitToLast 兼容的查询?【英文标题】: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”排序获取最后一个文档是行不通的) 非常感谢您的帮助。

谢谢!

【问题讨论】:

【参考方案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 */
  )

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

【讨论】:

以上是关于Firestore 分页 - 是不是有任何与 firebase 的 limitToLast 兼容的查询?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Android 对 Firestore 进行分页?

获取分页反应的firestore收集记录计数[重复]

Firestore 分页数据 + 快照监听器

Firestore 分页问题

如何使用 ReactJs 对 Cloud Firestore 数据进行分页

是否有任何与 rails ActiveRecord 迁移相当的 Firestore 数据库模式迁移概念?