使用新的Firebase数据库Cloud Firestore,是否可以计算集合有多少项?
如果是这样,我该怎么做?
使用新的Firebase数据库Cloud Firestore,是否可以计算集合有多少项?
如果是这样,我该怎么做?
Answers:
与许多问题一样,答案是- 它取决于。
在前端处理大量数据时,您应该非常小心。除了使您的前端变慢之外,Firestore还向您收取每百万次读取0.60美元的费用。
谨慎使用-前端用户体验可能会受到打击
只要您对返回的数组不做太多逻辑,就可以在前端进行处理。
db.collection('...').get().then(snap => {
size = snap.size // will return the collection size
});
谨慎使用-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
})
最具扩展性的解决方案
FieldValue.increment()
自2019年4月起,Firestore现在允许完全原子地递增计数器,并且无需事先读取数据。这可以确保即使同时从多个源进行更新(以前使用事务解决)时,我们也具有正确的计数器值,同时还减少了我们执行的数据库读取次数。
通过侦听任何文档的删除或创建,我们可以添加到数据库中的计数字段或从其中删除。
查看firestore文档- 分布式计数器 或查看Jeff Delaney撰写的Data Aggregation。对于使用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.runTransaction { ... }
块中。这样可以解决访问的并发问题numberOfDocs
。
最简单的方法是读取“ querySnapshot”的大小。
db.collection("cities").get().then(function(querySnapshot) {
console.log(querySnapshot.size);
});
您还可以在“ querySnapshot”中读取docs数组的长度。
querySnapshot.docs.length;
或者,如果通过读取空值来使“ querySnapshot”为空,则将返回布尔值。
querySnapshot.empty;
db.collection.count()
。考虑仅为此丢弃它们
据我所知,目前还没有内置解决方案,现在只能在sdk节点中进行。如果你有一个
db.collection('someCollection')
您可以使用
.select([fields])
定义要选择的字段。如果您执行一个空的select(),则只会得到一组文档引用。
例:
db.collection('someCollection').select().get().then(
(snapshot) => console.log(snapshot.docs.length)
);
此解决方案仅是针对下载所有文档的最坏情况的优化,不能在大型馆藏上扩展!
还可以看一下:
如何使用Cloud Firestore获取集合中文档的数量
select(['_id'])
它比select()
小心盘点大量收藏的文件数。如果您想为每个集合都拥有一个预先计算的计数器,那么对于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在所有情况下均相同。不幸的是,您必须在数据库中具有“事件”集合。
context.eventId
在同一触发器的多次调用中将始终相同吗?在我的测试中,它看起来是一致的,但是我找不到任何说明这一点的“官方”文档。
在2020年,此功能在Firebase SDK中仍然不可用,但是在Firebase Extensions(Beta)中可用,但是设置和使用起来相当复杂...
合理的方法
Helpers ...(创建/删除似乎多余,但比onUpdate便宜)
export const onCreateCounter = () => async (
change,
context
) => {
const collectionPath = change.ref.parent.path;
const statsDoc = db.doc("counters/" + collectionPath);
const countDoc = {};
countDoc["count"] = admin.firestore.FieldValue.increment(1);
await statsDoc.set(countDoc, { merge: true });
};
export const onDeleteCounter = () => async (
change,
context
) => {
const collectionPath = change.ref.parent.path;
const statsDoc = db.doc("counters/" + collectionPath);
const countDoc = {};
countDoc["count"] = admin.firestore.FieldValue.increment(-1);
await statsDoc.set(countDoc, { merge: true });
};
export interface CounterPath {
watch: string;
name: string;
}
出口Firestore挂钩
export const Counters: CounterPath[] = [
{
name: "count_buildings",
watch: "buildings/{id2}"
},
{
name: "count_buildings_subcollections",
watch: "buildings/{id2}/{id3}/{id4}"
}
];
Counters.forEach(item => {
exports[item.name + '_create'] = functions.firestore
.document(item.watch)
.onCreate(onCreateCounter());
exports[item.name + '_delete'] = functions.firestore
.document(item.watch)
.onDelete(onDeleteCounter());
});
在行动
将跟踪建筑物的根集合和所有子集合。
在/counters/
根目录下
现在收款计数将自动更新,并最终更新!如果您需要计数,只需使用收集路径并在前面加上前缀counters
。
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const collectionCount = await db
.doc('counters/' + collectionPath)
.get()
.then(snap => snap.get('count'));
我同意@Matthew,如果执行这样的查询,它将花费很多。
[开发者的建议在开始他们的项目之前]
由于我们在一开始就预见到了这种情况,因此我们实际上可以进行一个收集,即使用文档创建计数器,以将所有计数器存储在type为的字段中number
。
例如:
对于集合上的每个CRUD操作,更新计数器文档:
下次,当您要获取托收数量时,只需查询/指向文档字段。[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'}
因此,如果您不打算删除collection,则实际上可以使用array来存储collection名称列表,而不是每次都查询所有collection。
希望能帮助到你!
不,目前没有对聚合查询的内置支持。但是,您可以做一些事情。
第一个记录在这里。您可以使用事务或云功能来维护汇总信息:
本示例说明如何使用函数来跟踪子集合中的评分数量以及平均评分。
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中可用,因此该解决方案无法在移动应用中使用。
没有直接可用的选项。你做不到db.collection("CollectionName").count()
。以下是找到集合中文档数的两种方法。
db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})
通过使用上述代码,您的文档读取将等于集合中文档的大小,这就是为什么必须避免使用上述解决方案的原因。
db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})
上面我们创建了一个带有名称计数的文档来存储所有计数信息。您可以通过以下方式更新计数文档:-
价格(文档读取= 1)和快速的数据检索上述解决方案是好的。
使用admin.firestore.FieldValue.increment递增计数器:
exports.onInstanceCreate = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
.onCreate((snap, context) =>
db.collection('projects').doc(context.params.projectId).update({
instanceCount: admin.firestore.FieldValue.increment(1),
})
);
exports.onInstanceDelete = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
.onDelete((snap, context) =>
db.collection('projects').doc(context.params.projectId).update({
instanceCount: admin.firestore.FieldValue.increment(-1),
})
);
在此示例instanceCount
中,每次将文档添加到instances
子集合时,我们都会在项目中增加一个字段。如果该字段不存在,它将被创建并增加到1。
增量在内部是事务性的,但是您应该使用分布式计数器如果您需要比1秒更频繁地递增,。
这往往是最好的实现onCreate
和onDelete
而不是onWrite
因为你叫onWrite
更新,这意味着你是在不必要的函数调用花更多的钱(如果您更新集合中的文档)。
解决方法是:
在firebase doc中编写一个计数器,每次创建新条目时在事务中增加一个计数器
您可以将计数存储在新条目的字段中(即位置:4)。
然后,在该字段(位置DESC)上创建索引。
您可以对查询执行skip +limit。Where(“ position”,“ <” x).OrderBy(“ position”,DESC)
希望这可以帮助!
我使用所有这些想法创建了一个通用函数来处理所有计数器情况(查询除外)。
唯一的例外是在一秒钟内执行大量写入操作时,它会使您减速。例如,在热门帖子中喜欢。例如,这在博客文章上就显得过于矫kill过正,并且会使您付出更多。我建议在这种情况下使用分片创建单独的函数: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作业(计划功能)以删除旧事件,以节省数据库存储空间。您至少需要一个出色的计划,并且可能会有更多配置。例如,您可以在每个星期日的晚上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.rules中的集合:
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数据库一样,重复的代码和数据可以为您节省金钱。
花了我一些时间才能根据上面的一些答案来完成这项工作,所以我想与他人分享一下。我希望它有用。
'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;
});
我尝试了很多不同的方法。最后,我改进了其中一种方法。首先,您需要创建一个单独的集合并将所有事件保存在那里。其次,您需要创建一个新的lambda来触发时间。此lambda将对事件收集中的事件进行计数并清除事件文档。文章中的代码详细信息。 https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca
该查询将导致文档计数。
this.db.collection(doc).get().subscribe((data) => {
count = data.docs.length;
});
console.log(count)
firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
int Counter = documentSnapshots.size();
}
});