В этом руководстве рассматриваются некоторые ключевые концепции архитектуры данных и рекомендации по структурированию данных JSON в Firebase Realtime Database.
Создание правильно структурированной базы данных требует некоторой предусмотрительности. Самое главное, вам нужно спланировать, как данные будут сохраняться и позже извлекаться, чтобы сделать этот процесс максимально простым.
Как структурированы данные: дерево JSON
Все данные Firebase Realtime Database хранятся в виде объектов JSON. Базу данных можно представить как дерево JSON, размещенное в облаке. В отличие от базы данных SQL, в ней нет таблиц или записей. Когда вы добавляете данные в дерево JSON, они становятся узлом в существующей структуре JSON со связанным ключом. Вы можете указать собственные ключи, например идентификаторы пользователей или семантические имена, или использовать ключи, предоставленные childByAutoId.
Например, рассмотрим приложение для чата, которое позволяет пользователям хранить базовый профиль и список контактов. Обычно профиль пользователя находится по пути /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 указан в профиле Ады. Чтобы удалить Аду из группы, нужно обновить данные в двух местах.
Это необходимо для двусторонних отношений. Это позволяет быстро и эффективно получать информацию о членстве Ады, даже если список пользователей или групп исчисляется миллионами или если правила безопасности Realtime Database запрещают доступ к некоторым записям.
Такой подход, при котором данные инвертируются путем перечисления идентификаторов в качестве ключей и установки значения true, позволяет проверить ключ так же просто, как прочитать /users/$uid/groups/$group_id и проверить, равно ли оно null. Индекс работает быстрее и намного эффективнее, чем запрос или сканирование данных.