Как подробно описано в документации Cloud Firestore, данные в Firestore хранятся в документах, которые организованы в коллекции. Документы могут содержать вложенные коллекции, которые, в свою очередь, могут содержать документы.

В документации также указано, что:

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

Обычно, как архитектор базы данных Firestore, при разработке модели данных вы определяете идентификаторы различных вложенных коллекций документа.

Например, для приложения «Отзывы о ресторанах» вы можете создать reviews вложенную коллекцию для каждого restaurant документа¹.

А с помощью JavaScript SDK вы должны запросить все review документы для определенного restaurant документа с id равным 123 следующим образом:

db.collection("restaurants").doc("123").collection("reviews").get()
.then(querySnapshot => {
    querySnapshot.forEach(doc => {
        console.log(doc.id, " => ", doc.data());
    });
});

Однако в некоторых конкретных случаях может случиться так, что вложенные коллекции создаются динамически пользователями во время использования приложения.

Например, представьте себе приложение CRM, в котором пользователи создают отчеты о посещениях клиентов. У каждого пользователя приложения есть пользовательский документ в базе данных Firestore. Для каждого посещения клиента соответствующий отчет о посещении сохраняется в подколлекции документа пользователя с идентификатором клиента².

Рисунок ниже иллюстрирует эту модель данных, показывая user1 документ с двумя вложенными коллекциями, соответствующими двум клиентам: CUST_1234 и CUST_6541.

Каждая из этих двух коллекций была создана, когда пользователь1 впервые создал документ в коллекции (т. Е. Создал отчет о посещениях для соответствующего клиента).

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

Возникает вопрос: как получить этот список вложенных коллекций из приложения?

Если вы посмотрите документацию Firestore, вы увидите, что:

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

К счастью, есть несколько возможных обходных путей для получения списка (под) коллекций из веб-приложения или мобильного приложения. Давайте подробно рассмотрим парочку из них!

Подход №1: сохраните список в специальном поле родительского документа.

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

Как заполнить это поле?
Просто используйте arrayUnion(), как описано в документации, здесь и здесь, и как показано в примере ниже (JavaScript SDK):

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

Альтернативой может быть чтение родительского документа перед записью в коллекцию, чтобы проверить, присутствует ли уже идентификатор подколлекции в массиве. Но, помимо создания дополнительного «обхода» к базе данных, это потребует затрат на чтение одного документа.

Еще одним недостатком этого подхода является то, что вам нужно управлять случаем, когда все документы подколлекции удаляются. Это сложнее, чем кажется: вам либо нужно вести счетчик документов для каждой подколлекции, либо, что еще хуже, нужно запрашивать всю подколлекцию для подсчета документов (используя метод get() и свойство size)…

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

К счастью (опять же!), Есть еще один возможный подход.

Подход № 2: использование облачной функции

Хотя с помощью клиентских библиотек для мобильных устройств и веб-приложений невозможно получить список вложенных коллекций документа, это возможно с помощью клиентских библиотек сервера Cloud Firestore.

Следовательно, можно использовать Клиентский API Cloud Firestore Node.js для написания Облачной функции, которая перечисляет вложенные коллекции документа.

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

Вот код облачной функции:

Как видите, это довольно просто. Давайте подробно рассмотрим каждую строчку этого кода⁵.

  1. Мы импортируем модули Cloud Functions и Admin SDK с помощью операторов Node require.
  2. Мы инициализируем экземпляр admin приложения.
  3. Мы используем functions.https.onCall() для создания вызываемой облачной функции. Этот метод принимает два параметра: data и context (необязательно).
  4. Мы используем параметр data, чтобы получить docPath, значение пути к документу Firestore (разделенное косой чертой). Это значение передается от клиента, вызывающего облачную функцию (см. Ниже).
  5. Затем мы вызываем асинхронный listCollections() метод для DocumentReference, созданного с помощью docPath (т.е. admin.firestore().doc(docPath)). Метод listCollections() возвращает обещание, которое разрешается с массивом CollectionReferences. Обратите внимание, что listCollections() выбирает только вложенные коллекции, которые являются прямыми дочерними элементами документа.
  6. Затем мы используем метод map() для создания нового массива с id вложенными коллекциями. Свойство id объекта CollectionReferences содержит последний элемент пути в указанной коллекции.
  7. Наконец, мы отправляем обратно клиенту объект JavaScript, который можно закодировать в формате JSON и который содержит массив подколлекции ids.

