Раздел 36 · MongoDB с нуля

Аккумуляторы, $project и $lookup

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке $group считал числа: сколько книг, какая средняя цена. Сегодня три шага дальше. Научим группу собирать списки, научим конвейер считать новые поля прямо в документе и впервые соединим две коллекции. Примеры, как обычно, на учебной базе магазина.

Аккумуляторы, которые собирают списки

Вернёмся к сортировочному центру. Раньше на посту группировки считали коробки в каждом районе. А можно составлять опись: какие посылки приехали, от каких отправителей, какая пришла первой. Для этого у $group есть ещё аккумуляторы.

Сгруппируем книги по автору и соберём о каждом сводку:

db.books.aggregate([
  { $sort: { year: 1, _id: 1 } },
  { $group: {
      _id: '$author.name',
      titles: { $push: '$title' },
      genres: { $addToSet: '$genre' },
      first: { $first: '$title' },
      newest: { $max: '$year' }
  } },
  { $sort: { _id: 1 } }
])
[
  {
    _id: 'Булгаков',
    titles: [ 'Мастер и Маргарита', 'Собачье сердце' ],
    genres: [ 'повесть', 'роман' ],
    first: 'Мастер и Маргарита',
    newest: 1987
  },
  ...
  {
    _id: 'Гоголь',
    titles: [ 'Нос', 'Шинель', 'Мёртвые души' ],
    genres: [ 'роман', 'повесть' ],
    first: 'Нос',
    newest: 1842
  },
  ...
]

Здесь четыре новых аккумулятора:

  • $push складывает значение каждого документа группы в массив. Повторы остаются.
  • $addToSet делает то же, но без повторов: у Гоголя три книги, а жанров два. Порядок элементов в таком массиве сервер не обещает, у тебя он может отличаться.
  • $first берёт значение из первого документа группы. «Первый» имеет смысл только после сортировки, поэтому в начале конвейера стоит $sort по году. У Гоголя первым идёт «Нос» 1836 года.
  • $max с годом показывает самую свежую книгу автора. В пару к нему есть $min.

Заметь, что $push тоже сохранил порядок сортировки: книги Гоголя выстроились по году.

$project: придумать новые поля

С проекциями в find мы уже знакомы: 1 показать поле, 0 скрыть. Этап $project умеет то же, плюс главное: вычислять новые поля из выражений. Сколько стоит одна страница книги:

db.books.aggregate([
  { $match: { price: { $exists: true } } },
  { $project: { _id: 0, title: 1, price: 1, pricePerPage: { $round: [{ $divide: ['$price', '$pages'] }, 2] } } },
  { $sort: { pricePerPage: -1 } },
  { $limit: 3 }
])
[
  { title: 'Грокаем алгоритмы', price: 1290, pricePerPage: 4.48 },
  { title: 'Шинель', price: 250, pricePerPage: 3.91 },
  { title: 'Совершенный код', price: 2490, pricePerPage: 2.78 }
]

{ $divide: ['$price', '$pages'] } это выражение агрегации: оператор и массив аргументов. Здесь цена делится на число страниц, а $round округляет до двух знаков. $match в начале отбрасывает «Нос»: без цены делить нечего.

Короткая «Шинель» неожиданно оказалась одной из самых дорогих книг за страницу. Такой вопрос find не задашь: в документах нет поля «цена страницы», его посчитал конвейер.

$addFields: добавить, ничего не теряя

$project оставляет только перечисленные поля. Если нужно добавить поле и сохранить все остальные, есть $addFields. Сколько отзывов у книги:

db.books.aggregate([
  { $match: { _id: { $in: [1, 7] } } },
  { $addFields: { reviewsCount: { $size: { $ifNull: ['$reviews', []] } } } },
  { $project: { title: 1, reviewsCount: 1 } }
])
[
  { _id: 1, title: 'Мастер и Маргарита', reviewsCount: 2 },
  { _id: 7, title: 'Грокаем алгоритмы', reviewsCount: 2 }
]

$size считает элементы массива. У книг без отзывов поля reviews нет, и $size упал бы с ошибкой, поэтому $ifNull подставляет пустой массив.

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.