Мотивация

Документы Firestore летают в вашем приложении: оно записывает, обновляет, удаляет документы и коллекции либо через облачные функции, либо непосредственно на стороне клиента с помощью различных клиентских SDK.

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

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

Есть несколько способов получить доступ к вашим данным:

  • читать и записывать данные из вашего приложения с помощью клиентских SDK
  • перемещаться по набору данных в консоли Firebase
  • Экспортировать набор данных в Google Cloud Storage
  • изучить свой набор данных с помощью Admin SDK с помощью скрипта или облачной функции

В этой статье мы обсудим последнее: мы напишем сценарий, использующий Admin SDK для работы со всеми документами. Приятно чувствовать, что наши данные в Firestore всегда у нас под рукой, это позволяет фильтровать данные и действовать с документами так, как нам нужно (потому что мы находимся в нашем скрипте!). Кроме того, он позволяет посещать подколлекции несуществующих документов, что часто вызывает вопросы о переполнении стека.

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

Структура данных

В Firestore корневые коллекции содержат документы, которые могут содержать коллекции (иногда называемые вложенными коллекциями), которые содержат документы и т. Д. Эта рекурсивная структура формирует дерево: документы и коллекции являются узлами , у них один родитель и ноль или несколько детей.

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

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

Приступим к кодированию!

Весь код можно найти в этой сути. Он состоит из трех модулей: admin.js, который устанавливает Admin SDK, traverse.js, который реализует обход, и myscript.js, который использует обход для отображения всего набора данных. Давайте посмотрим на реализацию обхода дерева. Модуль traverse.js предоставляет две функции:

  • traverseFromRoot,, который обращается ко всему набору данных,
  • traverseFromPath,, который посещает все коллекции и документы по указанному пути.

Вот код, пояснения следуют:

Пояснения

Обе функции принимают в качестве параметров два определяемых пользователем обратных вызова: onDocument и onCollection. Эти обратные вызовы выполняются для каждого документа или коллекции соответственно. Они могут проверить данные и при необходимости остановить обход текущей ветки.

Функция traverseFromRoot посещает все корневые коллекции, возвращаемые listCollections.

Функция traverseFromPath принимает путь в качестве входных данных. Это строка вида: root-collection / document / collection /… Нечетное количество уровней указывает на коллекцию, четное число указывает на документ. Вооружившись этими знаниями, мы можем получить DocumentReference или CollectionReference, а затем посетить их.

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

  1. Сначала он определяет, является ли узел документом или коллекцией, проверяя наличие listCollections, которое можно найти только в документах.
  2. Он выполняет соответствующий обратный вызов пользователя, передавая текущий узел (документ коллекции) в качестве аргумента.
  3. Если обратный вызов пользователя возвращает false, функция возвращает: обход в этой ветви не идет глубже.
  4. В противном случае обход продолжается глубже: мы перечисляем дочерние элементы текущего узла с помощью listDocuments или listCollections и последовательно посещаем каждый дочерний элемент.

Осторожность!

Этот код загружает в память сразу целые списки документов, поэтому объем доступной памяти может быть превышен. См. «Бонус: версия потока» для кода, который считывает документ один за другим, чтобы предотвратить исчерпание памяти.

Полученные результаты

Я запускаю этот скрипт в своем приложении чата, чтобы распечатать все документы (подробности см. В Суть), вот результат:

chatrooms
   firebase: {"subject":"Firebase"}
      messages
         m1: {"author":"user1","content":"Welcome everyone!"}
         m2: {"author":"user2","content":"hello user1!"}
   flash: "not-existing"
      messages
         m1: {"author":"user3","content":"Welcome to Flashboard"}
   react: {"subject":"React.js"}
      messages
         m1: {"author":"user3","content":"Welcome to the chatroom!"}
         m2: {"author":"user4","content":"Let's start coding!"}
users
   user1: {"name":"Alice"}
   user2: {"name":"Bob"}
   user3: {"name":"Carol"}

Мы видим две мои корневые коллекции: чаты и пользователей. Он показывает три документа чатов, даже через chatrooms / flash не существует (подробнее об этом в другой статье). Эти документы чатов имеют тему в своих данных и вложенную коллекцию: messages, которые содержат все документы сообщений.

Дополнительные замечания

  • Для простоты мы использовали listDocuments для перечисления дочерних элементов коллекции, чтобы повысить масштабируемость, его можно было бы заменить на get для извлечения данных документов для каждого документа вместо того, чтобы делать это в onDocument, однако он не будет игнорировать не- существующие документы. Его также можно было заменить на поток для масштабируемости (см. Код в конце).
  • Ограничение: listCollections и listDocuments возвращают массив, содержащий все ссылки. Эти данные не передаются в потоковом режиме, поэтому они могут превысить доступную вам память, если у вас много документов с длинными идентификаторами.
  • Цена: listDocuments стоит 1 чтение на документ, согласно этому SO-ответу мы можем разумно ожидать, что listCollection стоит 1 чтение для каждой коллекции. Таким образом, в целом обход стоит 1 чтение на документ + 1 чтение на коллекцию в вашем наборе данных, плюс считывания, выполненные в onDocument и onCollection по усмотрению пользователя.
  • Наша реализация обращается к документам и коллекциям последовательно, потому что мы предпочитаем детерминизм упорядочивания и защиты памяти (только один документ читается за раз) скорости для этого сценария обслуживания. Его можно распараллелить, см. Код в конце.
  • Документы Firestore могут содержать ссылки на другие документы или коллекции, что делает его структуру графом, а не деревом (узлы могут иметь несколько родителей). Если мы не будем следовать этим ссылкам, это останется деревом, поскольку мы не исследуем эти ссылки.
  • Мы реализовали обход предзаказа, при необходимости вы можете расширить его до постзаказа. Порядок неприменим, потому что наше дерево не двоичное.

Бонус: параллельная версия

Бонус: потоковая версия