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

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

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

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

Все данные базы данных Firebase Realtime хранятся в виде объектов JSON. Вы можете представить базу данных как дерево JSON, размещенное в облаке. В отличие от базы данных SQL здесь нет таблиц и записей. Когда вы добавляете данные в дерево JSON, они становятся узлом в существующей структуре JSON со связанным ключом. Вы можете предоставить свои собственные ключи, такие как идентификаторы пользователей или семантические имена, или они могут быть предоставлены вам с помощью запроса POST .

Если вы создаете свои собственные ключи, они должны иметь кодировку 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 . Индекс работает быстрее и намного эффективнее, чем запрос или сканирование данных.

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