Статьи

6 практических правил для разработки схемы MongoDB: Часть 1

Уильям Золя, ведущий инженер технической поддержки MongoDB

«У меня большой опыт работы с SQL, но я только начинающий в MongoDB. Как мне смоделировать отношения один-к-одному? » Это один из наиболее распространенных вопросов, которые я получаю от пользователей, посещающих рабочие часы MongoDB.

У меня нет короткого ответа на этот вопрос, потому что нет только одного пути, есть целая радуга. MongoDB обладает богатым и тонким словарным запасом для выражения того, что в SQL сведено в термин «один к N». Позвольте мне познакомить вас с вашими предпочтениями в моделировании отношений One-to-N.

Здесь так много о чем поговорить, я разбил это на три части. В этой первой части я расскажу о трех основных способах моделирования отношений один-к-одному. Во второй части я расскажу о более сложных схемах, включая денормализацию и двусторонние ссылки. И в заключительной части я рассмотрю всю радугу выборов и дам несколько советов по выбору из тысяч (в действительности — тысяч) вариантов, которые вы можете учитывать при моделировании одного отношения «один к N».

Многие новички считают, что единственный способ моделировать «один-к-N» в MongoDB — это вставить массив поддокументов в родительский документ, но это просто неправда. То, что вы можете встраивать документ, не означает, что вы должны встраивать документ.

При разработке схемы MongoDB вам нужно начать с вопроса, который вы никогда не будете учитывать при использовании SQL: какова мощность отношений? Говоря менее формально: вам нужно охарактеризовать ваши отношения «один-к-одному» с немного большим количеством нюансов: «один-к-немногим», «один-ко-многим» или «один-к-миллиардам»? В зависимости от того, какой это, вы будете использовать другой формат для моделирования отношений.

Основы: моделирование от одного до нескольких

Примером «один к немногим» могут быть адреса человека. Это хороший вариант использования для встраивания — вы бы поместили адреса в массив внутри вашего объекта Person:

> db.person.findOne()
{
  name: 'Kate Monster',
  ssn: '123-456-7890',
  addresses : [
     { street: '123 Sesame St', city: 'Anytown', cc: 'USA' },
     { street: '123 Avenue Q', city: 'New York', cc: 'USA' }
  ]
}

Этот дизайн имеет все преимущества и недостатки встраивания. Основным преимуществом является то, что вам не нужно выполнять отдельный запрос, чтобы получить встроенные детали; главный недостаток в том, что у вас нет возможности получить доступ к встроенным деталям как к отдельным объектам.

Например, если вы моделируете систему отслеживания задач, каждому сотруднику будет назначено несколько задач. Встраивание задач в документ Person сделало бы такие запросы, как «Показать все задачи, которые должны быть выполнены завтра», намного сложнее, чем нужно. Я расскажу о более подходящем дизайне для этого варианта использования в следующем посте.

Основы: один ко многим

Примером «один ко многим» могут быть детали для продукта в системе заказа запасных частей. Каждый продукт может иметь до нескольких сотен запасных частей, но не более пары тысяч или около того. (Все эти болты, шайбы и прокладки разных размеров складываются.) Это хороший вариант использования для ссылок — вы бы поместили ObjectID-ы деталей в массив в документе продукта. (Для этих примеров я использую 2-байтовые ObjectID, потому что их легче читать: реальный код будет использовать 12-байтовые ObjectID.)

Каждая часть будет иметь свой собственный документ:

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    qty: 94,
    cost: 0.94,
    price: 3.99
}

Каждый продукт будет иметь свой собственный документ, который будет содержать массив ссылок ObjectID на части, составляющие этот продукт:

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the #4 grommet above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
    ]

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

 // Fetch the Product document identified by this catalog number
> product = db.products.findOne({catalog_number: 1234});
   // Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

Для эффективной работы вам потребуется индекс «products.catalog_number». Обратите внимание, что всегда будет индекс ‘parts._id’, поэтому запрос всегда будет эффективным.

Этот стиль ссылок имеет дополнительный набор преимуществ и недостатков для встраивания. Каждая часть является отдельным документом, поэтому их легко искать и обновлять независимо. Один из вариантов использования этой схемы — выполнить второй запрос, чтобы получить подробные сведения о деталях для продукта. (Но держите эту мысль, пока мы не доберемся до денормализации в части 2.)

В качестве дополнительного бонуса эта схема позволяет вам использовать отдельные Части, используемые несколькими Продуктами, поэтому ваша схема One-to-N просто стала схемой N-to-N без необходимости в объединении таблиц!

Основы: от одного до нескольких миллиардов

Примером «от одного до нескольких миллиардов» может служить система регистрации событий, которая собирает сообщения журнала для разных компьютеров. Любой заданный хост может генерировать достаточно сообщений для переполнения документа размером 16 МБ, даже если все, что вы храните в массиве, это ObjectID. Это классический вариант использования «родительских ссылок» — у вас будет документ для хоста, а затем вы сохраните ObjectID хоста в документах для сообщений журнала.

> db.hosts.findOne()
{
    _id : ObjectID('AAAB'),
    name : 'goofy.example.com',
    ipaddr : '127.66.66.66'
}

>db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    host: ObjectID('AAAB')       // Reference to the Host document
}

Вы бы использовали (немного другое) объединение на уровне приложений, чтобы найти самые последние 5000 сообщений для хоста:

  // find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // assumes unique index
   // find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

резюмировать

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

  • Нужно ли когда-либо существам на «N» стороне One-to-N стоять в одиночестве?
  • Какова кардинальность отношений: это один к немногим; один ко многим; или от одного до миллиардов?

Основываясь на этих факторах, вы можете выбрать одну из трех базовых схем схемы One-to-N:

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

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