É possível contar quantos itens uma coleção possui usando o novo banco de dados Firebase, o Cloud Firestore?
Se sim, como faço isso?
É possível contar quantos itens uma coleção possui usando o novo banco de dados Firebase, o Cloud Firestore?
Se sim, como faço isso?
Respostas:
Como em muitas perguntas, a resposta é - depende .
Você deve ter muito cuidado ao lidar com grandes quantidades de dados no front-end. Além de tornar seu front end lento, o Firestore também cobra US $ 0,60 por milhão de leituras que você faz.
Use com cuidado - a experiência do usuário do front-end pode sofrer um impacto
Lidar com isso no front-end deve ser bom, desde que você não faça muita lógica com essa matriz retornada.
db.collection('...').get().then(snap => {
size = snap.size // will return the collection size
});
Use com cuidado - as chamadas de leitura do Firestore podem custar muito
Lidar com isso no front end não é viável, pois tem muito potencial para desacelerar o sistema dos usuários. Devemos lidar com esse lado do servidor lógico e retornar apenas o tamanho.
A desvantagem desse método é que você ainda está invocando leituras do firestore (iguais ao tamanho da sua coleção), que a longo prazo podem acabar custando mais do que o esperado.
Função de nuvem:
...
db.collection('...').get().then(snap => {
res.status(200).send({length: snap.size});
});
A parte dianteira:
yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
size = snap.length // will return the collection size
})
Solução mais escalável
FieldValue.increment ()
A partir de abril de 2019, o Firestore agora permite incrementar contadores, completamente atomicamente e sem ler os dados antes . Isso garante que temos valores corretos de contador, mesmo quando atualizamos de várias fontes simultaneamente (resolvidas anteriormente usando transações), além de reduzir o número de leituras de banco de dados que realizamos.
Ao ouvir qualquer documento excluído ou criado, podemos adicionar ou remover de um campo de contagem localizado no banco de dados.
Consulte os documentos do firestore - Contadores distribuídos ou dê uma olhada na agregação de dados por Jeff Delaney. Seus guias são realmente fantásticos para quem usa o AngularFire, mas suas lições devem passar para outras estruturas também.
Função de nuvem:
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;
});
Agora, no front-end, você pode consultar este campo numberOfDocs para obter o tamanho da coleção.
firestore.runTransaction { ... }
bloco. Isso corrige problemas de concorrência com o acesso numberOfDocs
.
A maneira mais simples de fazer isso é ler o tamanho de um "querySnapshot".
db.collection("cities").get().then(function(querySnapshot) {
console.log(querySnapshot.size);
});
Você também pode ler o comprimento da matriz de documentos em "querySnapshot".
querySnapshot.docs.length;
Ou se um "querySnapshot" estiver vazio lendo o valor vazio, que retornará um valor booleano.
querySnapshot.empty;
db.collection.count()
. Pensando em largá-los apenas por isso #
Até onde eu sei, não existe uma solução incorporada para isso e só é possível no nó sdk no momento. Se você tem um
db.collection('someCollection')
você pode usar
.select([fields])
para definir qual campo você deseja selecionar. Se você selecionar vazio (), obterá apenas uma série de referências a documentos.
exemplo:
db.collection('someCollection').select().get().then(
(snapshot) => console.log(snapshot.docs.length)
);
Esta solução é apenas uma otimização para o pior caso de baixar todos os documentos e não é escalável em grandes coleções!
Veja também:
Como obter uma contagem do número de documentos em uma coleção com o Cloud Firestore
select(['_id'])
é mais rápido do queselect()
Tenha cuidado ao contar o número de documentos para grandes coleções . É um pouco complexo com o banco de dados do firestore se você deseja ter um contador pré-calculado para cada coleção.
Código como este não funciona neste 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;
});
O motivo é que cada acionador do firestore na nuvem precisa ser idempotente, como a documentação do firestore diz: https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees
Portanto, para impedir várias execuções do seu código, você precisa gerenciar com eventos e transações. Esta é minha maneira particular de lidar com grandes contadores de coleções:
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;
});
Casos de uso aqui:
/**
* 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 você pode ver, a chave para impedir a execução múltipla é a propriedade chamada eventId no objeto de contexto. Se a função tiver sido manipulada várias vezes para o mesmo evento, o ID do evento será o mesmo em todos os casos. Infelizmente, você deve ter a coleção "events" em seu banco de dados.
context.eventId
sempre será o mesmo em várias invocações do mesmo gatilho? Nos meus testes, parece ser consistente, mas não consigo encontrar nenhuma documentação "oficial" afirmando isso.
Em 2020, isso ainda não está disponível no Firebase SDK, no entanto, está disponível no Firebase Extensions (Beta), no entanto, é bastante complexo configurar e usar ...
Uma abordagem razoável
Ajudantes ... (criar / excluir parece redundante, mas é mais barato que o 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 de Firestore exportados
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());
});
Em ação
A coleção raiz do edifício e todas as sub-coleções serão rastreadas.
Aqui sob o /counters/
caminho da raiz
Agora a contagem de coleções será atualizada automaticamente e, eventualmente! Se você precisar de uma contagem, basta usar o caminho da coleção e prefixá-lo counters
.
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const collectionCount = await db
.doc('counters/' + collectionPath)
.get()
.then(snap => snap.get('count'));
Concordo com @ Matthew, vai custar muito se você realizar essa consulta.
[CONSELHOS PARA DESENVOLVEDORES ANTES DE INICIAR SEUS PROJETOS]
Como previmos essa situação no início, podemos realmente fazer uma coleção, ou seja, contadores com um documento para armazenar todos os contadores em um campo com o tipo number
.
Por exemplo:
Para cada operação CRUD na coleção, atualize o documento do contador:
Da próxima vez, quando você quiser obter o número de coleção, basta consultar / apontar para o campo do documento. [1 operação de leitura]
Além disso, você pode armazenar o nome das coleções em uma matriz, mas isso será complicado, a condição da matriz na firebase é mostrada abaixo:
// 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'}
Portanto, se você não excluir a coleção, poderá usar a matriz para armazenar a lista de nomes de coleções em vez de consultar toda a coleção todas as vezes.
Espero que ajude!
Não, não há suporte interno para consultas de agregação no momento. No entanto, existem algumas coisas que você poderia fazer.
O primeiro está documentado aqui . Você pode usar transações ou funções da nuvem para manter as informações agregadas:
Este exemplo mostra como usar uma função para acompanhar o número de classificações em uma subcoleção, bem como a classificação média.
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
});
});
});
});
A solução mencionada pela jbb também é útil se você quiser contar documentos com pouca frequência. Certifique-se de usar a select()
instrução para evitar o download de todos os documentos (é muita largura de banda quando você precisa apenas de uma contagem). select()
está disponível apenas nos SDKs do servidor por enquanto, para que a solução não funcione em um aplicativo móvel.
Não há opção direta disponível. Você não pode fazer db.collection("CollectionName").count()
. Abaixo estão as duas maneiras pelas quais você pode encontrar a contagem do número de documentos em uma coleção.
db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})
Ao usar o código acima, as leituras de seu documento serão iguais ao tamanho dos documentos em uma coleção e é por isso que é necessário evitar o uso da solução acima.
db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})
Acima, criamos um documento com contagem de nomes para armazenar todas as informações de contagem. Você pode atualizar o documento de contagem da seguinte maneira: -
preço wrt (leitura de documento = 1) e recuperação rápida de dados, a solução acima é boa.
Incremente um 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),
})
);
Neste exemplo, incrementamos um instanceCount
campo no projeto cada vez que um documento é adicionado à instances
sub-coleção. Se o campo ainda não existir, ele será criado e incrementado para 1.
A incrementação é transacional internamente, mas você deve usar um contador distribuído se precisar incrementar com mais frequência do que a cada 1 segundo.
Geralmente, é preferível implementar onCreate
e, em onDelete
vez de onWrite
solicitar onWrite
atualizações, o que significa que você está gastando mais dinheiro em invocações de funções desnecessárias (se você atualizar os documentos em sua coleção).
Uma solução alternativa é:
escreva um contador em um documento da base de firmas, que você incrementa em uma transação toda vez que cria uma nova entrada
Você armazena a contagem em um campo da sua nova entrada (ou seja: posição: 4).
Em seguida, você cria um índice nesse campo (posição DESC).
Você pode fazer um salto + limite com uma consulta.Where ("position", "<" x) .OrderBy ("position", DESC)
Espero que isto ajude!
Criei uma função universal usando todas essas idéias para lidar com todas as situações de contador (exceto consultas).
A única exceção seria ao fazer tantas gravações por segundo, que você fica mais lento. Um exemplo seria curtir em uma postagem de tendência. É um exagero em uma postagem de blog, por exemplo, e vai custar mais. Sugiro que você crie uma função separada usando shards: 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);
});;
}
}
Isso lida com eventos, incrementos e transações. A vantagem disso é que, se você não tiver certeza da precisão de um documento (provavelmente ainda em beta), poderá excluir o contador para que ele os adicione automaticamente no próximo gatilho. Sim, isso custa, portanto, não o exclua.
O mesmo tipo de coisa para obter a contagem:
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');
Além disso, convém criar um trabalho cron (função agendada) para remover eventos antigos e economizar dinheiro no armazenamento do banco de dados. Você precisa de pelo menos um plano de incêndio, e pode haver mais configurações. Você pode executá-lo todos os domingos às 23h, por exemplo. https://firebase.google.com/docs/functions/schedule-functions
Isso não foi testado , mas deve funcionar com alguns 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;
});
E por último, não se esqueça de proteger as coleções no firestore.rules :
match /_counters/{document} {
allow read;
allow write: if false;
}
match /_events/{document} {
allow read, write: if false;
}
Atualização: consultas
Adicionando à minha outra resposta, se você deseja automatizar as contagens de consultas, também pode usar este código modificado na sua função de nuvem:
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();
O qual atualizará automaticamente o postsCount no userDocument. Você pode facilmente adicionar outro a muitas contagens dessa maneira. Isso fornece idéias de como você pode automatizar as coisas. Também lhe dei outra maneira de excluir os eventos. Você precisa ler cada data para excluí-la, para que não seja realmente necessário excluí-las mais tarde, apenas torna a função mais 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;
}
Também devo avisar que funções universais serão executadas a cada período de chamada onWrite. Pode ser mais barato executar apenas a função nas instâncias onCreate e onDelete de suas coleções específicas. Como o banco de dados noSQL que estamos usando, códigos e dados repetidos podem economizar seu dinheiro.
Levei um tempo para que isso funcionasse com base em algumas das respostas acima, então pensei em compartilhar para outras pessoas usarem. Espero que seja ú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;
});
Eu tentei muito com diferentes abordagens. E, finalmente, aperfeiçoo um dos métodos. Primeiro, você precisa criar uma coleção separada e salvar todos os eventos. Segundo, você precisa criar um novo lambda para ser acionado pelo tempo. Este lambda Contará eventos na coleção de eventos e limpará os documentos do evento. Detalhes do código no artigo. https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca
Esta consulta resultará na contagem de documentos.
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();
}
});