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

upsert, replaceOne и удаление

junior~20 мин

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

В прошлом уроке мы правили массивы внутри документов, которые точно существуют. Сегодня закрываем кластер про изменение данных тремя темами: обновление, которое само создаёт документ, если его нет, замена документа целиком и удаление. Примеры идут по порядку на учебной базе магазина.

Журнал прихода на складе

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

Такое действие называют upsert, от английских update и insert. В PostgreSQL то же самое делали через INSERT … ON CONFLICT, а в MongoDB это опция обновления { upsert: true }.

upsert: обновить или создать

Заведём коллекцию склада stock. Её ещё нет, но MongoDB создаст её при первой записи. Учтём приход двух «Пикников» дважды:

db.stock.updateOne({ _id: 3 }, { $inc: { qty: 2 } }, { upsert: true })
db.stock.updateOne({ _id: 3 }, { $inc: { qty: 2 } }, { upsert: true })
db.stock.find()
{
  acknowledged: true,
  insertedId: 3,
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
[ { _id: 3, qty: 4 } ]

Сравни два ответа. В первый раз под фильтр ничего не подошло (matchedCount: 0), и сервер вставил новый документ: upsertedCount: 1, а в insertedId номер нового документа. Значение _id: 3 он взял из фильтра, а qty: 2 из $inc, который на отсутствующем поле работает как установка значения. Во второй раз документ уже был, и сработало обычное обновление.

Без { upsert: true } первая команда вернула бы matchedCount: 0 и ничего бы не создала, как в уроке про updateOne.

$setOnInsert: только при создании

Часто при создании нужно записать поля, которые потом трогать нельзя. Например, название книги и месяц первого прихода. Для этого есть $setOnInsert: он работает только тогда, когда upsert создаёт документ.

db.stock.updateOne(
  { _id: 7 },
  { $inc: { qty: 1 }, $setOnInsert: { title: 'Грокаем алгоритмы', firstArrival: 'октябрь' } },
  { upsert: true }
)

Выполним эту команду дважды и посмотрим на склад:

{
  acknowledged: true,
  insertedId: 7,
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
[
  { _id: 3, qty: 4 },
  {
    _id: 7,
    firstArrival: 'октябрь',
    qty: 2,
    title: 'Грокаем алгоритмы'
  }
]

qty выросло до двух, а title и firstArrival записались один раз. Если завтра в команде окажется другой месяц, первая дата прихода не перезапишется.

Поля из фильтра

Если в фильтре условие на равенство, при создании это поле попадёт в новый документ. Фильтр по названию и $set цены:

db.books.updateOne({ title: 'Пиши, сокращай' }, { $set: { price: 900 } }, { upsert: true })
db.books.find({ title: 'Пиши, сокращай' }, { _id: 0 })
{
  acknowledged: true,
  insertedId: ObjectId('6ac93c4b52b1c89566dd96bd'),
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}
[ { title: 'Пиши, сокращай', price: 900 } ]

_id в фильтре не было, поэтому сервер создал ObjectId, у тебя он будет другим. Будь осторожен с upsert по неуникальным полям: если два запроса одновременно не найдут книгу по названию, оба её создадут, и получатся дубли. Защита от этого уникальный индекс, о нём в уроке про индексы.

урок из курса

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

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