Reputation: 79
I like to count the number of documents in a subcollection with firestore cloud functions.
My database looks like that: groups/{groupId}/members/{memberId}
I like to count the number of members (memberId) for each group. That means every group can have a different amount of members, and they can increase or decrease flexible.
Would be happy about your ideas :-).
Upvotes: 4
Views: 2350
Reputation: 83093
I think about two possible approaches.
1. Directly count the document of the collection
You would use the size
property of the QuerySnapshot
like
admin.firestore().collection('groups/{groupId}/members/{memberId}')
.get()
.then(querySnapshot => {
console.log(querySnapshot.size);
//....
return null;
});
The main problem here is the cost, if the sub-collection contains a lot of documents: by executing this query you will be charged for one read for each doc of the sub-collection.
2. Another approach is to maintain some counters for each sub-collection
You would write two Cloud Functions, based on Distributed Counters, as presented in this Firebase documentation item: https://firebase.google.com/docs/firestore/solutions/counters. We use 3 shards in the following example.
Firstly a Cloud Function would increase the counter when a new doc is added to the subCollec
sub-collection:
//....
const num_shards = 3;
//....
exports.incrementSubCollecCounter = functions
.firestore.document('groups/{groupId}/members/{memberId}')
.onCreate((snap, context) => {
const groupId = context.params.groupId;
const shard_id = Math.floor(Math.random() * num_shards).toString();
const shard_ref = admin
.firestore()
.collection('shards' + groupId)
.doc(shard_id);
if (!snap.data().counterIncremented) {
return admin.firestore().runTransaction(t => {
return t
.get(shard_ref)
.then(doc => {
if (!doc.exists) {
throw new Error(
'Shard doc #' +
shard_id +
' does not exist.'
);
} else {
const new_count = doc.data().count + 1;
return t.update(shard_ref, { count: new_count });
}
})
.then(() => {
return t.update(snap.ref, {
counterIncremented: true //This is important to have the Function idempotent, see https://cloud.google.com/functions/docs/bestpractices/tips#write_idempotent_functions
});
});
});
} else {
console.log('counterIncremented NOT NULL');
return null;
}
});
Then a second Cloud Function would decrease the counter when a doc is deleted from the subCollec
sub-collection:
exports.decrementSubCollecCounter = functions
.firestore.document('groups/{groupId}/members/{memberId}')
.onDelete((snap, context) => {
const groupId = context.params.groupId;
const shard_id = Math.floor(Math.random() * num_shards).toString();
const shard_ref = admin
.firestore()
.collection('shards' + groupId)
.doc(shard_id);
return admin.firestore().runTransaction(t => {
return t.get(shard_ref).then(doc => {
if (!doc.exists) {
throw new Error(
'Shard doc #' +
shard_id +
' does not exist.'
);
} else {
const new_count = doc.data().count - 1;
return t.update(shard_ref, { count: new_count });
}
});
});
});
Here, compared to solution 1, since we have 3 shards, when you want to know the number of docs in the subCollec
sub-collection, you need to read only 3 documents.
Have a look at the documentation for details about how to initialize the distributed counters. You have to initialize once for each groupId
collection (i.e. admin.firestore().collection('shards' + groupId)
)
Upvotes: 2
Reputation: 375
Took me a while to get this working, so thought I'd share it for others to use:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.countDocumentsChange = functions.firestore.document('library/{categoryId}/documents/{documentId}').onWrite((change, context) => {
const categoryId = context.params.categoryId;
const categoryRef = db.collection('library').doc(categoryId)
let FieldValue = require('firebase-admin').firestore.FieldValue;
if (!change.before.exists) {
// new document created : add one to count
categoryRef.update({numberOfDocs: FieldValue.increment(1)});
console.log("%s numberOfDocs incremented by 1", categoryId);
} else if (change.before.exists && change.after.exists) {
// updating existing document : Do nothing
} else if (!change.after.exists) {
// deleting document : subtract one from count
categoryRef.update({numberOfDocs: FieldValue.increment(-1)});
console.log("%s numberOfDocs decremented by 1", categoryId);
}
return 0;
});
Upvotes: 5