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

如果是,我该怎么做?


当前回答

和许多问题一样,答案是——视情况而定。

在前端处理大量数据时应该非常小心。除了让你的前端感觉迟钝之外,Firestore还会向你收取每百万次读取60美元的费用。


小型收藏(少于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 ()

截至2019年4月,Firestore现在允许增量计数器,完全原子,无需事先读取数据。这确保了即使同时从多个源进行更新(以前使用事务解决)也能获得正确的计数器值,同时还减少了执行的数据库读取次数。


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

参见firestore文档-分布式计数器 或者看看杰夫·德莱尼的《数据聚合》。他的指南对于任何使用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中预览。

在2022年Firebase峰会上宣布:https://firebase.blog/posts/2022/10/whats-new-at-Firebase-Sumit-2022

摘录:

[开发人员预览]Count()函数:与新的计数函数 Firstore[原文],你现在可以得到匹配文件的计数当你 运行查询或从集合中读取,而不加载实际的 文件,这为你节省了很多时间。

他们在峰会上展示的代码示例:

在问答环节中,有人问了汇总查询的定价问题,Firebase团队给出的答案是,它的成本是读取价格的1 / 1000(四舍四入到最近的读取,详情见下面的评论),但将计算汇总的所有记录。

除了上面我的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函数:

set

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

更多信息请看这里…

J

截至2022年10月,Firestore在客户端sdk上引入了count()方法。现在您可以在没有下载的情况下计算查询。

对于1000份文件,它将收取你阅读1份文件的费用。

网络(v9)

Firebase 9.11.0介绍:

const collectionRef = collection(db, "cities");
const snapshot = await getCountFromServer(collectionRef);
console.log('count: ', snapshot.data().count);

Web V8

不可用。

节点(管理)

const collectionRef = db.collection('cities');
const snapshot = await collectionRef.count().get();
console.log(snapshot.data().count);

Android (Kotlin)

在firestore v24.4.0 (BoM 31.0.0)引入:

val query = db.collection("cities")
val countQuery = query.count()
countQuery.get(AggregateSource.SERVER).addOnCompleteListener { task ->
    if (task.isSuccessful) {
        val snapshot = task.result
        Log.d(TAG, "Count: ${snapshot.count}")
    } else {
        Log.d(TAG, "Count failed: ", task.getException())
    }
}

苹果平台(Swift)

Firestore v10.0.0引入:

do {
  let query = db.collection("cities")
  let countQuery = query.countAggregateQuery
  let snapshot = try await countQuery.aggregation(source: AggregateSource.server)
  print(snapshot.count)
} catch {
  print(error)
}

和许多问题一样,答案是——视情况而定。

在前端处理大量数据时应该非常小心。除了让你的前端感觉迟钝之外,Firestore还会向你收取每百万次读取60美元的费用。


小型收藏(少于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 ()

截至2019年4月,Firestore现在允许增量计数器,完全原子,无需事先读取数据。这确保了即使同时从多个源进行更新(以前使用事务解决)也能获得正确的计数器值,同时还减少了执行的数据库读取次数。


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

参见firestore文档-分布式计数器 或者看看杰夫·德莱尼的《数据聚合》。他的指南对于任何使用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字段来获得集合的大小。

目前,firebase只允许服务器端计数,如下所示

const collectionRef = db.collection('cities');
const snapshot = await collectionRef.count().get();
console.log(snapshot.data().count);

请不要,这是为nodeJS