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