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

如果是,我该怎么做?


当前回答

截至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)
}

其他回答

据我所知,目前还没有内置的解决方案,只能在节点sdk中实现。 如果你有

db.collection('someCollection')

你可以使用

.select([fields])

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

例子:

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

此解决方案只是针对下载所有文档的最坏情况的优化,并且不能扩展到大型集合!

再看看这个: 如何获得在一个集合与云Firestore的文件的数量计数

不,目前还没有内置的聚合查询支持。然而,有几件事你可以做。

这里记录了第一个。您可以使用事务或云函数来维护聚合信息:

这个例子展示了如何使用一个函数来跟踪子集合中的评级数量,以及平均评级。

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中可用,因此该解决方案不适用于移动应用程序。

firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
        @Override
        public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {

            int Counter = documentSnapshots.size();

        }
    });

其中一个快速省钱的技巧是:-

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

注:计数用(-1)递减,用(1)递增

如何节省金钱和时间:-

你(firebase)不需要循环通过集合,浏览器也不需要加载整个集合来计数文档的数量。 所有的计数都保存在一个名为“count”或其他变量的文档中,因此使用不到1kb的数据,并且它只在firebase firestore中使用1次读取。

更新11/20

为了方便访问计数器函数,我创建了一个npm包:https://code.build/p/9DicAmrnRoK4uk62Hw1bEV/firestore-counters


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

唯一的例外是当一秒钟写这么多的时候,它 放慢你的速度。一个例子就是热门帖子上的点赞。它是 例如,在一篇博客文章上写得太多,会让你付出更多的代价。我 建议在这种情况下使用shards创建一个单独的函数: 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中的集合。

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数据库,重复的代码和数据可以节省你 钱。