Не надо лезть в MongoDB с реляционным подходом. Этот тезис вроде бы очевиден, но когда дело доходит до реализации, возникает множество вопросов. И стопроцентно одним из них будет: «Как делать join?». Правильный ответ: никак.
Да, в версии 3.2 появился $lookup в агрегациях. Отчасти это замена классическому JOIN из реляционного мира. Но в целом агрегации — не самый быстрый в MongoDB инструмент. Лучше, когда запрос идёт к одной коллекции и это обычный find.
Стоит отметить, что использование NoSQL не обходится бесплатно. К примеру, обеспечение консистентности данных встроено в реляционные базы по умолчанию. При использовании MongoDB задача обеспечить консистентность ложится на программу‑клиент.
Будет лучше посмотреть, как это происходит, на примере с постом в блог. Рассмотрим сначала, как его представить в обычной реляционной БД в нормализованном виде, а потом — разные варианты представления постов в MongoDB.
Предположим, что нам нужно решить следующие задачи:
- выводить список постов с заголовками, категорией и тегами;
- выводить отдельный пост;
- выводить список тегов;
- выводить список категорий;
- редактировать имена тегов и категорий.
В реляционной базе, скорее всего, структура БД будет примерно такой:
posts
idtitlebodyauthor_idcategory_idslug
authors
idname
categories
idtitleslug
tags
idnameslug
posts_tags
post_idtag_id
Есть ряд вариантов, как организовать документы с постами в MongoDB.
Вариант 1. Хранить данные в одной коллекции
posts
_idtitlebodyauthorcategorytitleslug
tags[nameslug
]
slug
Такая структура хороша тем, что список постов и вывод отдельного поста решаются одним запросом. Мы сразу получим все необходимые данные. Список тегов и категорий придётся строить с помощью агрегаций. Если их количество невелико, то можно результаты агрегаций просто хранить в памяти или кэшировать. Редактировать имена тегов и категорий можно достаточно простыми UPDATE. При наличии индексов эта операция будет относительно быстрой.
Такой подход подойдёт для относительно небольшой коллекции. На большом количестве документов операции обновления начнут заметно тормозить. К тому же, хоть в данном примере это и маловероятно, если документы у вас сложные и содержат много данных, можно упереться в предельный размер документа MongoDB. На «живой» базе разрывать документы по отдельным коллекциям — весьма сомнительное удовольствие.
Вариант 2. Вынести данные тегов и категории в отдельные коллекции
По сути, сделать обычную нормализацию немного в «монговском» стиле: для связи many‑to‑many тегов и постов не делать отдельную коллекцию, а хранить в постах список id тегов.
Коллекции в этом случае будут выглядеть так:
categories
_idtitleslug
tags
_idnameslug
posts
_idtitlebodyauthorcategory_idtags[tag_id]
slug
Отлично, проблему с редактированием категорий и тегов решили. Теперь эта операция занимает константное время. Построить список тегов и категорий — не проблема. Но бесплатного ничего не бывает: теперь для построения поста нам надо сделать 3 запроса — получить сам пост, получить категорию, получить теги. Аналогичные проблемы придётся решать при построении списка постов.
Этот способ подойдёт тогда, когда имена категорий и тегов меняются часто, но их количество не очень велико. При этом нет жёстких требований к скорости отдачи постов на чтение.
Вариант 3. Гибридная схема
categories
_idtitleslug
tags
_idnameslug
posts
_idtitlebodyauthorcategorycategory_idtitleslug
tags[tag_idnameslug
]
slug
По сути, мы добавляем поля, которых нам не хватало во втором варианте, чтобы отдавать пост за один запрос. При этом возникают проблемы с редактированием: нужно не только поменять тег в коллекции тегов, но и пройтись по постам и исправить тег там. Но, в отличие от первого варианта, в этом легче написать код, который будет исправлять посты асинхронно в фоновом режиме.
Пример с блогом очень искусственный (я бы вообще десять раз подумал, прежде чем делать это не на реляционной базе), но даже на нём видно, что все подходы к разделению данных по коллекциям требуют написания какого‑то количества дополнительного кода. И, правильно оценив свою предметную область и объёмы данных, можно прикинуть, где кода будет меньше, он будет проще и не будет провалов по производительности.
P.S. Понимаю, что я не то что капитан Очевидность, а даже адмирал Ясен Пень. Но подобные вопросы с MongoDB (да и в целом с NoSQL) возникают с завидной регулярностью.