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

如果是,我该怎么做?


当前回答

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

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

请不要,这是为nodeJS

其他回答

我同意@Matthew的观点,如果你执行这样的查询,成本会很高。

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

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

例如:

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

当你创建一个新的集合/子集合时:(在计数器中+1)[1写操作] 当你删除一个集合/子集合时:(-1在计数器中)[1写操作] 当你更新一个现有的集合/子集合时,在counter文档上什么都不做:(0) 当你读取一个现有的集合/子集合时,在计数器文档上什么都不做:(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'}

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

希望能有所帮助!

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

FireStore现在支持该功能,尽管是Beta版。 下面是Firebase的官方文档

对于大量的收藏,要仔细计算文件的数量。如果你想为每个收集都有一个预先计算好的计数器,那么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;
});

原因是每个云防火墙触发器都必须是幂等的,正如防火墙文档所示: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在所有情况下都是相同的。不幸的是,您的数据库中必须有“事件”集合。

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

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

你也可以在querySnapshot中读取文档数组的长度。

querySnapshot.docs.length;

或者如果“querySnapshot”为空,则读取空值,将返回一个布尔值。

querySnapshot.empty;