324 votes

Nombre de collections Cloud Firestore

Est-il possible de compter le nombre d'éléments d'une collection en utilisant la nouvelle base de données Firebase, Cloud Firestore ?

Si oui, comment dois-je m'y prendre ?

11 votes

0 votes

Je pense que cet article pourrait également vous intéresser, Comment compter le nombre de documents dans une collection Firestore ? .

365voto

Matthew Mullin Points 2160

Comme pour beaucoup de questions, la réponse est - Cela dépend .

Vous devez être très prudent lorsque vous manipulez de grandes quantités de données sur le front-end. En plus de rendre votre front-end plus lent, Firestore a également vous facture 0,60 $ par million de lectures que vous faites.


Petite collection (moins de 100 documents)

A utiliser avec précaution - l'expérience de l'utilisateur frontal peut en souffrir.

La gestion de ces données en amont devrait être correcte tant que vous ne faites pas trop de logique avec le tableau retourné.

db.collection('...').get().then(snap => {
  size = snap.size // will return the collection size
});

Collection moyenne (100 à 1000 documents)

Utilisez avec précaution - les invocations de lecture de Firestore peuvent coûter cher.

Il n'est pas possible de gérer cela en amont, car cela risque trop de ralentir le système des utilisateurs. Nous devrions gérer cette logique côté serveur et ne renvoyer que la taille.

L'inconvénient de cette méthode est que vous invoquez toujours des lectures Firestore (égales à la taille de votre collection), ce qui, à long terme, peut vous coûter plus cher que prévu.

Fonction cloud :

db.collection('...').get().then(snap => {
  res.status(200).send({length: snap.size});
});

Front End :

yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
   size = snap.length // will return the collection size
})

Grande collection (1000+ documents)

La solution la plus évolutive


FieldValue.incrément()

Depuis avril 2019, Firestore permet désormais d'incrémenter les compteurs, de manière totalement atomique et sans lire les données au préalable. . Cela nous permet d'avoir des valeurs de compteur correctes, même en cas de mise à jour simultanée à partir de plusieurs sources (problème précédemment résolu à l'aide de transactions), tout en réduisant le nombre de lectures de la base de données que nous effectuons.


En écoutant les suppressions et les créations de documents, nous pouvons ajouter ou supprimer un champ de comptage qui se trouve dans la base de données.

Voir la documentation de firestore - Compteurs distribués Ou bien, jetez un coup d'œil à Agrégation de données par Jeff Delaney. Ses guides sont vraiment fantastiques pour quiconque utilise AngularFire, mais ses leçons devraient également s'appliquer aux autres frameworks.

Fonction cloud :

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;
  });

Maintenant, sur le front-end, vous pouvez simplement interroger ce champ numberOfDocs pour obtenir la taille de la collection.

27 votes

Une solution idéale pour les grandes collections ! Je voudrais juste ajouter que les implémenteurs devraient envelopper la lecture et l'écriture dans un fichier de type firestore.runTransaction { ... } bloc. Cela résout les problèmes de concurrence avec l'accès à numberOfDocs .

3 votes

Ces méthodes utilisent un recomptage du nombre d'enregistrements. Si vous utilisez un compteur et que vous l'incrémentez à l'aide d'une transaction, n'obtiendriez-vous pas le même résultat sans le coût supplémentaire et la nécessité d'une fonction en nuage ?

33 votes

La solution pour les grandes collections n'est pas idempotente et ne fonctionne pas à n'importe quelle échelle. Les déclencheurs de documents Firestore sont garantis pour s'exécuter au moins une fois, mais peuvent s'exécuter plusieurs fois. Dans ce cas, même le maintien de la mise à jour à l'intérieur d'une transaction peut s'exécuter plus d'une fois, ce qui vous donnera un faux chiffre. Lorsque j'ai essayé cela, j'ai rencontré des problèmes avec moins d'une douzaine de créations de documents à la fois.

41voto

Ompel Points 282

La façon la plus simple de le faire est de lire la taille d'un "querySnapshot".

db.collection("cities").get().then(function(querySnapshot) {      
    console.log(querySnapshot.size); 
});

Vous pouvez également lire la longueur du tableau de docs dans "querySnapshot".

querySnapshot.docs.length;

Ou si un "querySnapshot" est vide en lisant la valeur vide, ce qui renverra une valeur booléenne.

querySnapshot.empty;

