В этом руководстве рассматриваются некоторые ключевые концепции архитектуры данных и передовые методы структурирования данных JSON в Firebase Realtime Database .
Создание правильно структурированной базы данных требует тщательного продумывания. Самое главное — спланировать, как данные будут сохраняться и извлекаться, чтобы максимально упростить этот процесс.
Как структурированы данные: это JSON-дерево
Все данные Firebase Realtime Database хранятся в виде JSON-объектов. Базу данных можно представить как JSON-дерево, размещённое в облаке. В отличие от базы данных SQL, здесь нет таблиц и записей. При добавлении данных в JSON-дерево они становятся узлом существующей JSON-структуры со связанным ключом. Вы можете предоставить собственные ключи, такие как идентификаторы пользователей или семантические имена, или их можно получить с помощью метода push()
.
Если вы создаете собственные ключи, они должны быть в кодировке UTF-8, иметь длину не более 768 байт и не могут содержать .
, $
, #
, [
, ]
, /
или управляющие символы ASCII 0-31 или 127. Вы также не можете использовать управляющие символы ASCII в самих значениях.
Например, рассмотрим чат-приложение, позволяющее пользователям хранить базовый профиль и список контактов. Типичный профиль пользователя находится по адресу, например, /users/$uid
. Запись в базе данных пользователя alovelace
может выглядеть примерно так:
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { "..." }, "eclarke": { "..." } } }
Хотя база данных использует дерево JSON, данные, хранящиеся в базе данных, могут быть представлены в виде определенных собственных типов, соответствующих доступным типам JSON, что поможет вам писать более удобный для поддержки код.
Лучшие практики для структуры данных
Избегайте вложенных данных
Поскольку Firebase Realtime Database допускает вложенность данных до 32 уровней, может возникнуть соблазн подумать, что это должна быть структура по умолчанию. Однако при извлечении данных из определённого места в базе данных вы также извлекаете все его дочерние узлы. Кроме того, предоставляя кому-либо доступ на чтение или запись к узлу базы данных, вы также предоставляете ему доступ ко всем данным в этом узле. Поэтому на практике лучше всего поддерживать максимально плоскую структуру данных.
В качестве примера того, почему вложенные данные плохи, рассмотрим следующую многовложенную структуру:
{ // This is a poorly nested data architecture, because iterating the children // of the "chats" node to get a list of conversation titles requires // potentially downloading hundreds of megabytes of messages "chats": { "one": { "title": "Historical Tech Pioneers", "messages": { "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." }, "m2": { ... }, // a very long list of messages } }, "two": { "..." } } }
При такой вложенной структуре итерация данных становится проблематичной. Например, для перечисления заголовков чатов требуется загрузить на клиент всё дерево chats
, включая всех участников и сообщения.
Сглаживание структур данных
Если же данные разделить на отдельные пути (это также называется денормализацией), их можно эффективно загружать отдельными вызовами по мере необходимости. Рассмотрим эту уплощённую структуру:
{ // Chats contains only meta info about each conversation // stored under the chats's unique ID "chats": { "one": { "title": "Historical Tech Pioneers", "lastMessage": "ghopper: Relay malfunction found. Cause: moth.", "timestamp": 1459361875666 }, "two": { "..." }, "three": { "..." } }, // Conversation members are easily accessible // and stored by chat conversation ID "members": { // we'll talk about indices like this below "one": { "ghopper": true, "alovelace": true, "eclarke": true }, "two": { "..." }, "three": { "..." } }, // Messages are separate from data we may want to iterate quickly // but still easily paginated and queried, and organized by chat // conversation ID "messages": { "one": { "m1": { "name": "eclarke", "message": "The relay seems to be malfunctioning.", "timestamp": 1459361875337 }, "m2": { "..." }, "m3": { "..." } }, "two": { "..." }, "three": { "..." } } }
Теперь можно перебирать список комнат, загружая всего несколько байтов за разговор, быстро извлекая метаданные для составления списка или отображения комнат в пользовательском интерфейсе. Сообщения можно получать отдельно и отображать по мере поступления, что позволяет интерфейсу оставаться отзывчивым и быстрым.
Создавайте масштабируемые данные
При разработке приложений часто бывает выгоднее загрузить подмножество списка. Это особенно актуально, если список содержит тысячи записей. Если эта связь статична и однонаправлена, можно просто вложить дочерние объекты в родительский.
Иногда эта связь более динамична, или может потребоваться денормализация данных. Во многих случаях денормализацию данных можно выполнить с помощью запроса для извлечения подмножества данных, как описано в разделе «Извлечение данных» .
Но даже этого может быть недостаточно. Рассмотрим, например, двустороннюю связь между пользователями и группами. Пользователи могут принадлежать к группе, а группы представляют собой список пользователей. Когда приходит время определить, к каким группам принадлежит пользователь, всё усложняется.
Нужен элегантный способ составить список групп, к которым принадлежит пользователь, и извлечь данные только по этим группам. Индекс групп может здесь очень помочь:
// An index to track Ada's memberships { "users": { "alovelace": { "name": "Ada Lovelace", // Index Ada's groups in her profile "groups": { // the value here doesn't matter, just that the key exists "techpioneers": true, "womentechmakers": true } }, // ... }, "groups": { "techpioneers": { "name": "Historical Tech Pioneers", "members": { "alovelace": true, "ghopper": true, "eclarke": true } }, // ... } }
Вы могли заметить, что некоторые данные дублируются, поскольку связь сохраняется как в записи Ады, так и в группе. Теперь alovelace
индексируется в группе, а techpioneers
указан в профиле Ады. Поэтому, чтобы удалить Аду из группы, её нужно обновить в двух местах.
Это необходимая избыточность для двусторонних отношений. Она позволяет быстро и эффективно получать информацию о членстве в Ada, даже если список пользователей или групп исчисляется миллионами или когда правила безопасности Realtime Database запрещают доступ к некоторым записям.
Этот подход, инвертирующий данные путём перечисления идентификаторов в качестве ключей и установки значения true, делает проверку ключа такой же простой, как чтение /users/$uid/groups/$group_id
и проверка его на null
. Индекс работает быстрее и значительно эффективнее, чем запросы или сканирование данных.