如何使用 Cloud Firestore 计算集合中文档的数量
How to get a count of number of documents in a collection with Cloud Firestore
在 Firestore 中,如何获取集合中的文档总数?
例如,如果我有
/people
/123456
/name - 'John'
/456789
/name - 'Jane'
我想查询我有多少人,得到2。
我可以对 /people 进行查询,然后获取返回结果的长度,但这似乎很浪费,尤其是因为我将在更大的数据集上执行此操作。
您目前有 3 个选项:
选项 1:客户端
这基本上就是你提到的方法。 Select 全部来自客户端的收集和计数。这对于小型数据集来说效果很好,但如果数据集较大,则显然不起作用。
选项 2:写入时尽力而为
通过这种方法,您可以使用 Cloud Functions 为集合中的每次添加和删除更新计数器。
这适用于任何数据集大小,只要 additions/deletions 仅以小于或等于每秒 1 的速率发生。这为您提供了一个可供阅读的文档,可以立即为您提供 几乎最新的 计数。
如果需要超过每秒1次,需要实现distributed counters per our documentation。
选项 3:准确写入时间
在您的客户端中,您可以在添加或删除文档的同时更新计数器,而不是使用 Cloud Functions。这意味着计数器也将是最新的,但您需要确保在添加或删除文档的任何地方都包含此逻辑。
与选项 2 一样,如果要超过每秒,则需要实施分布式计数器
Following Dan Answer: 你可以在你的数据库中有一个单独的计数器,并使用 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
如果你使用 AngulareFire2,你可以这样做(假设 private afs: AngularFirestore
被注入到你的构造函数中):
this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));
这里,values
是 myCollection
中所有项目的数组。您不需要元数据,因此可以直接使用 valueChanges()
方法。
聚合是可行的方法(firebase 函数看起来像是推荐的更新这些聚合的方法,因为客户端向您可能不希望公开的用户公开信息)https://firebase.google.com/docs/firestore/solutions/aggregation
另一种方式(不推荐)不适用于大型列表并且涉及下载整个列表:res.size 就像这个例子:
db.collection("logs")
.get()
.then((res) => console.log(res.size));
使用云函数计算 大型集合 的文档数量时要小心。如果您希望每个集合都有一个预先计算的计数器,那么使用 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 在所有情况下都将相同。不幸的是,您的数据库中必须有 "events" 个集合。
请检查我在另一个线程中找到的以下答案。你的计数应该是原子的。在这种情况下需要使用 FieldValue.increment() 函数。
使用事务来更新数据库写入成功侦听器中的计数。
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;
}
});
我创建了一个 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
获取新的写入批次
WriteBatch batch = db.batch();
为集合“NYC”添加新值
DocumentReference nycRef = db.collection("cities").document();
batch.set(nycRef, new City());
使用 Id 作为 Count 和 初始值 作为 维护文档]总计=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");
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'
//
// 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
}
}
云函数 HTTP 请求在 1 秒内完成,收集了 500 个文档,每个文档包含大量数据。性能并不惊人,但比不使用 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 功能详细说明服务器在部署时的位置。
在 Firestore 中,如何获取集合中的文档总数?
例如,如果我有
/people
/123456
/name - 'John'
/456789
/name - 'Jane'
我想查询我有多少人,得到2。
我可以对 /people 进行查询,然后获取返回结果的长度,但这似乎很浪费,尤其是因为我将在更大的数据集上执行此操作。
您目前有 3 个选项:
选项 1:客户端
这基本上就是你提到的方法。 Select 全部来自客户端的收集和计数。这对于小型数据集来说效果很好,但如果数据集较大,则显然不起作用。
选项 2:写入时尽力而为
通过这种方法,您可以使用 Cloud Functions 为集合中的每次添加和删除更新计数器。
这适用于任何数据集大小,只要 additions/deletions 仅以小于或等于每秒 1 的速率发生。这为您提供了一个可供阅读的文档,可以立即为您提供 几乎最新的 计数。
如果需要超过每秒1次,需要实现distributed counters per our documentation。
选项 3:准确写入时间
在您的客户端中,您可以在添加或删除文档的同时更新计数器,而不是使用 Cloud Functions。这意味着计数器也将是最新的,但您需要确保在添加或删除文档的任何地方都包含此逻辑。
与选项 2 一样,如果要超过每秒,则需要实施分布式计数器
Following Dan Answer: 你可以在你的数据库中有一个单独的计数器,并使用 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
如果你使用 AngulareFire2,你可以这样做(假设 private afs: AngularFirestore
被注入到你的构造函数中):
this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));
这里,values
是 myCollection
中所有项目的数组。您不需要元数据,因此可以直接使用 valueChanges()
方法。
聚合是可行的方法(firebase 函数看起来像是推荐的更新这些聚合的方法,因为客户端向您可能不希望公开的用户公开信息)https://firebase.google.com/docs/firestore/solutions/aggregation
另一种方式(不推荐)不适用于大型列表并且涉及下载整个列表:res.size 就像这个例子:
db.collection("logs")
.get()
.then((res) => console.log(res.size));
使用云函数计算 大型集合 的文档数量时要小心。如果您希望每个集合都有一个预先计算的计数器,那么使用 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 在所有情况下都将相同。不幸的是,您的数据库中必须有 "events" 个集合。
请检查我在另一个线程中找到的以下答案。你的计数应该是原子的。在这种情况下需要使用 FieldValue.increment() 函数。
使用事务来更新数据库写入成功侦听器中的计数。
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;
}
});
我创建了一个 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
获取新的写入批次
WriteBatch batch = db.batch();
为集合“NYC”添加新值
DocumentReference nycRef = db.collection("cities").document();
batch.set(nycRef, new City());
使用 Id 作为 Count 和 初始值 作为 维护文档]总计=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");
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'
//
// 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
}
}
云函数 HTTP 请求在 1 秒内完成,收集了 500 个文档,每个文档包含大量数据。性能并不惊人,但比不使用 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 功能详细说明服务器在部署时的位置。