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

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

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

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

,

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

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