¿Es posible contar cuántos elementos tiene una colección usando la nueva base de datos Firebase, Cloud Firestore?
Si es así, ¿cómo hago eso?
¿Es posible contar cuántos elementos tiene una colección usando la nueva base de datos Firebase, Cloud Firestore?
Si es así, ¿cómo hago eso?
Respuestas:
Como con muchas preguntas, la respuesta es: depende .
Debe tener mucho cuidado al manejar grandes cantidades de datos en el front-end. Además de hacer que su front-end se sienta lento, Firestore también le cobra $ 0.60 por millón de lecturas que realiza.
Úselo con cuidado: la experiencia del usuario frontend puede verse afectada
Manejar esto en el front end debería estar bien siempre que no esté haciendo demasiada lógica con esta matriz devuelta.
db.collection('...').get().then(snap => {
size = snap.size // will return the collection size
});
Úselo con cuidado: las invocaciones de lectura de Firestore pueden costar mucho
Manejar esto en el front end no es factible ya que tiene demasiado potencial para ralentizar el sistema de los usuarios. Deberíamos manejar este lado del servidor lógico y solo devolver el tamaño.
El inconveniente de este método es que todavía está invocando lecturas de almacén de incendios (igual al tamaño de su colección), que a la larga puede terminar costándole más de lo esperado.
Función de la nube:
...
db.collection('...').get().then(snap => {
res.status(200).send({length: snap.size});
});
Interfaz:
yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
size = snap.length // will return the collection size
})
La solución más escalable
FieldValue.increment ()
A partir de abril de 2019, Firestore ahora permite incrementar los contadores, de forma completamente atómica, y sin leer los datos antes . Esto garantiza que tengamos los valores de contador correctos incluso cuando se actualizan desde múltiples fuentes simultáneamente (previamente resueltas mediante transacciones), al tiempo que se reduce la cantidad de lecturas de bases de datos que realizamos.
Al escuchar cualquier documento eliminado o creado, podemos agregar o eliminar de un campo de recuento que se encuentra en la base de datos.
Vea los documentos de la tienda de incendios: contadores distribuidos o eche un vistazo a la agregación de datos por Jeff Delaney. Sus guías son realmente fantásticas para cualquiera que use AngularFire, pero sus lecciones también deberían trasladarse a otros marcos.
Función de la nube:
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;
});
Ahora en la interfaz puede consultar este campo numberOfDocs para obtener el tamaño de la colección.
firestore.runTransaction { ... }
bloque. Esto soluciona problemas de concurrencia con el acceso numberOfDocs
.
La forma más sencilla de hacerlo es leer el tamaño de una "consulta instantánea".
db.collection("cities").get().then(function(querySnapshot) {
console.log(querySnapshot.size);
});
También puede leer la longitud de la matriz de documentos dentro de "querySnapshot".
querySnapshot.docs.length;
O si un "querySnapshot" está vacío leyendo el valor vacío, que devolverá un valor booleano.
querySnapshot.empty;
db.collection.count()
. Pensando en dejarlos solo por esto
Hasta donde sé, no hay una solución integrada para esto y solo es posible en el nodo sdk en este momento. Si tienes un
db.collection('someCollection')
puedes usar
.select([fields])
para definir qué campo desea seleccionar. Si hace una selección vacía (), solo obtendrá una matriz de referencias de documentos.
ejemplo:
db.collection('someCollection').select().get().then(
(snapshot) => console.log(snapshot.docs.length)
);
¡Esta solución es solo una optimización para el peor de los casos de descarga de todos los documentos y no se escala en grandes colecciones!
También eche un vistazo a esto:
Cómo obtener un recuento de la cantidad de documentos en una colección con Cloud Firestore
select(['_id'])
es más rápido queselect()
Tenga cuidado al contar el número de documentos para grandes colecciones . Es un poco complejo con la base de datos de firestore si desea tener un contador precalculado para cada colección.
Un código como este no funciona en este caso:
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;
});
La razón es porque cada desencadenante del almacén de incendios en la nube debe ser idempotente, como dice la documentación del almacén de incendios: https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees
Por lo tanto, para evitar múltiples ejecuciones de su código, debe administrar eventos y transacciones. Esta es mi forma particular de manejar grandes contadores de colección:
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;
});
Use los casos aquí:
/**
* 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'));
Como puede ver, la clave para evitar la ejecución múltiple es la propiedad llamada eventId en el objeto de contexto. Si la función se ha manejado muchas veces para el mismo evento, la identificación del evento será la misma en todos los casos. Desafortunadamente, debe tener una colección de "eventos" en su base de datos.
context.eventId
siempre será igual en múltiples invocaciones del mismo activador? En mis pruebas parece ser consistente, pero no puedo encontrar ninguna documentación "oficial" que indique esto.
En 2020, esto todavía no está disponible en Firebase SDK; sin embargo, está disponible en Firebase Extensions (Beta); sin embargo, es bastante complejo de configurar y usar ...
Un enfoque razonable
Ayudantes ... (crear / eliminar parece redundante pero es más barato que 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;
}
Ganchos exportados de 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());
});
En acción
Se rastreará la colección raíz del edificio y todas las subcolecciones .
Aquí debajo de la /counters/
ruta raíz
¡Ahora los recuentos de colecciones se actualizarán automáticamente y eventualmente! Si necesita un recuento, simplemente use la ruta de recopilación y agregue el prefijo counters
.
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const collectionCount = await db
.doc('counters/' + collectionPath)
.get()
.then(snap => snap.get('count'));
Estoy de acuerdo con @Matthew, costará mucho si realiza dicha consulta.
[CONSEJO PARA DESARROLLADORES ANTES DE COMENZAR SUS PROYECTOS]
Como hemos previsto esta situación al principio, podemos hacer una colección, a saber, contadores con un documento para almacenar todos los contadores en un campo con tipo number
.
Por ejemplo:
Para cada operación CRUD en la colección, actualice el documento de contador:
La próxima vez, cuando desee obtener el número de recopilación, solo necesita consultar / señalar el campo del documento. [1 operación de lectura]
Además, puede almacenar el nombre de las colecciones en una matriz, pero esto será complicado, la condición de la matriz en firebase se muestra a continuación:
// 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'}
Entonces, si no va a eliminar la colección, en realidad puede usar la matriz para almacenar la lista de nombres de colecciones en lugar de consultar toda la colección cada vez.
¡Espero eso ayude!
No, en este momento no hay soporte incorporado para consultas de agregación. Sin embargo, hay algunas cosas que podrías hacer.
El primero está documentado aquí . Puede usar transacciones o funciones en la nube para mantener información agregada:
Este ejemplo muestra cómo usar una función para realizar un seguimiento del número de calificaciones en una subcolección, así como la calificación promedio.
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
});
});
});
});
La solución que mencionó jbb también es útil si solo desea contar documentos con poca frecuencia. Asegúrese de usar la select()
declaración para evitar descargar todos los documentos (eso es mucho ancho de banda cuando solo necesita un recuento). select()
por ahora solo está disponible en los SDK del servidor, por lo que la solución no funcionará en una aplicación móvil.
No hay una opción directa disponible. Usted cant't haces db.collection("CollectionName").count()
. A continuación se muestran las dos formas en que puede encontrar el recuento de la cantidad de documentos dentro de una colección.
db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})
Al usar el código anterior, las lecturas de sus documentos serán iguales al tamaño de los documentos dentro de una colección y esa es la razón por la cual uno debe evitar el uso de la solución anterior.
db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})
Arriba creamos un documento con recuentos de nombres para almacenar toda la información de recuento. Puede actualizar el documento de recuento de la siguiente manera: -
wrt price (Document Read = 1) y recuperación rápida de datos, la solución anterior es buena.
Incremente un contador usando 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),
})
);
En este ejemplo, incrementamos un instanceCount
campo en el proyecto cada vez que se agrega un documento a la instances
subcolección. Si el campo aún no existe, se creará e incrementará a 1.
El incremento es transaccional internamente, pero debe usar un contador distribuido si necesita incrementar con más frecuencia que cada 1 segundo.
A menudo es preferible implementarlo onCreate
y en onDelete
lugar de hacerlo onWrite
, solicitará onWrite
actualizaciones, lo que significa que está gastando más dinero en invocaciones de funciones innecesarias (si actualiza los documentos de su colección).
Una solución alternativa es:
escribe un contador en un documento de Firebase, que incrementa dentro de una transacción cada vez que crea una nueva entrada
Almacena el recuento en un campo de su nueva entrada (es decir: posición: 4).
Luego crea un índice en ese campo (posición DESC).
Puede hacer un salto + límite con una consulta. Donde ("posición", "<" x) .OrderBy ("posición", DESC)
¡Espero que esto ayude!
Creé una función universal usando todas estas ideas para manejar todas las situaciones de contador (excepto las consultas).
La única excepción sería cuando hacer tantas escrituras por segundo, te ralentiza. Un ejemplo sería Me gusta en una publicación de tendencias. Es excesivo en una publicación de blog, por ejemplo, y le costará más. Sugiero crear una función separada en ese caso usando fragmentos: 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);
});;
}
}
Esto maneja eventos, incrementos y transacciones. Lo bueno de esto es que si no está seguro de la precisión de un documento (probablemente mientras esté en versión beta), puede eliminar el contador para que se agregue automáticamente en el siguiente activador. Sí, esto cuesta, así que no lo elimine de otra manera.
El mismo tipo de cosas para obtener el recuento:
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');
Además, puede crear un trabajo cron (función programada) para eliminar eventos antiguos y ahorrar dinero en el almacenamiento de la base de datos. Necesita al menos un plan de incendio, y puede haber alguna configuración más. Puede ejecutarlo todos los domingos a las 11 p.m., por ejemplo. https://firebase.google.com/docs/functions/schedule-functions
Esto no se ha probado , pero debería funcionar con algunos ajustes:
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;
});
Y por último, no olvide proteger las colecciones en firestore.rules :
match /_counters/{document} {
allow read;
allow write: if false;
}
match /_events/{document} {
allow read, write: if false;
}
Actualización: consultas
Agregando a mi otra respuesta si también desea automatizar el conteo de consultas, puede usar este código modificado en su función en la nube:
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();
Lo que actualizará automáticamente el postsCount en el userDocument. Podría agregar fácilmente otro a muchos recuentos de esta manera. Esto solo te da ideas de cómo puedes automatizar las cosas. También te di otra forma de eliminar los eventos. Debe leer cada fecha para eliminarla, por lo que realmente no le ahorrará eliminarlas más tarde, solo hace que la función sea más lenta.
/**
* 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;
}
También debo advertirle que las funciones universales se ejecutarán en cada período de llamadas onWrite. Puede ser más barato ejecutar solo la función en instancias onCreate y onDelete de sus colecciones específicas. Al igual que la base de datos noSQL que estamos usando, el código y los datos repetidos pueden ahorrarle dinero.
Me tomó un tiempo hacer que esto funcionara en base a algunas de las respuestas anteriores, así que pensé en compartirlo para que otros lo usen. Espero que sea útil.
'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;
});
He intentado mucho con diferentes enfoques. Y finalmente, mejoro uno de los métodos. Primero debe crear una colección separada y guardar allí todos los eventos. En segundo lugar, debe crear una nueva lambda que se activará con el tiempo. Esta lambda contará eventos en la colección de eventos y borrará documentos de eventos. Detalles del código en el artículo. https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca
Esta consulta dará como resultado el recuento de documentos.
this.db.collection(doc).get().subscribe((data) => {
count = data.docs.length;
});
console.log(count)
Esto usa el conteo para crear una identificación numérica única. En mi uso, no volveré a disminuir nunca , incluso cuando document
se elimine la identificación necesaria.
Sobre una collection
creación que necesita un valor numérico único
appData
con un documento, set
con .doc
identificaciónonly
uniqueNumericIDAmount
a 0 en elfirebase firestore console
doc.data().uniqueNumericIDAmount + 1
como la identificación numérica únicaappData
colección uniqueNumericIDAmount
confirebase.firestore.FieldValue.increment(1)
firebase
.firestore()
.collection("appData")
.doc("only")
.get()
.then(doc => {
var foo = doc.data();
foo.id = doc.id;
// your collection that needs a unique ID
firebase
.firestore()
.collection("uniqueNumericIDs")
.doc(user.uid)// user id in my case
.set({// I use this in login, so this document doesn't
// exist yet, otherwise use update instead of set
phone: this.state.phone,// whatever else you need
uniqueNumericID: foo.uniqueNumericIDAmount + 1
})
.then(() => {
// upon success of new ID, increment uniqueNumericIDAmount
firebase
.firestore()
.collection("appData")
.doc("only")
.update({
uniqueNumericIDAmount: firebase.firestore.FieldValue.increment(
1
)
})
.catch(err => {
console.log(err);
});
})
.catch(err => {
console.log(err);
});
});
firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
int Counter = documentSnapshots.size();
}
});