В прошлом уроке мы правили массивы внутри документов, которые точно существуют. Сегодня закрываем кластер про изменение данных тремя темами: обновление, которое само создаёт документ, если его нет, замена документа целиком и удаление. Примеры идут по порядку на учебной базе магазина.
Журнал прихода на складе
На склад привезли две «Пикника на обочине». Кладовщик открывает журнал: если строка про эту книгу уже есть, он прибавляет к остатку два. Если строки нет, потому что книгу привезли впервые, он заводит новую строку с остатком два. Кладовщику всё равно, какой из двух случаев: действие одно, «учти приход».
Такое действие называют 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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.