В прошлом уроке мы ускоряли запросы. Этот кластер о другом: что MongoDB обещает, когда данные меняют сразу несколько клиентов. В обзорном уроке мы уже спасли последний экземпляр книги условием в фильтре. Сегодня разберём, где именно проходит граница атомарности, почему updateMany по ней не проходит и какие ещё приёмы держатся на одном документе.
Окно на почте
На почте одно окно обслуживает одного человека. Пока ты заполняешь бланк, взвешиваешь посылку и платишь, никто не влезет посередине и не отправит твою посылку по чужому адресу. Но если тебе нужно в три окна по очереди, между ними может вклиниться кто угодно.
В MongoDB окно это один документ. Всё, что ты делаешь с одним документом одной командой, выполняется целиком и без вмешательства других клиентов. Такую гарантию называют атомарностью на уровне документа, и она есть всегда, без настроек и без транзакций.
Что входит в одно окно
Граница проходит по документу, а не по полю. Одна команда может добавить отзыв во вложенный массив, поднять цену и записать, кто менял, и всё это случится разом:
db.books.updateOne(
{ _id: 3 },
{
$push: { reviews: { user: 'Светлана', rating: 4, text: 'Страшно и красиво' } },
$inc: { price: 30 },
$set: { updatedBy: 'Светлана' }
}
)
db.books.findOne({ _id: 3 }, { price: 1, updatedBy: 1, 'reviews.user': 1 })
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
_id: 3,
price: 480,
reviews: [ { user: 'Роман' }, { user: 'Светлана' } ],
updatedBy: 'Светлана'
}
Ни один читатель не увидит книгу с новым отзывом, но старой ценой. Отсюда главный совет моделирования из урока про встраивание: то, что должно меняться вместе, держи в одном документе. Заказ с позициями и итоговой суммой внутри обновляется одной командой, и сумма никогда не разойдётся с позициями.
updateMany это много окон
updateMany выглядит как одна команда, но атомарна она только для каждого документа по отдельности. Сервер идёт по подходящим документам и меняет их по одному. Если на середине случится ошибка, уже изменённые документы останутся изменёнными.
Проверим. Создадим уникальный индекс по полю code и попробуем выдать всем повестям один и тот же код:
db.books.createIndex({ code: 1 }, { unique: true, sparse: true })
db.books.updateMany({ genre: 'повесть' }, { $set: { code: 'P' } })
db.books.find({ genre: 'повесть' }, { title: 1, code: 1 }).sort({ _id: 1 })
code_1
Uncaught MongoServerError: Plan executor error during update :: caused by :: E11000 duplicate key error collection: test.books index: code_1 dup key: { code: "P" }
[
{ _id: 2, title: 'Собачье сердце', code: 'P' },
{ _id: 5, title: 'Шинель' },
{ _id: 9, title: 'Нос' }
]
Первая повесть получила код, на второй сработал уникальный индекс, до третьей очередь не дошла. Команда упала, но откатилась не целиком: в базе осталось половинчатое состояние. Опция sparse: true здесь нужна, чтобы документы без поля code не считались одинаковыми, мы обсуждали это в уроке про типы индексов.
Если нужно «все или ни одного» для нескольких документов, нужна транзакция, ей посвящён следующий урок. В SQL такой вопрос даже не возникает: в PostgreSQL одиночный UPDATE всегда работает внутри транзакции, как мы видели в уроке про транзакции.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.