【问题标题】:How to get a count of number of documents in a collection with Cloud Firestore [duplicate]如何使用 Cloud Firestore 获取集合中文档的数量
【发布时间】:2018-03-15 03:58:29
【问题描述】:

在 Firestore 中,如何获取集合中的文档总数?

例如,如果我有

/people
    /123456
        /name - 'John'
    /456789
        /name - 'Jane'

我想查询我有多少人,得到2个。

我可以对 /people 进行查询,然后获取返回结果的长度,但这似乎是一种浪费,尤其是因为我将在更大的数据集上执行此操作。

【问题讨论】:

  • 我一直在使用 db.collection('products').get().then(res => console.log(res.size)) 这给了我该集合中的文档数量这似乎工作
  • @BenCochrane,如果您有大量文档,这将不起作用。
  • 我想你可能也对这篇文章感兴趣,How to count the number of documents in a Firestore collection?

标签: database firebase google-cloud-firestore


【解决方案1】:

您目前有 3 个选项:

选项 1:客户端

这基本上就是你提到的方法。从集合中选择所有并在客户端进行计数。这对于小型数据集来说足够好,但如果数据集更大,显然就不起作用了。

选项 2:写入时尽力而为

通过这种方法,您可以使用 Cloud Functions 为集合中的每次添加和删除更新计数器。

这适用于任何数据集大小,只要添加/删除的发生率小于或等于每秒 1 次。这为您提供了一个可供阅读的文档,以便立即为您提供几乎当前的计数。

如果需要超过每秒1次,则需要实现distributed counters per our documentation

选项 3:精确写入时间

在您的客户端中,您可以在添加或删除文档的同时更新计数器,而不是使用 Cloud Functions。这意味着计数器也将是最新的,但您需要确保在添加或删除文档的任何位置都包含此逻辑。

与选项 2 一样,如果您想超过每秒,则需要实现分布式计数器

【讨论】:

  • 只是想知道,将来是否有其他方法可以做到这一点?像 SQL COUNT 聚合函数这样简单的东西可以在大型数据集上工作,同时保持良好的性能?
  • @saricden COUNT 聚合实际存在很多潜在的性能问题。 Cloud Firestore 系统设计用于无论数据集大小都保持相同性能特征的操作,而 COUNT 则没有。我们正在寻找在未来取得平衡的选择。
  • 为什么我没有想到选项3!我的应用程序为每个文档使用 5 个分片,每次检索文档时将读取次数乘以 6。这确实效率低下并且花费了太多钱。选项 3 允许我在每次增加分片时更新文档总数。它需要 2 次写入而不是 1 次写入,但在读取方面确实值得。
  • @DanMcGrath 我是 FireStore 的新手,但在我看来,选项 3 在客户端安全范围内是有问题的。说我可以喜欢一个帖子。但我也必须能够更新计数器。因此,如果我是恶意用户,我可以将其设置为任何值。有解决办法吗?
  • 对于选项 2,我们如何确保相同的 Firestore 事件传递两次,不会导致计数器增加/减少两次?只有eventId上的交易才有可能吗?
【解决方案2】:

按照 Dan 的回答:您可以在数据库中拥有一个单独的计数器并使用 Cloud Functions 来维护它。 (写入时尽力而为

// Example of performing an increment when item is added
module.exports.incrementIncomesCounter = collectionRef.onCreate(event => {
  const counterRef = event.data.ref.firestore.doc('counters/incomes')

  counterRef.get()
  .then(documentSnapshot => {
    const currentCount = documentSnapshot.exists ? documentSnapshot.data().count : 0

    counterRef.set({
      count: Number(currentCount) + 1
    })
    .then(() => {
      console.log('counter has increased!')
    })
  })
})

此代码向您展示了如何执行此操作的完整示例: https://gist.github.com/saintplay/3f965e0aea933a1129cc2c9a823e74d7

【讨论】:

  • 你的操作不是原子的。换句话说,如果两个增量同时发生,您可能会失去一个。您可以将其包装在事务中,也可以使用更简单的 doc.update("count", firebase.firestore.FieldValue.increment(1));
  • @FranckJeannin 是的,这需要更新,因为 firebase.firestore.FieldValue.increment 是一个东西
【解决方案3】:

如果你使用 AngulareFire2,你可以这样做(假设 private afs: AngularFirestore 被注入到你的构造函数中):

this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));

这里,valuesmyCollection 中所有项目的数组。您不需要元数据,因此您可以直接使用valueChanges() 方法。

【讨论】:

  • 如果集合包含数百万个文档怎么办?
【解决方案4】:

聚合是要走的路(firebase 函数看起来像是更新这些聚合的推荐方法,因为客户端会向用户公开您可能不想公开的信息)https://firebase.google.com/docs/firestore/solutions/aggregation

另一种方式(不推荐)不适合大型列表并涉及下载整个列表: res.size 像这个例子:

   db.collection("logs")
      .get()
      .then((res) => console.log(res.size));

【讨论】:

  • 这需要您下载整个产品集合才能获得大小。这可能对大型收藏极为不利
  • @justinbc820 你是对的 - 我已经修改了我的答案以推荐聚合而不是firebase.google.com/docs/firestore/solutions/aggregation
  • 例如,如果您有一个应将结果限制在 1 和 0 之间的 where 子句,则不需要下载整个集合。喜欢我的
  • res.size 总是返回零不知道我做错了什么。
【解决方案5】:

使用云功能仔细计算大型集合的文档数量。如果您想为每个集合设置一个预先计算的计数器,那么使用 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 在所有情况下都相同。不幸的是,您的数据库中必须有“事件”集合。

