Моделирование данных в MongoDB

в 13:03, , рубрики: mongodb, nosql

imageОдна из самых разрекламированных фич MongoDB — это гибкость. Я сам не раз подчеркивал это в бесчисленных разговорах о MongoDB. Однако, гибкость — это палка о двух концах: большая гибкость подразумевает более широкий выбор решений для моделирования данных. Тем не менее, мне нравится гибкость, которую предоставляет MongoDB, просто нужно иметь ввиду некоторые рекомендации, прежде чем начать разрабатывать модель данных.

В этой статье мы рассмотрим, как смоделировать структуру, содержащую списки рассылок и данные о людях, которые входят в эти списки.

Ниже представлены требования:

  • Человек может иметь один или более e-mail адресов;
  • Человек может состоять в любом количестве списков рассылок;
  • Человек может выбрать любое название для любого списка рассылки, в котором состоит.

Стратегия «без встраиваний»

Давайте посмотрим, как будет выглядеть наша модель данных, если никакие данные никуда не встраивать.

У нас есть подписчики People с именами и паролями:

{
    _id: PERSON_ID,
    name: "Василий Пупкин",
    pw: "Хешированный пароль"
}

У нас есть коллекция адресов Adresses, в которой каждый документ содержит e-mail адрес и привязку к конкретному подписчику:

{
    _id: ADDRESS_ID,
    person: PERSON_ID,
    address: "vpupkin@gmail.com"
}

У нас есть группы Groups, каждая из которых содержит только идентификатор группы (она, конечно же, может содержать и другие данные, но мы специально опустим этот момент, дабы сконцентрироваться на подписках)

{
    _id: GROUP_ID
}

И наконец, мы имеем коллекцию подписок Memberships. Каждая Подписка объединяет людей в Группы, кроме этого, содержит название, которое человек выбрал для данной Группы, и ссылку на e-mail адрес, который он хочет использовать для получения рассылки в данной Группе:

{
    _id: MEMBERSHIP_ID,
    person: PERSON_ID,
    group: GROUP_ID,
    address: ADDRESS_ID,
    group_name: "Семья"
}

Такая модель данных понятна, легка в разработке и проста в обслуживании. Мы создали модель, которую удобно использовать в реляционной базе данных. При этом мы совсем не приняли во внимание документо-ориентированный подход MongoDB. Давайте рассмотрим, что мы будем делать, чтобы получить, например, e-mail адреса всех членов одной Группы, имея один известный e-mail адрес и название этой Группы:

  1. В коллекции Addresses по известному e-mail найдем PERSON_ID;
  2. В коллекции Memberships по полученному PERSON_ID и известному названию Группы найдем GROUP_ID;
  3. Опять же в коллекции Memberships по полученному GROUP_ID найдем список Подписок данной Группы;
  4. И наконец из коллекции Addresses по ADDRESS_ID, пройдя по каждой Подписке из полученного списка, получим список e-mail адресов.

Слегка сложновато, не правда ли?

Стратегия «все встроено»

Теперь рассмотрим случай, когда все данные встроены в один документ. Для этого мы возьмем все Подписки Группы и встроим их в модель Группы. Плюсом в каждую Подписку встроим данные об Подписчике и его e-mail адресах:

{
    _id: GROUP_ID,
    memberships: [{
        address: "vpupkin@gmail.com",
        name: "Василий Пупкин",
        pw: "Хешированный пароль",
        person_addresses: [vpupkin@gmail.com", "vpupkin@mail.ru", ...],
        group_name: "Семья"
    }, ...]
}

Смысл встраивать все связные данные в один документ заключается в том, что теперь некоторые запросы к данным делать намного проще. Запрос из предыдущей части статьи становится совсем простым (помните, нам нужно, имея один известный e-mail адрес и название Группы, узнать e-mail адреса остальных членов этой Группы):

  1. В коллекции Groups найдем Группу, содержащую Подписку, в которой group_name совпадает с известным нам названием Группы и массив person_addresses содержит известный нам e-mail;
  2. Разберем полученный документ для извлечения остальных e-mail адресов.

Гораздо проще. Но что будет, если Подписчик захочет поменять имя или пароль? Нам придется менять его имя или пароль в каждой встроенной Подписке каждой Группы, в которой состоит этот Подписчик. Это также касается добавления нового или удаление существующего e-mail адреса из массива person_addresses. Такие моменты говорят нам об определенном характере данной модели: она хорошо подходит для специфичных запросов (потому что все необходимые данные уже внутри, типа pre-join), но может стать кошмаров в долгосрочной перспективе в плане сопровождения.

Стратегия «частичного встраивания»

Подход, который я чаще всего рекомендую, это начать думать о модели данных без встраивания. После того, как у вас есть черновая модель, можно начинать выделять случаи, когда встраивание имеет смысл. Как правило, это отношения один-ко-многим.

Например, несолько e-mail адресов из коллекции Addresses принадлежат одному Подписчику (они также участвуют в модели Подписки) и обычно меняются не так часто. Поэтому мы объединим их в массив и добавим в нашу модель Подписчика, сделав её чуточку схожей с ментальной моделью.

Каждая Подписка связана с конкретным Подписчиком и конкретной Группой, поэтому можно встроить Подписки как в модель Подписчика, так и в модель Группы. В подобных случаях важно думать и о модели доступа к данным и о размере встраиваемых данных. Мы ожидаем, что люди вряд ли подпишутся на рассылку более чем из 1000 разных групп, и отдельно взятая группа в свою очередь также вряд ли наберет более 1000 подписчиков. В данном случае, цифры нам ничего полезного не говорят. Однако, наша модель доступа к данным, напротив, говорит нам, что при выводе на экран необходимо видеть все подписки конкретного человека. Для упрощения запроса мы встроим Подписки в модель Подписчика. Преимущество ещё и в том, что список e-mail адресов Подписчика находятся в модели Подписчика, а в Подписке используется один из адресов этого списка, и если нам нужно изменить или удалить e-mail адрес, это можно сделать в одном месте.

Теперь наша модель данных выглядит так:

{
    _id: PERSON_ID,
    name: "Василий Пупкин",
    pw: "Хешированный пароль",
    addresses: ["vpupkin@gmail.com", "vpupkin@mail.ru", ...],
    memberships: [{
        address: "vpupkin@gmail.com",
        group_name: "Семья",
        group: GROUP_ID
    }, ...]
}

Это модель Подписчика, кроме неё есть ещё модель Группы, которая идентична той, что рассмотрна в описании стратегии «без встраивания».

Запрос, который мы обсуждали выше, теперь будет выглядеть так:

  1. В коллекции People найдем Подписчика с искомым e-mail адресом, среди Подписок которого есть Подписка с искомым названием;
  2. Используя GROUP_ID найденной Подписки, найдем в коллекции People других Подписчиков этой Группы и возьмем их e-mail адреса непосредственно из Подписки.

Это по-прежнему почти так же просто, как и в случае, когда все встроено, но теперь наша модель данных намного чище и проще в обслуживании. Надеюсь, эта статья было полезной для вас.

От переводчика: в целом это довольно вольный перевод и делался сугубо для личных целей, поэтому все оплеухи, пожалуйста, в личку. Оговорюсь также, что некоторые понятия подменены по субъективным причинам, некоторые моменты опущены, так как для передачи сути не играли значимой роли. В любом случае прошу обращаться к оригиналу.

Автор: mgrach


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js