Вызов этой облачной функции из клиента еще проще. Вот код для JavaScript SDK, чтобы вызвать его из веб-приложения:

Вот как это работает:

  1. Мы объявляем HttpsCallable, который является ссылкой на вызываемый HTTP-триггер в облачных функциях. При вызове он возвращает обещание, которое разрешается с помощью HttpsCallableResult, которое, в свою очередь, обертывает единственный результат.
  2. Затем мы вызываем его, передавая в объект путь к документу Firestore (collectionId/documentId).
  3. Наконец, мы используем свойство data элемента HttpsCallableResult, чтобы получить массив collections, возвращаемый облачной функцией getSubCollection.

Затем мы можем делать с этим массивом все, что захотим: записывать его в консоль или перебирать его и печатать каждый идентификатор коллекции, или даже перебирать его и извлекать для каждой коллекции (т.е. для каждого элемента массива) все документы коллекции и др.

Обратите внимание, что он работает аналогично с SDK для Android и iOS (см. документацию), а также с FlutterFire (см. документацию).

Вот и все! Теперь у нас есть способ получить все подколлекции данного документа Firestore от клиента (Интернет, Android или iOS). И массив подколлекций сразу же адаптируется, если подколлекция добавляется или удаляется.

Кроме того, это решение не требует дополнительного чтения или записи документов.

Это стоит всего лишь одного вызова облачной функции, что не дорого (0,40 доллара США за миллион вызовов). Более того, Firebase предлагает щедрый уровень бесплатного пользования, состоящий из 2 000 000 вызовов, 400 000 ГБ-сек, 200 000 ЦП-секунд и 5 ГБ исходящего интернет-трафика каждый месяц. Подробнее о точных расходах см. Здесь и здесь.

В следующем репозитории на github вы найдете код небольшого проекта Firebase, который включает:

  • Код облачной функции, представленный выше;
  • Простая HTML-страница, демонстрирующая, как она работает в веб-клиенте.

Разверните его в одном из своих проектов Firebase (см. Файл readme) и откройте корневой URL-адрес проекта (https: // ‹your-project-id› .firebaseapp.com) в предпочитаемом вами браузере.

Просто введите путь к документу в специальном поле и нажмите кнопку «Получить вложенные коллекции»:

  • Если в документе есть одна или несколько вложенных коллекций, на странице будут отображаться их идентификаторы.
  • Если документ по пути не существует или у него нет вложенной коллекции, облачная функция вернет пустой массив.

Если у вас есть какие-либо вопросы или предложения, оставьте комментарий ниже.

#Лучше вместе

[1] На самом деле это один из примеров, использованных в официальном видео Firestore https://youtu.be/v_hR4K4auoQ

[2] Конечно, это лишь одна из возможностей моделирования данного конкретного случая. Другие подходы могут быть абсолютно верными! Идея состоит в том, чтобы просто использовать эту модель данных в качестве примера подколлекций с идентификаторами, динамически назначаемыми пользователями.

[3] Т.е. одна запись для документа субколлекции и одна запись для обновления родительского документа.

[4] Мы могли бы выбрать облачную функцию HTTPS, но вызываемые функции имеют несколько преимуществ по сравнению с HTTPS, как описано в документе.

[5] Обратите внимание, что некоторые части пояснительного текста, который следует ниже, напрямую скопированы / вставлены из документации Firebase!