【问题标题】:Cloud Firestore collection countCloud Firestore 收集计数
【发布时间】:2021-12-03 01:10:33
【问题描述】:

是否可以使用新的 Firebase 数据库 Cloud Firestore 计算一个集合有多少项目?

如果是这样,我该怎么做?

【问题讨论】:

标签: firebase google-cloud-firestore


【解决方案1】:

11/20 更新

我创建了一个 npm 包以便轻松访问计数器功能:https://fireblog.io/post/Zebl6sSbaLdrnSFKbCJx/firestore-counters


我使用所有这些想法创建了一个通用函数来处理所有计数器情况(查询除外)。

唯一的例外是当一秒钟写这么多的时候,它 减慢你的速度。一个例子是热门帖子上的likes。它是 例如,在博客文章中过度使用,并且会花费更多。一世 建议在这种情况下使用分片创建一个单独的函数: https://firebase.google.com/docs/firestore/solutions/counters

// trigger collections
exports.myFunction = functions.firestore
    .document('{colId}/{docId}')
    .onWrite(async (change: any, context: any) => {
        return runCounter(change, context);
    });

// trigger sub-collections
exports.mySubFunction = functions.firestore
    .document('{colId}/{docId}/{subColId}/{subDocId}')
    .onWrite(async (change: any, context: any) => {
        return runCounter(change, context);
    });

// add change the count
const runCounter = async function (change: any, context: any) {

    const col = context.params.colId;

    const eventsDoc = '_events';
    const countersDoc = '_counters';

    // ignore helper collections
    if (col.startsWith('_')) {
        return null;
    }
    // simplify event types
    const createDoc = change.after.exists && !change.before.exists;
    const updateDoc = change.before.exists && change.after.exists;

    if (updateDoc) {
        return null;
    }
    // check for sub collection
    const isSubCol = context.params.subDocId;

    const parentDoc = `${countersDoc}/${context.params.colId}`;
    const countDoc = isSubCol
        ? `${parentDoc}/${context.params.docId}/${context.params.subColId}`
        : `${parentDoc}`;

    // collection references
    const countRef = db.doc(countDoc);
    const countSnap = await countRef.get();

    // increment size if doc exists
    if (countSnap.exists) {
        // createDoc or deleteDoc
        const n = createDoc ? 1 : -1;
        const i = admin.firestore.FieldValue.increment(n);

        // create event for accurate increment
        const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);

        return db.runTransaction(async (t: any): Promise<any> => {
            const eventSnap = await t.get(eventRef);
            // do nothing if event exists
            if (eventSnap.exists) {
                return null;
            }
            // add event and update size
            await t.update(countRef, { count: i });
            return t.set(eventRef, {
                completed: admin.firestore.FieldValue.serverTimestamp()
            });
        }).catch((e: any) => {
            console.log(e);
        });
        // otherwise count all docs in the collection and add size
    } else {
        const colRef = db.collection(change.after.ref.parent.path);
        return db.runTransaction(async (t: any): Promise<any> => {
            // update size
            const colSnap = await t.get(colRef);
            return t.set(countRef, { count: colSnap.size });
        }).catch((e: any) => {
            console.log(e);
        });;
    }
}

这处理事件、增量和事务。这样做的好处是,如果您不确定文档的准确性(可能仍处于测试阶段),您可以删除计数器,让它在下一次触发时自动添加它们。是的,这是成本,所以不要删除它。

获取计数的方法相同:

const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');

此外,您可能希望创建一个 cron 作业(计划函数)来删除旧事件以节省数据库存储费用。您至少需要一个 blaze 计划,并且可能需要更多配置。例如,您可以在每周日晚上 11 点运行它。 https://firebase.google.com/docs/functions/schedule-functions

这是未经测试,但应该进行一些调整:

exports.scheduledFunctionCrontab = functions.pubsub.schedule('5 11 * * *')
    .timeZone('America/New_York')
    .onRun(async (context) => {

        // get yesterday
        const yesterday = new Date();
        yesterday.setDate(yesterday.getDate() - 1);

        const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
        const eventFilterSnap = await eventFilter.get();
        eventFilterSnap.forEach(async (doc: any) => {
            await doc.ref.delete();
        });
        return null;
    });

最后,别忘了保护 firestore.rules 中的集合:

match /_counters/{document} {
  allow read;
  allow write: if false;
}
match /_events/{document} {
  allow read, write: if false;
}

更新:查询

如果您还想自动化查询计数,添加到我的其他答案中,您可以在您的云函数中使用此修改后的代码:

    if (col === 'posts') {

        // counter reference - user doc ref
        const userRef = after ? after.userDoc : before.userDoc;
        // query reference
        const postsQuery = db.collection('posts').where('userDoc', "==", userRef);
        // add the count - postsCount on userDoc
        await addCount(change, context, postsQuery, userRef, 'postsCount');

    }
    return delEvents();

