Структурируйте свою базу данных

В этом руководстве рассматриваются некоторые ключевые концепции архитектуры данных и лучшие практики структурирования данных JSON в базе данных Firebase Realtime.

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

Как структурированы данные: это JSON-дерево.

Все данные базы данных Firebase Realtime хранятся в виде объектов 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 позволяет вкладывать данные глубиной до 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, даже если список пользователей или групп исчисляется миллионами или когда правила безопасности базы данных реального времени запрещают доступ к некоторым записям.

Этот подход, инвертирующий данные путем перечисления идентификаторов в качестве ключей и установки значения true, делает проверку ключа такой же простой, как чтение /users/$uid/groups/$group_id и проверка того, является ли он null . Индекс работает быстрее и намного эффективнее, чем запрос или сканирование данных.

Следующие шаги