В прошлом уроке мы перекраивали документы одной коллекции. В уроке про аккумуляторы было первое соединение: $lookup подтягивал покупателя к заказу по customer_id. Сегодня идём дальше. Научимся разворачивать массивы в отдельные документы, соединять заказы с книгами через их позиции, находить документы без пары и писать $lookup со своим конвейером. Примеры на учебной базе магазина.
Связка ключей
Представь связку ключей на кольце. Пока ключи на кольце, это один предмет. Чтобы рассортировать их по замкам, кольцо разбирают: каждый ключ ложится на стол отдельно, и к каждому можно приклеить бирку «от какой двери».
В заказе массив позиций это такое кольцо. Пока позиции внутри заказа, сгруппировать их по книгам нельзя: $group работает с документами, а не с элементами массивов. Сначала кольцо надо разобрать.
$unwind: один документ на каждый элемент
$unwind берёт документ с массивом и выдаёт столько документов, сколько элементов в массиве. В каждом поле массива заменено одним элементом, остальные поля копируются. Заказ 3, где две книги:
db.orders.aggregate([
{ $match: { _id: 3 } },
{ $unwind: '$items' },
{ $project: { customer_id: 1, title: '$items.title', qty: '$items.qty' } }
])
[
{ _id: 3, customer_id: 2, title: 'Совершенный код', qty: 1 },
{ _id: 3, customer_id: 2, title: 'Шинель', qty: 1 }
]
Один заказ стал двумя документами, и у обоих _id: 3. Это нормально: после $unwind поле _id перестаёт быть уникальным в потоке. Уникальность нужна в коллекции, а не в промежуточных документах конвейера.
Теперь позиции можно группировать. Сколько штук каждой книги продано, без отменённых заказов:
db.orders.aggregate([
{ $match: { status: { $ne: 'cancelled' } } },
{ $unwind: '$items' },
{ $group: { _id: '$items.title', sold: { $sum: '$items.qty' } } },
{ $sort: { sold: -1, _id: 1 } }
])
[
{ _id: 'Мастер и Маргарита', sold: 2 },
{ _id: 'Пикник на обочине', sold: 2 },
{ _id: 'Грокаем алгоритмы', sold: 1 },
{ _id: 'Совершенный код', sold: 1 },
{ _id: 'Трудно быть богом', sold: 1 },
{ _id: 'Шинель', sold: 1 }
]
«Мастера и Маргариту» брали в двух разных заказах по одной штуке, «Пикник» один раз, но две штуки. После $unwind это неважно: каждая позиция стала документом, и $sum сложил количество.
Если ты проходил соединения в PostgreSQL, то узнаешь в $unwind таблицу позиций заказа: там позиции и так лежат по одной строке.
Куда пропадают документы с пустым массивом
Развернём отзывы книг:
db.books.aggregate([
{ $unwind: '$reviews' },
{ $project: { _id: 0, title: 1, user: '$reviews.user', rating: '$reviews.rating' } }
])
[
{ title: 'Мастер и Маргарита', user: 'Ольга', rating: 5 },
{ title: 'Мастер и Маргарита', user: 'Павел', rating: 4 },
{ title: 'Пикник на обочине', user: 'Роман', rating: 5 },
{ title: 'Грокаем алгоритмы', user: 'Павел', rating: 5 },
{ title: 'Грокаем алгоритмы', user: 'Светлана', rating: 3 }
]
Из девяти книг в ответе только три. У остальных поля reviews нет, и $unwind их молча выбросил: ключей на кольце не было, класть на стол нечего. Это частая ошибка в отчётах вида «средняя оценка по жанрам»: книги без отзывов исчезают, а вместе с ними целые жанры.
Сохранить такие документы помогает длинная форма с параметром preserveNullAndEmptyArrays:
db.books.aggregate([
{ $match: { genre: 'повесть' } },
{ $unwind: { path: '$reviews', preserveNullAndEmptyArrays: true } },
{ $project: { _id: 0, title: 1, rating: '$reviews.rating' } }
])
[ { title: 'Собачье сердце' }, { title: 'Шинель' }, { title: 'Нос' } ]
Все три повести на месте, только без оценки. Обрати внимание: поля rating в ответе нет вовсе. $project не создаёт поле, если путь ведёт в пустоту. Это та же логика, что у LEFT JOIN с NULL в SQL, только вместо NULL поля просто нет.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.