在 Firestore 中,如何获取集合中的文档总数?
例如,如果我有
/people
/123456
/name - 'John'
/456789
/name - 'Jane'
我想查询我有多少人并得到2。
我可以对 /people 进行查询,然后获取返回结果的长度,但这似乎是一种浪费,尤其是因为我将在更大的数据集上执行此操作。
在 Firestore 中,如何获取集合中的文档总数?
例如,如果我有
/people
/123456
/name - 'John'
/456789
/name - 'Jane'
我想查询我有多少人并得到2。
我可以对 /people 进行查询,然后获取返回结果的长度,但这似乎是一种浪费,尤其是因为我将在更大的数据集上执行此操作。
您目前有 3 个选项:
这基本上就是你提到的方法。从集合中选择所有并在客户端进行计数。这对于小型数据集来说足够好,但如果数据集更大,显然不起作用。
通过这种方法,您可以使用 Cloud Functions 为集合中的每次添加和删除更新计数器。
这适用于任何数据集大小,只要添加/删除仅以小于或等于每秒 1 的速率发生。这为您提供了一个可供阅读的文档,以便立即为您提供几乎当前的计数。
如果需要每秒超过 1 个,您需要按照我们的文档实现分布式计数器。
在您的客户端中,您可以在添加或删除文档的同时更新计数器,而不是使用 Cloud Functions。这意味着计数器也将是最新的,但您需要确保在添加或删除文档的任何地方都包含此逻辑。
与选项 2 一样,如果您想超过每秒,则需要实现分布式计数器
聚合是要走的路(firebase 函数看起来像是更新这些聚合的推荐方法,因为客户端向您可能不希望公开的用户公开信息)https://firebase.google.com/docs/firestore/solutions/aggregation
另一种方式(不推荐)不适合大型列表并涉及下载整个列表: res.size 像这个例子:
db.collection("logs")
.get()
.then((res) => console.log(res.size));
如果你使用 AngulareFire2,你可以这样做(假设private afs: AngularFirestore
在你的构造函数中注入):
this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));
这里,values
是一个包含所有项目的数组myCollection
。您不需要元数据,因此您可以valueChanges()
直接使用方法。
小心计算具有云功能的大型集合的文档数量。如果您想为每个集合设置一个预先计算的计数器,那么使用 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 在所有情况下都相同。不幸的是,您的数据库中必须有“事件”集合。
请检查我在另一个线程上找到的以下答案。你的计数应该是原子的。在这种情况下需要使用FieldValue.increment()函数。
firebase-admin允许select(fields)
您仅获取集合中文档的特定字段。使用select
比获取所有字段更高效。但是,它仅适用于firebase-admin
并且firebase-admin
通常仅用于服务器端。
select
可以如下使用:
select('age', 'name') // fetch the age and name fields
select() // select no fields, which is perfect if you just want a count
select
可用于 Node.js 服务器,但我不确定其他语言:
https://googleapis.dev/nodejs/firestore/latest/Query.html#select https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#select
这是一个用 Node.js 编写的服务器端云函数,用于select
计算过滤后的集合并获取所有结果文档的 ID。它是用 TS 编写的,但很容易转换为 JS。
import admin from 'firebase-admin'
// https://stackoverflow.com/questions/46554091/cloud-firestore-collection-count
// we need to use admin SDK here as select() is only available for admin
export const videoIds = async (req: any): Promise<any> => {
const id: string = req.query.id || null
const group: string = req.query.group || null
let processed: boolean = null
if (req.query.processed === 'true') processed = true
if (req.query.processed === 'false') processed = false
let q: admin.firestore.Query<admin.firestore.DocumentData> = admin.firestore().collection('videos')
if (group != null) q = q.where('group', '==', group)
if (processed != null) q = q.where('flowPlayerProcessed', '==', processed)
// select restricts returned fields such as ... select('id', 'name')
const query: admin.firestore.QuerySnapshot<admin.firestore.DocumentData> = await q.orderBy('timeCreated').select().get()
const ids: string[] = query.docs.map((doc: admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>) => doc.id) // ({ id: doc.id, ...doc.data() })
return {
id,
group,
processed,
idx: id == null ? null : ids.indexOf(id),
count: ids.length,
ids
}
}
对于 500 个文档的集合,云函数 HTTP 请求在 1 秒内完成,其中每个文档包含大量数据。性能并不惊人,但比不使用要好得多select
。通过引入客户端缓存(甚至服务器端缓存)可以提高性能。
云函数入口点如下所示:
exports.videoIds = functions.https.onRequest(async (req, res) => {
const response: any = await videoIds(req)
res.json(response)
})
HTTP 请求 URL 将是:
https://SERVER/videoIds?group=my-group&processed=true
Firebase 函数详细说明了服务器在部署时的位置。
获取新的写入批次
WriteBatch batch = db.batch();
为“NYC”系列增添新价值
DocumentReference nycRef = db.collection("cities").document();
batch.set(nycRef, new City());
维护一个ID为Count且初始值为 total=0的文档
在添加操作期间执行如下
DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(1));
在删除操作期间执行如下
DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(-1));
总是从
DocumentReference nycRef = db.collection("cities").document("count");
我创建了一个 NPM 包来处理所有计数器:
首先在你的函数目录中安装模块:
npm i adv-firestore-functions
然后像这样使用它:
import { eventExists, colCounter } from 'adv-firestore-functions';
functions.firestore
.document('posts/{docId}')
.onWrite(async (change: any, context: any) => {
// don't run if repeated function
if (await eventExists(context)) {
return null;
}
await colCounter(change, context);
}
它处理事件和其他一切。
如果你想让它成为所有功能的通用计数器:
import { eventExists, colCounter } from 'adv-firestore-functions';
functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change: any, context: any) => {
const colId = context.params.colId;
// don't run if repeated function
if (await eventExists(context) || colId.startsWith('_')) {
return null;
}
await colCounter(change, context);
}
不要忘记你的规则:
match /_counters/{document} {
allow read;
allow write: if false;
}
当然可以通过这种方式访问它:
const collectionPath = 'path/to/collection';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');
阅读更多:https
:
//fireblog.io/post/Zebl6sSbaLdrnSFKbCJx/firestore-counters GitHub:https ://github.com/jdgamble555/adv-firestore-functions
按照 Dan 的回答:您可以在数据库中有一个单独的计数器并使用 Cloud Functions 来维护它。(写时尽力而为)
// Example of performing an increment when item is added
module.exports.incrementIncomesCounter = collectionRef.onCreate(event => {
const counterRef = event.data.ref.firestore.doc('counters/incomes')
counterRef.get()
.then(documentSnapshot => {
const currentCount = documentSnapshot.exists ? documentSnapshot.data().count : 0
counterRef.set({
count: Number(currentCount) + 1
})
.then(() => {
console.log('counter has increased!')
})
})
})
此代码向您展示了如何执行此操作的完整示例: https ://gist.github.com/saintplay/3f965e0aea933a1129cc2c9a823e74d7
使用 Transaction 更新数据库写入成功侦听器中的计数。
FirebaseFirestore.getInstance().runTransaction(new Transaction.Function<Long>() {
@Nullable
@Override
public Long apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction
.get(pRefs.postRef(forumHelper.getPost_id()));
long newCount;
if (b) {
newCount = snapshot.getLong(kMap.like_count) + 1;
} else {
newCount = snapshot.getLong(kMap.like_count) - 1;
}
transaction.update(pRefs.postRef(forumHelper.getPost_id()),
kMap.like_count, newCount);
return newCount;
}
});