130 votes

Sachez que chaque document "coûte" une lecture. Ainsi, si vous comptez 100 éléments dans une collection de cette manière, vous êtes facturé pour 100 lectures !

0 votes

C'est exact, mais il n'y a pas d'autre moyen de résumer le nombre de documents dans une collection. Et si vous avez déjà extrait la collection, la lecture du tableau "docs" ne nécessitera pas d'autres extractions et ne "coûtera" donc pas plus de lectures.

9 votes

Cela permet de lire tous les documents en mémoire ! Bonne chance avec ça pour les grands ensembles de données...

34voto

jbb Points 160

Pour autant que je sache, il n'y a pas de solution intégrée pour cela et c'est seulement possible dans le sdk node pour le moment. Si vous avez un

db.collection('someCollection')

vous pouvez utiliser

.select([fields])

pour définir le champ que vous voulez sélectionner. Si vous effectuez un select() vide, vous obtiendrez simplement un tableau de références de documents.

exemple :

db.collection('someCollection').select().get().then( (snapshot) => console.log(snapshot.docs.length) );

Cette solution n'est qu'une optimisation pour le pire cas de téléchargement de tous les documents et ne s'adapte pas aux grandes collections !

Jetez également un coup d'œil à ceci :
Comment obtenir le nombre de documents dans une collection avec Cloud Firestore ?

1 votes

D'après mon expérience, select(['_id']) est plus rapide que select()

21voto

Ferran Verdés Points 136

Faites attention en comptant le nombre de documents pour grandes collections . C'est un peu complexe avec la base de données firestore si vous voulez avoir un compteur précalculé pour chaque collection.

Un code comme celui-ci ne fonctionne pas dans ce cas :

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 raison en est que chaque déclencheur firestore en nuage doit être idempotent, comme le dit la documentation firestore : https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees

Solution

Ainsi, afin d'empêcher les exécutions multiples de votre code, vous devez gérer les événements et les transactions. C'est ma façon particulière de gérer les grands compteurs de collection :

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;
    });

Cas d'utilisation ici :

/**
 * 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'));

Comme vous pouvez le voir, la clé pour empêcher l'exécution multiple est la propriété appelée eventId dans l'objet contexte. Si la fonction a été traitée plusieurs fois pour le même événement, l'identifiant de l'événement sera le même dans tous les cas. Malheureusement, vous devez avoir la collection "events" dans votre base de données.

2 votes

Ils formulent cela comme si ce comportement serait corrigé dans la version 1.0. Les fonctions Amazon AWS souffrent du même problème. Quelque chose d'aussi simple que de compter les champs devient complexe et coûteux.

0 votes

Je vais essayer ceci maintenant car cela semble être une meilleure solution. Est-ce que tu reviens en arrière et purges ta collection d'événements ? Je pensais simplement ajouter un champ de date et purger les événements plus anciens qu'un jour ou quelque chose comme ça pour garder un petit ensemble de données (peut-être plus de 1 million d'événements par jour). A moins qu'il n'y ait un moyen facile de le faire dans FS... je n'utilise FS que depuis quelques mois.

1 votes

Pouvons-nous vérifier que context.eventId sera toujours le même lors de multiples invocations du même déclencheur ? Dans mes tests, cela semble cohérent, mais je ne parviens pas à trouver de documentation "officielle" à ce sujet.

6voto

hatboysam Points 1191

Non, il n'y a pas de support intégré pour les requêtes d'agrégation pour le moment. Cependant, il y a plusieurs choses que vous pouvez faire.

Le premier est documenté ici . Vous pouvez utiliser des transactions ou des fonctions cloud pour gérer des informations agrégées :

Cet exemple montre comment utiliser une fonction pour suivre le nombre d'évaluations dans une sous-collection, ainsi que l'évaluation moyenne.

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 solution mentionnée par jbb est également utile si vous ne voulez compter les documents que rarement. Veillez à utiliser la fonction select() pour éviter de télécharger l'intégralité de chaque document (cela représente beaucoup de bande passante alors que vous n'avez besoin que d'un nombre limité). select() n'est pour l'instant disponible que dans les kits SDK pour serveurs. Cette solution ne fonctionnera donc pas dans une application mobile.

1 votes

Cette solution n'est pas idempotente, donc tout déclencheur qui se déclenche plus d'une fois va fausser le nombre d'évaluations et la moyenne.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X