这将自动更新 userDocument 中的 postsCount。您可以通过这种方式轻松地将其他计数添加到多个计数中。这只是让您了解如何使事情自动化。我还为您提供了另一种删除事件的方法。您必须阅读每个日期才能删除它,因此以后删除它们并不能真正节省您,只会使功能变慢。

/**
 * Adds a counter to a doc
 * @param change - change ref
 * @param context - context ref
 * @param queryRef - the query ref to count
 * @param countRef - the counter document ref
 * @param countName - the name of the counter on the counter document
 */
const addCount = async function (change: any, context: any, 
  queryRef: any, countRef: any, countName: string) {

    // events collection
    const eventsDoc = '_events';

    // simplify event type
    const createDoc = change.after.exists && !change.before.exists;

    // doc references
    const countSnap = await countRef.get();

    // increment size if field exists
    if (countSnap.get(countName)) {
        // createDoc or deleteDoc
        const n = createDoc ? 1 : -1;
        const i = admin.firestore.FieldValue.increment(n);

        // create event for accurate increment
        const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);

        return db.runTransaction(async (t: any): Promise<any> => {
            const eventSnap = await t.get(eventRef);
            // do nothing if event exists
            if (eventSnap.exists) {
                return null;
            }
            // add event and update size
            await t.set(countRef, { [countName]: i }, { merge: true });
            return t.set(eventRef, {
                completed: admin.firestore.FieldValue.serverTimestamp()
            });
        }).catch((e: any) => {
            console.log(e);
        });
        // otherwise count all docs in the collection and add size
    } else {
        return db.runTransaction(async (t: any): Promise<any> => {
            // update size
            const colSnap = await t.get(queryRef);
            return t.set(countRef, { [countName]: colSnap.size }, { merge: true });
        }).catch((e: any) => {
            console.log(e);
        });;
    }
}
/**
 * Deletes events over a day old
 */
const delEvents = async function () {

    // get yesterday
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);

    const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
    const eventFilterSnap = await eventFilter.get();
    eventFilterSnap.forEach(async (doc: any) => {
        await doc.ref.delete();
    });
    return null;
}

我还应该警告您,通用功能将在每个 onWrite 调用周期。仅在其上运行该功能可能更便宜 onCreate 和 onDelete 特定集合的实例。喜欢 我们正在使用的noSQL数据库,重复的代码和数据可以为您节省 钱。

【讨论】:

  • 在媒体上写一篇关于它的文章以便于访问。
【解决方案2】:

快速+省钱的诀窍之一是:-

创建一个doc 并在firestore 中存储一个“计数”变量,当用户在集合中添加新文档时,增加该变量,当用户删除文档时,减少变量。例如 updateDoc(doc(db, "Count_collection", "Count_Doc"), {count: increment(1)});

注意:使用 (-1) 表示减少,(1) 表示增加count

如何节省金钱和时间:-

  1. you(firebase) 不需要遍历集合,浏览器也不需要加载整个集合来计算文档数。
  2. 所有计数都保存在只有一个名为“count”或其他变量的文档中,因此使用的数据少于 1kb,并且在 firebase firestore 中仅使用 1 次读取。