【讨论】:

    【解决方案6】:

    请检查我在另一个线程上找到的以下答案。你的计数应该是原子的。在这种情况下需要使用 FieldValue.increment() 函数。

    https://stackoverflow.com/a/49407570/3337028

    【讨论】:

      【解决方案7】:

      使用 Transaction 更新数据库写入成功侦听器中的计数。

      FirebaseFirestore.getInstance().runTransaction(new Transaction.Function<Long>() {
                      @Nullable
                      @Override
                      public Long apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
                          DocumentSnapshot snapshot = transaction
                                  .get(pRefs.postRef(forumHelper.getPost_id()));
                          long newCount;
                          if (b) {
                              newCount = snapshot.getLong(kMap.like_count) + 1;
                          } else {
                              newCount = snapshot.getLong(kMap.like_count) - 1;
                          }
      
                          transaction.update(pRefs.postRef(forumHelper.getPost_id()),
                                  kMap.like_count, newCount);
      
                          return newCount;
                      }
                  });
      

      【讨论】:

        【解决方案8】:

        我创建了一个 NPM 包来处理所有计数器:

        首先在你的函数目录中安装模块:

        npm i adv-firestore-functions

        然后像这样使用它:

        import { eventExists, colCounter } from 'adv-firestore-functions';
        
        functions.firestore
            .document('posts/{docId}')
            .onWrite(async (change: any, context: any) => {
        
            // don't run if repeated function
            if (await eventExists(context)) {
              return null;
            }
        
            await colCounter(change, context);
        }
        

        它处理事件和其他一切。

        如果你想让它成为所有功能的通用计数器:

        import { eventExists, colCounter } from 'adv-firestore-functions';
        
        functions.firestore
            .document('{colId}/{docId}')
            .onWrite(async (change: any, context: any) => {
        
            const colId = context.params.colId;
        
            // don't run if repeated function
            if (await eventExists(context) || colId.startsWith('_')) {
              return null;
            }
        
            await colCounter(change, context);
        }
        

        不要忘记你的规则:

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

        当然也可以这样访问:

        const collectionPath = 'path/to/collection';
        const colSnap = await db.doc('_counters/' + collectionPath).get();
        const count = colSnap.get('count');
        

        阅读更多:https://fireblog.io/post/Zebl6sSbaLdrnSFKbCJx/firestore-counters
        GitHub:https://github.com/jdgamble555/adv-firestore-functions

        【讨论】:

          【解决方案9】:

          获取新的写入批次

          WriteBatch batch = db.batch();
          

          为“NYC”系列添加新价值

          DocumentReference nycRef = db.collection("cities").document();
          batch.set(nycRef, new City());
          

          维护一个文档,其中 Id 作为 Countinitial Value 作为 total=0

          在添加操作期间执行如下操作

          DocumentReference countRef= db.collection("cities").document("count");
          batch.update(countRef, "total", FieldValue.increment(1));
          

          在删除操作期间执行如下操作

          DocumentReference countRef= db.collection("cities").document("count");
          batch.update(countRef, "total", FieldValue.increment(-1));
          

          始终从以下位置获取文档计数

          DocumentReference nycRef = db.collection("cities").document("count");
          

          【讨论】:

            【解决方案10】:

            firebase-admin 提供select(fields),它允许您仅获取集合中文档的特定字段。使用select 比获取所有字段的性能更高。但是,它仅适用于firebase-admin,而firebase-admin 通常仅用于服务器端。

            select可以这样使用:

            select('age', 'name') // fetch the age and name fields
            select() // select no fields, which is perfect if you just want a count
            

            select 可用于 Node.js 服务器,但我不确定其他语言:

            https://googleapis.dev/nodejs/firestore/latest/Query.html#select https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#select

            这是一个用 Node.js 编写的服务器端云函数,它使用 select 来计算过滤后的集合并获取所有结果文档的 ID。它是用 TS 编写的,但很容易转换为 JS。

            import admin from 'firebase-admin'
            
            // https://stackoverflow.com/questions/46554091/cloud-firestore-collection-count
            
            // we need to use admin SDK here as select() is only available for admin
            export const videoIds = async (req: any): Promise<any> => {
            
              const id: string = req.query.id || null
              const group: string = req.query.group || null
              let processed: boolean = null
              if (req.query.processed === 'true') processed = true
              if (req.query.processed === 'false') processed = false
            
              let q: admin.firestore.Query<admin.firestore.DocumentData> = admin.firestore().collection('videos')
              if (group != null) q = q.where('group', '==', group)
              if (processed != null) q = q.where('flowPlayerProcessed', '==', processed)
              // select restricts returned fields such as ... select('id', 'name')
              const query: admin.firestore.QuerySnapshot<admin.firestore.DocumentData> = await q.orderBy('timeCreated').select().get()
            
              const ids: string[] = query.docs.map((doc: admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>) => doc.id) // ({ id: doc.id, ...doc.data() })
            
              return {
                id,
                group,
                processed,
                idx: id == null ? null : ids.indexOf(id),
                count: ids.length,
                ids
              }
            }
            

            对于 500 个文档的集合,云函数 HTTP 请求在 1 秒内完成,每个文档包含大量数据。性能并不惊人,但比不使用select 要好得多。通过引入客户端缓存(甚至服务器端缓存)可以提高性能。

            云函数入口点如下所示:

            exports.videoIds = functions.https.onRequest(async (req, res) => {
              const response: any = await videoIds(req)
              res.json(response)
            })
            

            HTTP 请求 URL 将是:

            https://SERVER/videoIds?group=my-group&processed=true
            

            Firebase 函数详细说明了服务器在部署时的位置。

            【讨论】:

              猜你喜欢
              • 2021-03-27
              • 2018-04-14
              • 1970-01-01
              • 2021-05-28
              • 2020-08-01
              • 1970-01-01
              • 1970-01-01
              • 2021-05-09
              相关资源
              最近更新 更多