Как получить количество документов в коллекции с Cloud Firestore

В Firestore, как я могу получить общее количество документов в коллекции?

Например, если у меня есть

/people
    /123456
        /name - 'John'
    /456789
        /name - 'Jane'

Я хочу запросить, сколько у меня людей и получить 2.

Я мог бы сделать запрос на /people, а затем получить длину возвращаемых результатов, но это кажется пустой тратой, особенно потому, что я буду делать это на больших наборах данных.

Ответ 1

В настоящее время у вас есть 3 варианта:

Вариант 1: Клиентская сторона

Это в основном подход, который вы упомянули. Выбрать все из коллекции и рассчитывать на стороне клиента. Это работает достаточно хорошо для небольших наборов данных, но, очевидно, не работает, если набор данных больше.

Вариант 2: лучшее время записи

При таком подходе вы можете использовать облачные функции для обновления счетчика для каждого добавления и удаления из коллекции.

Это хорошо работает для любого размера набора данных, если добавления/удаления происходят только со скоростью, меньшей или равной 1 в секунду. Это дает вам один документ для чтения, чтобы сразу подсчитать практически текущий счет.

Если вам нужно превышать 1 раз в секунду, вам необходимо реализовать распределенные счетчики в соответствии с нашей документацией.

Вариант 3: точное время записи

Вместо использования облачных функций в вашем клиенте вы можете обновить счетчик одновременно с добавлением или удалением документа. Это означает, что счетчик также будет текущим, но вам нужно обязательно включать эту логику в любое место, куда вы добавляете или удаляете документы.

Как и в варианте 2, вам нужно будет реализовать распределенные счетчики, если вы хотите увеличить в секунду

Ответ 2

Если вы используете AngulareFire2, вы можете сделать это (при условии, что в вашем конструкторе private afs: AngularFirestore):

this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));

Здесь values - это массив всех элементов в myCollection. Вам не нужны метаданные, поэтому вы можете valueChanges() использовать valueChanges().

Ответ 3

Агрегации - это путь (функции firebase выглядят как рекомендуемый способ обновления этих агрегаций, поскольку клиентская сторона предоставляет информацию пользователю, которого вы не хотите раскрывать) https://firebase.google.com/docs/firestore/solutions/aggregation

Другой способ (НЕ рекомендуется), который не подходит для больших списков и включает загрузку всего списка: res.size, как в этом примере:

db.collection('products').get().then(res => console.log(res.size))

Ответ 4

Будьте внимательны, считая количество документов для больших коллекций с облачной функцией. Это немного сложно с базой данных 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;
});

Причина в том, что каждый триггер облачного пожарного хранилища должен быть идемпотентным, как сказано в документации пожарного магазина: 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 в объекте контекста. Если функция была обработана много раз для одного и того же события, идентификатор события будет одинаковым во всех случаях. К сожалению, у вас должна быть коллекция "events" в вашей базе данных.

Ответ 5

После ответа 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