【讨论】:

    【解决方案3】:

    除了上面我的 npm 包adv-firestore-functions,你也可以只使用firestore规则来强制一个好的计数器:

    Firestore 规则

    function counter() {
      let docPath = /databases/$(database)/documents/_counters/$(request.path[3]);
      let afterCount = getAfter(docPath).data.count;
      let beforeCount = get(docPath).data.count;
      let addCount = afterCount == beforeCount + 1;
      let subCount = afterCount == beforeCount - 1;
      let newId = getAfter(docPath).data.docId == request.path[4];
      let deleteDoc = request.method == 'delete';
      let createDoc = request.method == 'create';
      return (newId && subCount && deleteDoc) || (newId && addCount && createDoc);
    }
    
    function counterDoc() {
      let doc = request.path[4];
      let docId = request.resource.data.docId;
      let afterCount = request.resource.data.count;
      let beforeCount = resource.data.count;
      let docPath = /databases/$(database)/documents/$(doc)/$(docId);
      let createIdDoc = existsAfter(docPath) && !exists(docPath);
      let deleteIdDoc = !existsAfter(docPath) && exists(docPath);
      let addCount = afterCount == beforeCount + 1;
      let subCount = afterCount == beforeCount - 1;
      return (createIdDoc && addCount) || (deleteIdDoc && subCount);
    }
    

    并像这样使用它们:

    match /posts/{document} {
      allow read;
      allow update;
      allow create: if counter();
      allow delete: if counter();
    }
    match /_counters/{document} {
      allow read;
      allow write: if counterDoc();
    }
    

    前端

    用这些替换你的 set 和 delete 函数:

    设置

    async setDocWithCounter(
      ref: DocumentReference<DocumentData>,
      data: {
        [x: string]: any;
      },
      options: SetOptions): Promise<void> {
    
      // counter collection
      const counterCol = '_counters';
    
      const col = ref.path.split('/').slice(0, -1).join('/');
      const countRef = doc(this.afs, counterCol, col);
      const countSnap = await getDoc(countRef);
      const refSnap = await getDoc(ref);
    
      // don't increase count if edit
      if (refSnap.exists()) {
        await setDoc(ref, data, options);
    
        // increase count
      } else {
        const batch = writeBatch(this.afs);
        batch.set(ref, data, options);
    
        // if count exists
        if (countSnap.exists()) {
          batch.update(countRef, {
            count: increment(1),
            docId: ref.id
          });
          // create count
        } else {
          // will only run once, should not use
          // for mature apps
          const colRef = collection(this.afs, col);
          const colSnap = await getDocs(colRef);
          batch.set(countRef, {
            count: colSnap.size + 1,
            docId: ref.id
          });
        }
        batch.commit();
      }
    }
    

    删除

    async delWithCounter(
      ref: DocumentReference<DocumentData>
    ): Promise<void> {
    
      // counter collection
      const counterCol = '_counters';
    
      const col = ref.path.split('/').slice(0, -1).join('/');
      const countRef = doc(this.afs, counterCol, col);
      const countSnap = await getDoc(countRef);
      const batch = writeBatch(this.afs);
    
      // if count exists
      batch.delete(ref);
      if (countSnap.exists()) {
        batch.update(countRef, {
          count: increment(-1),
          docId: ref.id
        });
      }
      /*
      if ((countSnap.data() as any).count == 1) {
        batch.delete(countRef);
      }*/
      batch.commit();
    }
    

    请参阅here 了解更多信息...

    J

    【讨论】:

      【解决方案4】:

      与许多问题一样,答案是 - 视情况而定

      在前端处理大量数据时应该非常小心。除了让你的前端感觉迟钝之外,Firestore 还 charges you $0.60 per million reads 你做的。


      小集合(少于 100 个文档)

      谨慎使用 - 前端用户体验可能会受到影响

      在前端处理这个应该没问题,只要你没有对这个返回的数组做太多的逻辑。

      db.collection('...').get().then(snap => {
        size = snap.size // will return the collection size
      });
      

      中等集合(100 到 1000 个文档)

      谨慎使用 - Firestore 读取调用可能会花费很多

      在前端处理这个是不可行的,因为它有太多可能减慢用户系统的速度。我们应该处理这个逻辑服务器端并且只返回大小。

      此方法的缺点是您仍在调用 Firestore 读取(等于您的集合的大小),从长远来看,这最终可能会花费您超出预期的费用。

      云功能:

      db.collection('...').get().then(snap => {
        res.status(200).send({length: snap.size});
      });
      

      前端:

      yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
         size = snap.length // will return the collection size
      })
      

      大型集合(1000 多个文档)

      最具扩展性的解决方案


      FieldValue.increment()

      As of April 2019 Firestore now allows incrementing counters, completely atomically, and without reading the data prior. 这可确保我们即使在同时从多个来源更新时也有正确的计数器值(之前使用事务解决),同时还减少了我们执行的数据库读取次数。


      通过监听任何文档删除或创建,我们可以添加或删除位于数据库中的计数字段。

      查看 Firestore 文档 - Distributed Counters 或者看看 Jeff Delaney 的 Data Aggregation。他的指南对于任何使用 AngularFire 的人来说都非常棒,但他的课程也应该适用于其他框架。

      云功能:

      export const documentWriteListener = functions.firestore
        .document('collection/{documentUid}')
        .onWrite((change, context) => {
      
          if (!change.before.exists) {
            // New document Created : add one to count
            db.doc(docRef).update({ numberOfDocs: FieldValue.increment(1) });
          } else if (change.before.exists && change.after.exists) {
            // Updating existing document : Do nothing
          } else if (!change.after.exists) {
            // Deleting document : subtract one from count
            db.doc(docRef).update({ numberOfDocs: FieldValue.increment(-1) });
          }
      
          return;
        });
      
      

      现在您可以在前端查询这个 numberOfDocs 字段来获取集合的大小。

      【讨论】:

      • 这些方法使用记录数的重新计算。如果您使用计数器并使用事务增加计数器,如果没有增加成本和需要云功能,那会不会达到相同的结果?
      • 大型集合的解决方案不是幂等的,在任何规模下都不起作用。 Firestore 文档触发器保证至少运行一次,但可以运行多次。发生这种情况时,即使在事务中维护更新也可能运行多次,这会给你一个错误的数字。当我尝试这样做时,我遇到了一次创建不到十几个文档的问题。
      • 嗨@TymPollack。我注意到使用云触发器的一些不一致的行为。您是否有机会将我链接到文章或论坛来解释您所经历的行为?
      • @cmprogram 您在使用 db.collection('...')... 时正在读取整个集合和数据...所以当您不需要数据时,您是对的 -您可以轻松地请求收集 ID 列表(不是收集文档数据),它计为一次读取。
      • @MatthewMullin 你能提供一个前端代码示例来访问 numberOfDocs 字段吗?我不明白该字段是在集合引用中还是在另一个集合中,如“计数器”。谢谢!!
      【解决方案5】:

      所以我对这个问题的解决方案有点非技术性,不是超级精确,但对我来说已经足够了。

      那些是我的文件。因为我有很多(100k+),所以发生了“大数定律”。我可以假设具有以 0、1、2 等开头的 id 的项目的数量或多或少。

      所以我要做的是滚动我的列表,直到进入从 1 或 01 开始的 id,这取决于您必须滚动多长时间

      ? 我们来了。

      现在,滚动到现在,我打开检查器,看看我滚动了多少,然后除以单个元素的高度

      必须滚动 82000 像素才能获取 id 以 1 开头的项目。单个元素的高度为 32px。

      这意味着我有 2500 个 id 以0 开头,所以现在我将它乘以可能的“起始字符”的数量。在 Firebase 中,它可以是 A-Z、a-z、0-9,这意味着它是 24 + 24 + 10 = 58。

      这意味着我有 ~~2500*58 所以它在我的收藏中提供了大约 145000 件物品。

      总结:你的 firebase 出了什么问题?

      【讨论】:

      • 好吧,我只需要时不时地数一数,就可以了解我的应用数据的增长情况。 TBH 我认为这不是我的想法很荒谬,而是 firebase 中缺少简单的“计数”功能。这对我来说已经足够好了,这里的其他答案似乎很烦人。单次测量需要我大约 3 分钟,这可能比设置此处列出的其他解决方案要快得多。
      【解决方案6】:

      使用offset & limit 的分页解决方案:

      public int collectionCount(String collection) {
              Integer page = 0;
              List<QueryDocumentSnapshot> snaps = new ArrayList<>();
              findDocsByPage(collection, page, snaps);
              return snaps.size();
          }
      
      public void findDocsByPage(String collection, Integer page, 
                                 List<QueryDocumentSnapshot> snaps) {
          try {
              Integer limit = 26000;
              FieldPath[] selectedFields = new FieldPath[] { FieldPath.of("id") };
              List<QueryDocumentSnapshot> snapshotPage;
              snapshotPage = fireStore()
                              .collection(collection)
                              .select(selectedFields)
                              .offset(page * limit)
                              .limit(limit)
                              .get().get().getDocuments();    
              if (snapshotPage.size() > 0) {
                  snaps.addAll(snapshotPage);
                  page++;
                  findDocsByPage(collection, page, snaps);
              }
          } catch (InterruptedException | ExecutionException e) {
              e.printStackTrace();
          }
      }
      
      • findDocsPage是递归查找所有页面的方法

      • selectedFields 用于优化查询并仅获取 id 字段而不是完整的文档正文

      • limit每个查询页面的最大大小

      • page定义分页的初始页面

      根据我所做的测试,它适用于最多大约 120k 记录的集合!

      【讨论】:

      • 请记住,使用后端偏移功能,您需要为偏移文档之前的所有文档的读取付费...所以offset(119223) 将收取 119,223 次读取的费用,这可能会非常昂贵如果一直使用。如果您知道startAt(doc) 的文档,那会有所帮助,但通常您没有该信息,或者您不会搜索!
      【解决方案7】:
      var variable=0
      variable=variable+querySnapshot.count
      

      如果你要在字符串变量上使用它

      let stringVariable= String(variable)
      

      【讨论】:

        【解决方案8】:

        在 2020 年,Firebase SDK 中仍不提供此功能,但 Firebase Extensions (Beta) 提供此功能,但设置和使用非常复杂...

        合理的方法

        Helpers...(创建/删除似乎是多余的,但比 onUpdate 便宜)

        export const onCreateCounter = () => async (
          change,
          context
        ) => {
          const collectionPath = change.ref.parent.path;
          const statsDoc = db.doc("counters/" + collectionPath);
          const countDoc = {};
          countDoc["count"] = admin.firestore.FieldValue.increment(1);
          await statsDoc.set(countDoc, { merge: true });
        };
        
        export const onDeleteCounter = () => async (
          change,
          context
        ) => {
          const collectionPath = change.ref.parent.path;
          const statsDoc = db.doc("counters/" + collectionPath);
          const countDoc = {};
          countDoc["count"] = admin.firestore.FieldValue.increment(-1);
          await statsDoc.set(countDoc, { merge: true });
        };
        
        export interface CounterPath {
          watch: string;
          name: string;
        }
        
        

        导出的 Firestore 挂钩

        
        export const Counters: CounterPath[] = [
          {
            name: "count_buildings",
            watch: "buildings/{id2}"
          },
          {
            name: "count_buildings_subcollections",
            watch: "buildings/{id2}/{id3}/{id4}"
          }
        ];
        
        
        Counters.forEach(item => {
          exports[item.name + '_create'] = functions.firestore
            .document(item.watch)
            .onCreate(onCreateCounter());
        
          exports[item.name + '_delete'] = functions.firestore
            .document(item.watch)
            .onDelete(onDeleteCounter());
        });
        
        

        在行动

        将跟踪正在构建的集合和所有子集合

        这里是/counters/根路径下

        现在收集计数将自动更新!如果需要计数,只需使用集合路径并在其前面加上 counters 前缀即可。

        const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
        const collectionCount = await db
          .doc('counters/' + collectionPath)
          .get()
          .then(snap => snap.get('count'));
        

        限制

        由于此方法使用单个数据库和文档,因此每个计数器受限于 每秒更新 1 次的 Firestore 约束。它最终会保持一致,但在添加/删除大量文档的情况下,计数器将落后于实际收集计数。

        【讨论】:

        • 这不是同样的限制“每秒1个文档更新”吗?
        • 是的,但它最终是一致的,这意味着收集计数将最终与实际收集计数保持一致,这是最容易实施的解决方案,并且在许多情况下会出现短暂的延迟计数是可以接受的。
        • 限制:每秒 10,000 个(根据官方文档:firebase.google.com/products/extensions/firestore-counter
        • @Pooja 限制是错误的,因为它指的是 distributed 计数器,上述解决方案是 not 分布式的。
        【解决方案9】:

        这使用计数来创建数字唯一 ID。在我的使用中,我永远不会递减,即使需要 ID 的 document 被删除。

        在需要唯一数值的 collection 创建时

        1. 用一个文档指定集合appDataset.doc id only
        2. firebase firestore console 中将uniqueNumericIDAmount 设置为0
        3. 使用doc.data().uniqueNumericIDAmount + 1 作为唯一的数字ID
        4. appData 集合uniqueNumericIDAmount 更新为firebase.firestore.FieldValue.increment(1)
        firebase
            .firestore()
            .collection("appData")
            .doc("only")
            .get()
            .then(doc => {
                var foo = doc.data();
                foo.id = doc.id;
        
                // your collection that needs a unique ID
                firebase
                    .firestore()
                    .collection("uniqueNumericIDs")
                    .doc(user.uid)// user id in my case
                    .set({// I use this in login, so this document doesn't
                          // exist yet, otherwise use update instead of set
                        phone: this.state.phone,// whatever else you need
                        uniqueNumericID: foo.uniqueNumericIDAmount + 1
                    })
                    .then(() => {
        
                        // upon success of new ID, increment uniqueNumericIDAmount
                        firebase
                            .firestore()
                            .collection("appData")
                            .doc("only")
                            .update({
                                uniqueNumericIDAmount: firebase.firestore.FieldValue.increment(
                                    1
                                )
                            })
                            .catch(err => {
                                console.log(err);
                            });
                    })
                    .catch(err => {
                        console.log(err);
                    });
            });
        

        【讨论】:

          【解决方案10】:

          我尝试了很多不同的方法。 最后,我改进了其中一种方法。 首先,您需要创建一个单独的集合并将所有事件保存在那里。 其次,您需要创建一个由时间触发的新 lambda。此 lambda 将统计事件集合中的事件并清除事件文档。 文章中的代码详细信息。 https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca

          【讨论】:

          • 请附上相关细节和代码in the answer itself,将人们指向您的博客帖子并不是StackOverflow的真正意义。
          【解决方案11】:

          据我所知,目前还没有内置解决方案,目前只能在 node sdk 中使用。 如果你有一个

          db.collection('someCollection')
          

          你可以使用

          .select([fields])
          

          定义您要选择的字段。如果你执行一个空的 select(),你只会得到一个文档引用数组。

          示例:

          db.collection('someCollection').select().get().then( (snapshot) => console.log(snapshot.docs.length) );

          此解决方案仅针对下载所有文档的最坏情况进行了优化,不适用于大型集合!

          也看看这个:
          How to get a count of number of documents in a collection with Cloud Firestore

          【讨论】:

          • 根据我的经验,select(['_id'])select()
          • 很好的答案谢谢
          【解决方案12】:

          解决方法是:

          在 firebase 文档中编写一个计数器,每次创建新条目时都会在事务中递增该计数器

          您将计数存储在新条目的字段中(即:位置:4)。

          然后在该字段上创建一个索引(位置 DESC)。

          您可以使用查询进行跳过+限制。Where("position", "

          希望这会有所帮助!

          【讨论】:

            【解决方案13】:

            根据上面的一些答案,我花了一段时间才完成这项工作,所以我想我会分享给其他人使用。我希望它有用。

            'use strict';
            
            const functions = require('firebase-functions');
            const admin = require('firebase-admin');
            admin.initializeApp();
            const db = admin.firestore();
            
            exports.countDocumentsChange = functions.firestore.document('library/{categoryId}/documents/{documentId}').onWrite((change, context) => {
            
                const categoryId = context.params.categoryId;
                const categoryRef = db.collection('library').doc(categoryId)
                let FieldValue = require('firebase-admin').firestore.FieldValue;
            
                if (!change.before.exists) {
            
                    // new document created : add one to count
                    categoryRef.update({numberOfDocs: FieldValue.increment(1)});
                    console.log("%s numberOfDocs incremented by 1", categoryId);
            
                } else if (change.before.exists && change.after.exists) {
            
                    // updating existing document : Do nothing
            
                } else if (!change.after.exists) {
            
                    // deleting document : subtract one from count
                    categoryRef.update({numberOfDocs: FieldValue.increment(-1)});
                    console.log("%s numberOfDocs decremented by 1", categoryId);
            
                }
            
                return 0;
            });
            

            【讨论】:

              【解决方案14】:

              没有可用的直接选项。你不能做db.collection("CollectionName").count()。 以下是查找集合中文档数的两种方法。

              1 :- 获取集合中的所有文档,然后获取它的大小。(不是最好的解决方案)

              db.collection("CollectionName").get().subscribe(doc=>{
              console.log(doc.size)
              })
              

              通过使用上述代码,您的文档读取将等于集合中文档的大小,这就是必须避免使用上述解决方案的原因。

              2:- 在您的集合中创建一个单独的文档,该文档将存储集合中文档的数量。(最佳解决方案)

              db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
              console.log(doc.count)
              })
              

              上面我们创建了一个包含名称counts的文档来存储所有的count信息。您可以通过以下方式更新count文档:-

              • 根据文档计数创建 Firestore 触发器
              • 创建新文档时增加计数文档的计数属性。
              • 删除文档时减少计数文档的计数属性。

              w.r.t 价格(Document Read = 1)和快速数据检索上述解决方案很好。

              【讨论】:

                【解决方案15】:

                使用admin.firestore.FieldValue.increment 增加一个计数器:

                exports.onInstanceCreate = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
                  .onCreate((snap, context) =>
                    db.collection('projects').doc(context.params.projectId).update({
                      instanceCount: admin.firestore.FieldValue.increment(1),
                    })
                  );
                
                exports.onInstanceDelete = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
                  .onDelete((snap, context) =>
                    db.collection('projects').doc(context.params.projectId).update({
                      instanceCount: admin.firestore.FieldValue.increment(-1),
                    })
                  );
                

                在此示例中,每次将文档添加到 instances 子集合时,我们都会在项目中增加一个 instanceCount 字段。如果该字段尚不存在,它将被创建并递增到 1。

                增量在内部是事务性的,但如果您需要比每 1 秒更频繁地增加,则应使用 distributed counter

                通常最好实现 onCreateonDelete 而不是 onWrite,因为您将调用 onWrite 进行更新,这意味着您在不必要的函数调用上花费了更多的钱(如果您更新集合中的文档) .

                【讨论】:

                  【解决方案16】:

                  小心计算大型集合的文档数量。如果您想为每个集合设置一个预先计算的计数器,那么使用 firestore 数据库会有点复杂。

                  这样的代码在这种情况下不起作用:

                  export const customerCounterListener = 
                      functions.firestore.document('customers/{customerId}')
                      .onWrite((change, context) => {
                  
                      // on create
                      if (!change.before.exists && change.after.exists) {
                          return firestore
                                   .collection('metadatas')
                                   .doc('customers')
                                   .get()
                                   .then(docSnap =>
                                       docSnap.ref.set({
                                           count: docSnap.data().count + 1
                                       }))
                      // on delete
                      } else if (change.before.exists && !change.after.exists) {
                          return firestore
                                   .collection('metadatas')
                                   .doc('customers')
                                   .get()
                                   .then(docSnap =>
                                       docSnap.ref.set({
                                           count: docSnap.data().count - 1
                                       }))
                      }
                  
                      return null;
                  });
                  

                  原因是因为每个 Cloud Firestore 触发器都必须是幂等的,正如 Firestore 文档所说:https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees

                  解决方案

                  因此,为了防止代码多次执行,您需要使用事件和事务进行管理。这是我处理大型收集计数器的特殊方式:

                  const executeOnce = (change, context, task) => {
                      const eventRef = firestore.collection('events').doc(context.eventId);
                  
                      return firestore.runTransaction(t =>
                          t
                           .get(eventRef)
                           .then(docSnap => (docSnap.exists ? null : task(t)))
                           .then(() => t.set(eventRef, { processed: true }))
                      );
                  };
                  
                  const documentCounter = collectionName => (change, context) =>
                      executeOnce(change, context, t => {
                          // on create
                          if (!change.before.exists && change.after.exists) {
                              return t
                                      .get(firestore.collection('metadatas')
                                      .doc(collectionName))
                                      .then(docSnap =>
                                          t.set(docSnap.ref, {
                                              count: ((docSnap.data() && docSnap.data().count) || 0) + 1
                                          }));
                          // on delete
                          } else if (change.before.exists && !change.after.exists) {
                              return t
                                       .get(firestore.collection('metadatas')
                                       .doc(collectionName))
                                       .then(docSnap =>
                                          t.set(docSnap.ref, {
                                              count: docSnap.data().count - 1
                                          }));
                          }
                  
                          return null;
                      });
                  

                  这里的用例:

                  /**
                   * Count documents in articles collection.
                   */
                  exports.articlesCounter = functions.firestore
                      .document('articles/{id}')
                      .onWrite(documentCounter('articles'));
                  
                  /**
                   * Count documents in customers collection.
                   */
                  exports.customersCounter = functions.firestore
                      .document('customers/{id}')
                      .onWrite(documentCounter('customers'));
                  

                  如您所见,防止多次执行的关键是上下文对象中名为 eventId 的属性。如果函数已针对同一事件多次处理,则事件 ID 在所有情况下都相同。不幸的是,您的数据库中必须有“事件”集合。

                  【讨论】:

                  • 他们的措辞好像这种行为将在 1.0 版本中得到修复。亚马逊 AWS 功能也存在同样的问题。像计算字段这样简单的事情变得复杂且昂贵。
                  • 现在就尝试一下,因为它似乎是一个更好的解决方案。您是否会返回并清除您的事件集合?我正在考虑只添加一个日期字段并清除一天以上的时间,或者只是为了保持数据集很小(可能每天 100 万+个事件)。除非 FS 有一种简单的方法可以做到这一点……只使用 FS 几个月。
                  • 我们能否验证context.eventId 在多次调用同一个触发器时总是相同的?在我的测试中,它似乎是一致的,但我找不到任何说明这一点的“官方”文档。
                  • 所以在使用了一段时间后,我发现,虽然这个解决方案只适用于一次写入,但如果同时写入多个文档并尝试触发太多触发器,这很好更新相同的计数文档,您可以从 firestore 获取争用错误。你有没有遇到过这些,你是如何解决的? (错误:10 ABORTED:这些文档争用过多。请重试。)
                  • @TymPollack 查看distributed counters 文档写入被限制为每秒大约一次更新
                  【解决方案17】:

                  我同意@Matthew,如果您执行这样的查询,它将花费很多

                  [开发者开始项目前的建议]

                  由于我们一开始就预见到了这种情况,我们实际上可以用一个文档来做一个集合,即计数器,将所有计数器存储在一个类型为number的字段中。

                  例如:

                  对于集合上的每个 CRUD 操作,更新计数器文档:

                  1. 当您创建一个新的集合/子集合时:(在计数器中+1) [1 次写入操作]
                  2. 当您删除集合/子集合时:(计数器中的-1) [1 次写入操作]
                  3. 更新现有集合/子集合时,对计数器文档不执行任何操作:(0)
                  4. 当您读取现有集合/子集合时,对计数器文档不执行任何操作:(0)

                  下次要获取集合的数量时,只需要查询/指向文档字段即可。 [1 次读取操作]

                  另外,你可以将集合名称存储在数组中,但这会很棘手,firebase中数组的条件如下所示:

                  // we send this
                  ['a', 'b', 'c', 'd', 'e']
                  // Firebase stores this
                  {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
                  
                  // since the keys are numeric and sequential,
                  // if we query the data, we get this
                  ['a', 'b', 'c', 'd', 'e']
                  
                  // however, if we then delete a, b, and d,
                  // they are no longer mostly sequential, so
                  // we do not get back an array
                  {2: 'c', 4: 'e'}
                  

                  所以,如果你不打算删除集合,你实际上可以使用数组来存储集合名称的列表,而不是每次都查询所有集合。

                  希望对你有帮助!

                  【讨论】:

                  • 对于一个小收藏,也许。但请记住,Firestore 文档大小限制为 ~1MB,如果集合中的文档 ID 是自动生成的(20 个字节),那么您只能在保存数组的文档之前存储 ~52,425 个太大了。我想作为一种解决方法,您可以每 50,000 个元素创建一个新文档,但是维护这些数组将完全无法管理。此外,随着文档大小的增长,读取和更新将需要更长的时间,这最终会导致对其进行的任何其他操作都超时。
                  【解决方案18】:
                  firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
                          @Override
                          public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
                  
                              int Counter = documentSnapshots.size();
                  
                          }
                      });
                  

                  【讨论】:

                  • 这个答案可以使用更多的上下文作为代码示例。
                  【解决方案19】:

                  最简单的方法是读取“querySnapshot”的大小。

                  db.collection("cities").get().then(function(querySnapshot) {      
                      console.log(querySnapshot.size); 
                  });
                  

                  您还可以在“querySnapshot”中读取 docs 数组的长度。

                  querySnapshot.docs.length;
                  

                  或者如果“querySnapshot”通过读取空值是空的,这将返回一个布尔值。

                  querySnapshot.empty;
                  

                  【讨论】:

                  • 请注意,每份文档“需要”阅读一次。因此,如果您以这种方式计算一个集合中的 100 个项目,您需要为 100 次读取付费!
                  • 正确,但没有其他方法可以总结集合中的文档数量。如果您已经提取了集合,则读取“docs”数组将不再需要提取,因此不会“花费”更多读数。
                  • 这会读取内存中的所有文件!对于大型数据集,祝你好运......
                  • Firebase Firestore 没有db.collection.count(),这真是令人难以置信。考虑只为此而放弃它们
                  • 特别是对于大型馆藏,如果我们真的下载并使用了所有文档,就向我们收费是不公平的。表(集合)的计数就是这样一个基本功能。考虑到他们的定价模式和 Firestore 于 2017 年推出,令人难以置信的是,Google 没有提供另一种方法来获取集合的大小。在他们不实施之前,他们至少应该避免为此收费。
                  【解决方案20】:

                  不,目前没有对聚合查询的内置支持。但是,您可以做一些事情。

                  第一个是documented here。您可以使用事务或云功能来维护汇总信息:

                  此示例展示了如何使用函数来跟踪子集合中的评分数量以及平均评分。

                  exports.aggregateRatings = firestore
                    .document('restaurants/{restId}/ratings/{ratingId}')
                    .onWrite(event => {
                      // Get value of the newly added rating
                      var ratingVal = event.data.get('rating');
                  
                      // Get a reference to the restaurant
                      var restRef = db.collection('restaurants').document(event.params.restId);
                  
                      // Update aggregations in a transaction
                      return db.transaction(transaction => {
                        return transaction.get(restRef).then(restDoc => {
                          // Compute new number of ratings
                          var newNumRatings = restDoc.data('numRatings') + 1;
                  
                          // Compute new average rating
                          var oldRatingTotal = restDoc.data('avgRating') * restDoc.data('numRatings');
                          var newAvgRating = (oldRatingTotal + ratingVal) / newNumRatings;
                  
                          // Update restaurant info
                          return transaction.update(restRef, {
                            avgRating: newAvgRating,
                            numRatings: newNumRatings
                          });
                        });
                      });
                  });
                  

                  如果您只想不频繁地统计文档,jbb 提到的解决方案也很有用。确保使用select() 语句来避免下载每个文档的所有内容(当您只需要计数时,这会占用大量带宽)。 select() 目前仅在服务器 SDK 中可用,因此该解决方案无法在移动应用中运行。

                  【讨论】:

                  • 此解决方案不是幂等的,因此任何多次触发的触发器都会影响您的评分数和平均值。
                  猜你喜欢
                  • 2022-10-20
                  • 2021-09-03
                  • 1970-01-01
                  • 2021-05-06
                  • 2020-04-07
                  相关资源
                  最近更新 更多