如何使用 Cloud Firestore 计算集合中文档的数量

How to get a count of number of documents in a collection with Cloud Firestore

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

例如,如果我有

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

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

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

您目前有 3 个选项:

选项 1:客户端

这基本上就是你提到的方法。 Select 全部来自客户端的收集和计数。这对于小型数据集来说效果很好,但如果数据集较大,则显然不起作用。

选项 2:写入时尽力而为

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

这适用于任何数据集大小,只要 additions/deletions 仅以小于或等于每秒 1 的速率发生。这为您提供了一个可供阅读的文档,可以立即为您提供 几乎最新的 计数。

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

选项 3:准确写入时间

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

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

Following Dan Answer: 你可以在你的数据库中有一个单独的计数器,并使用 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

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

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

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

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

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

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

使用云函数计算 大型集合 的文档数量时要小心。如果您希望每个集合都有一个预先计算的计数器,那么使用 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;
});

原因是因为每个云 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 在所有情况下都将相同。不幸的是,您的数据库中必须有 "events" 个集合。

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

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

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;
                }
            });

我创建了一个 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

获取新的写入批次

WriteBatch batch = db.batch();

为集合“NYC”添加新值

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

使用 Id 作为 Count 初始值 作为 维护文档]总计=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");

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'

// 

// 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
  }
}

云函数 HTTP 请求在 1 秒内完成,收集了 500 个文档,每个文档包含大量数据。性能并不惊人,但比不使用 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 功能详细说明服务器在部署时的位置。