Справочник по MongoDB · модуль 1 · подключение и первые данные
Глава 1.7
Обновление
документов
Третья операция — изменить то, что уже лежит в базе. Разберём операторы $set, $inc и $unset, разница между «нашлось» и «изменилось», изменение пачки документов одним запросом и то, что делает upsert.
- Категория
- Операции над данными
- Уровень
- Начальный
- Связанные темы
- Вставка, удаление, операторы фильтра
- Зачем это знать
- Менять данные точечно и понимать, что именно изменилось
Как запускать примеры этой главы
docker compose run --rm reset или скрипт из главы 1.0, §6.mongodb-glava-1.7-python.zip, 66 КБmongodb-glava-1.7-ruby.zip, 66 КБmongodb-glava-1.7-go.zip, 73 КБmongodb-glava-1.7-cpp.zip, 71 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.lessons/ch1_7.py — пример вставляется в конец файлаlessons/ch1_7.rb — пример вставляется в конец файлаlessons/ch1_7.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch1_7.cpp — пример внутрь скобок в конце main, функции — выше maincd lessons, затем python ch1_7.pycd lessons, затем python3 ch1_7.py · в Docker: docker compose run --rm python lessons/ch1_7.pycd lessons, затем ruby ch1_7.rb · в Docker: docker compose run --rm ruby lessons/ch1_7.rbcd lessons, затем go run ch1_7.go · в Docker: docker compose run --rm go lessons/ch1_7.gocd lessons, затем g++ -std=c++17 ch1_7.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch1_7.cppsandbox.orders, если их там нет.§1Два документа: кого и что
У любого обновления два аргумента, и оба — документы:
find, — со всеми его правилами.кого$set, $inc, $unset и других.чтоМетодов тоже два: update_one меняет первый подходящий документ, update_many — все подходящие. Разница в одном слове, а последствия разные: если фильтр случайно подошёл к сотне документов, update_one испортит один, а update_many — сто.
Работаем, как и в главе о вставке, в песочнице:
box = client["sandbox"]["products"]
auto box = client["sandbox"]["products"];
box = client.Database("sandbox").Collection("products")
box = client.use("sandbox").database[:products]
§2$set: задать значение
Оператор $set записывает значение в поле. Если поля не было — оно появится, если было — заменится. Остальные поля документа не тронуты.
res = box.update_one( {"_id": "p-101"}, {"$set": {"price": 2690}}) print("matched:", res.matched_count, "modified:", res.modified_count)
auto res = box.update_one( make_document(kvp("_id", "p-101")), make_document(kvp("$set", make_document(kvp("price", 2690))))); std::cout << "matched: " << res->matched_count() << " modified: " << res->modified_count() << std::endl;
res, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-101"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "price", Value: 2690}}}}) fmt.Println("matched:", res.MatchedCount, "modified:", res.ModifiedCount)
res = box.update_one( { "_id" => "p-101" }, { "$set" => { "price" => 2690 } } ) puts "matched: #{res.matched_count} modified: #{res.modified_count}"
db.products.updateOne( { _id: "p-101" }, { $set: { price: 2690 } })
matched: 1 modified: 1
Одним $set можно задать сразу несколько полей, в том числе вложенных — через точку:
orders_box.update_one( {"_id": "o-0001"}, {"$set": { "status": "доставлен", "payment.paid": True, # поле внутри вложенного документа "delivery.days": 2, }})
orders_box.update_one( make_document(kvp("_id", "o-0001")), make_document(kvp("$set", make_document( kvp("status", "доставлен"), kvp("payment.paid", true), // поле внутри вложенного kvp("delivery.days", 2)))));
ordersBox.UpdateOne(ctx, bson.D{{Key: "_id", Value: "o-0001"}}, bson.D{{Key: "$set", Value: bson.D{ {Key: "status", Value: "доставлен"}, {Key: "payment.paid", Value: true}, // вложенное поле {Key: "delivery.days", Value: 2}, }}})
orders_box.update_one( { "_id" => "o-0001" }, { "$set" => { "status" => "доставлен", "payment.paid" => true, # поле внутри вложенного "delivery.days" => 2 } } )
Запись {"$set": {"payment": {"paid": True}}} не поменяет одно поле — она заменит весь вложенный документ payment на новый, в котором есть только paid. Способ оплаты при этом исчезнет. Чтобы изменить одно поле внутри, путь пишут через точку: "payment.paid".
§3matched против modified
Результат обновления возвращает два числа, и означают они разное.
res = box.update_one({"_id": "p-101"}, {"$set": {"price": 2690}}) print("matched:", res.matched_count, "modified:", res.modified_count)
auto res = box.update_one(make_document(kvp("_id", "p-101")), make_document(kvp("$set", make_document(kvp("price", 2690))))); std::cout << "matched: " << res->matched_count() << " modified: " << res->modified_count() << std::endl;
res, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-101"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "price", Value: 2690}}}}) fmt.Println("matched:", res.MatchedCount, "modified:", res.ModifiedCount)
res = box.update_one({ "_id" => "p-101" }, { "$set" => { "price" => 2690 } }) puts "matched: #{res.matched_count} modified: #{res.modified_count}"
matched: 1 modified: 0
matched: 1 modified: 0
matched: 1 modified: 0
matched: 1 modified: 0
| matched | modified | Что это значит |
|---|---|---|
| 1 | 1 | Документ найден и изменён — обычный успешный случай |
| 1 | 0 | Документ найден, но в поле уже лежало это же значение. Запись не понадобилась |
| 0 | 0 | Фильтр не подошёл ни к одному документу. Менять было нечего |
Проверять нужно число совпавших документов (matched), а не изменённых (modified) — как бы это поле ни называлось в вашем драйвере. API, который отвечает «объект не найден» по нулевому modified, ошибается каждый раз, когда пользователь сохранил форму, ничего в ней не изменив.
§4$inc и $unset
$set — не единственный оператор. Два других нужны почти так же часто.
res = box.update_one( {"_id": "p-003"}, {"$inc": {"reviews": 1}, "$unset": {"discount": ""}}) print("matched:", res.matched_count, "modified:", res.modified_count) print(box.find_one({"_id": "p-003"}, {"title": 1, "reviews": 1, "discount": 1}))
auto res = box.update_one( make_document(kvp("_id", "p-003")), make_document( kvp("$inc", make_document(kvp("reviews", 1))), kvp("$unset", make_document(kvp("discount", ""))))); std::cout << "matched: " << res->matched_count() << std::endl;
res, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-003"}}, bson.D{ {Key: "$inc", Value: bson.D{{Key: "reviews", Value: 1}}}, {Key: "$unset", Value: bson.D{{Key: "discount", Value: ""}}}, }) fmt.Println("matched:", res.MatchedCount, "modified:", res.ModifiedCount) var doc bson.D box.FindOne(ctx, bson.D{{Key: "_id", Value: "p-003"}}, options.FindOne().SetProjection(bson.D{ {Key: "title", Value: 1}, {Key: "reviews", Value: 1}, {Key: "discount", Value: 1}}), ).Decode(&doc) fmt.Println(doc)
res = box.update_one( { "_id" => "p-003" }, { "$inc" => { "reviews" => 1 }, "$unset" => { "discount" => "" } } ) puts "matched: #{res.matched_count} modified: #{res.modified_count}" puts box.find({ "_id" => "p-003" }, projection: { "title" => 1, "reviews" => 1, "discount" => 1 }).first.inspect
matched: 1 modified: 1
{'_id': 'p-003', 'title': 'Ноутбук HP Pavilion 14', 'reviews': 32}
matched: 1
matched: 1 modified: 1
{"_id":"p-003","title":"Ноутбук HP Pavilion 14","reviews":{"$numberInt":"32"}}
matched: 1 modified: 1
{"_id"=>"p-003", "title"=>"Ноутбук HP Pavilion 14", "reviews"=>32}
Было 31 отзыв, стало 32; поля discount в документе больше нет — не null, а именно нет. Разница существенная: фильтр { discount: { $exists: false } } найдёт такой товар, а { discount: null } — нет.
Способ «прочитать, прибавить, записать» теряет данные при одновременной работе: два процесса прочитают 31, оба запишут 32, и один отзыв пропадёт. $inc выполняется на сервере одним неделимым действием, поэтому такой потери не будет.
Ещё три оператора обновления: $min и $max записывают значение, только если оно меньше (больше) текущего, $rename переименовывает поле. Операторы для массивов — $push, $pull, $addToSet — разберём после главы о массивах в фильтрах.
§5update_many: пачкой
Одинаковая правка для всех подходящих документов — это update_many. Форма та же, меняется только метод.
res = box.update_many( {"category": "аксессуары"}, {"$set": {"free_shipping": True}}) print("matched:", res.matched_count, "modified:", res.modified_count)
auto res = box.update_many( make_document(kvp("category", "аксессуары")), make_document(kvp("$set", make_document(kvp("free_shipping", true))))); std::cout << "matched: " << res->matched_count() << " modified: " << res->modified_count() << std::endl;
res, _ := box.UpdateMany(ctx, bson.D{{Key: "category", Value: "аксессуары"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "free_shipping", Value: true}}}}) fmt.Println("matched:", res.MatchedCount, "modified:", res.ModifiedCount)
res = box.update_many( { "category" => "аксессуары" }, { "$set" => { "free_shipping" => true } } ) puts "matched: #{res.matched_count} modified: #{res.modified_count}"
matched: 6 modified: 6
matched: 6 modified: 6
matched: 6 modified: 6
matched: 6 modified: 6
Шесть документов: три исходных аксессуара, коврик и две позиции, добавленные в главе о вставке. Поле free_shipping появилось у них и только у них — остальные товары не изменились.
Перед любым update_many выполняют count_documents с тем же фильтром. Число показывает, скольких документов коснётся правка, до того как она произойдёт. Если вместо шести получилось шестьсот, фильтр написан не так, как задумано.
query = {"category": "аксессуары"}
print("будет изменено:", box.count_documents(query)) # 6
box.update_many(query, {"$set": {"free_shipping": True}})
auto query = make_document(kvp("category", "аксессуары")); std::cout << "будет изменено: " << box.count_documents(query.view()) << std::endl; // 6 box.update_many(query.view(), make_document(kvp("$set", make_document(kvp("free_shipping", true)))));
query := bson.D{{Key: "category", Value: "аксессуары"}}
count, _ := box.CountDocuments(ctx, query)
fmt.Println("будет изменено:", count) // 6
box.UpdateMany(ctx, query,
bson.D{{Key: "$set", Value: bson.D{{Key: "free_shipping", Value: true}}}})
query = { "category" => "аксессуары" }
puts "будет изменено: #{box.count_documents(query)}" # 6
box.update_many(query, { "$set" => { "free_shipping" => true } })
§6upsert: обновить или создать
Параметр upsert=True означает: если документ не нашёлся, создать его. Один запрос вместо связки «проверить, потом вставить».
res = box.update_one( {"_id": "p-999"}, {"$set": {"title": "Товар из upsert", "price": 100}}, upsert=True) print("matched:", res.matched_count, "modified:", res.modified_count, "upserted_id:", res.upserted_id)
mongocxx::options::update options; options.upsert(true); auto res = box.update_one( make_document(kvp("_id", "p-999")), make_document(kvp("$set", make_document( kvp("title", "Товар из upsert"), kvp("price", 100)))), options); std::cout << "matched: " << res->matched_count() << " upserted_id: " << res->upserted_id()->get_string().value << std::endl;
res, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-999"}}, bson.D{{Key: "$set", Value: bson.D{ {Key: "title", Value: "Товар из upsert"}, {Key: "price", Value: 100}}}}, options.UpdateOne().SetUpsert(true)) fmt.Println("matched:", res.MatchedCount, "modified:", res.ModifiedCount, "upserted_id:", res.UpsertedID)
res = box.update_one( { "_id" => "p-999" }, { "$set" => { "title" => "Товар из upsert", "price" => 100 } }, upsert: true ) puts "matched: #{res.matched_count} modified: #{res.modified_count} " \ "upserted_id: #{res.upserted_id}"
matched: 0 modified: 0 upserted_id: p-999
matched: 0 upserted_id: p-999
matched: 0 modified: 0 upserted_id: p-999
matched: 0 modified: 0 upserted_id: p-999
Ноль совпадений, ноль изменений — и новый документ. Ключ _id взят из фильтра: при вставке через upsert сервер собирает документ из полей фильтра с точным равенством и полей из $set.
Применение: счётчики и агрегаты, которые обновляются при каждом событии. Например, счётчик просмотров товара за день — не нужно проверять, есть ли уже запись за сегодня:
stats.update_one( {"product_id": "p-003", "date": today}, {"$inc": {"views": 1}}, upsert=True) print(stats.find_one({"product_id": "p-003", "date": today}, {"_id": 0}))
mongocxx::options::update options; options.upsert(true); stats.update_one( make_document(kvp("product_id", "p-003"), kvp("date", today)), make_document(kvp("$inc", make_document(kvp("views", 1)))), options); mongocxx::options::find only; only.projection(make_document(kvp("_id", 0))); auto doc = stats.find_one(make_document(kvp("product_id", "p-003"), kvp("date", today)), only); std::cout << bsoncxx::to_json(*doc) << std::endl;
stats.UpdateOne(ctx, bson.D{{Key: "product_id", Value: "p-003"}, {Key: "date", Value: today}}, bson.D{{Key: "$inc", Value: bson.D{{Key: "views", Value: 1}}}}, options.UpdateOne().SetUpsert(true)) var doc bson.D stats.FindOne(ctx, bson.D{{Key: "product_id", Value: "p-003"}, {Key: "date", Value: today}}, options.FindOne().SetProjection(bson.D{{Key: "_id", Value: 0}}), ).Decode(&doc) fmt.Println(doc)
stats.update_one( { "product_id" => "p-003", "date" => today }, { "$inc" => { "views" => 1 } }, upsert: true ) puts stats.find({ "product_id" => "p-003", "date" => today }, projection: { "_id" => 0 }).first.inspect
{'product_id': 'p-003', 'date': '2026-09-14', 'views': 1}
{ "product_id" : "p-003", "date" : "2026-09-14", "views" : 1 }
{"date":"2026-09-14","product_id":"p-003","views":{"$numberInt":"1"}}
{"date"=>"2026-09-14", "product_id"=>"p-003", "views"=>1}
§7Обновление без операторов
Если забыть $set и передать просто документ, драйвер не пропустит запрос дальше — во всех языках курса, кроме Ruby:
ValueError: update only works with $ operators
terminate called after throwing an instance of 'mongocxx::v_noabi::logic_error' what(): Invalid key 'price': update only works with $ operators and pipelines: generic server error
update document must contain key beginning with '$'
WARN -- : Invalid update document provided. Updates documents must only contain only atomic modifiers. The "price" key is invalid.
matched: 1 modified: 1
{"_id"=>"p-001", "price"=>100}
Драйвер Ruby только пишет предупреждение в журнал и отправляет запрос. Сервер понимает документ без операторов как замену: у p-001 остались только _id и price, название, категория и склады исчезли. Ошибки нет, modified равен единице — поэтому предупреждение Invalid update document в выводе нужно читать, а не пропускать.
Метод replace_one при этом заменяет документ целиком. Все поля, которых нет в новом документе, исчезают; сохраняется только _id.
| Метод | Что происходит с полями, которых нет в запросе | Когда применяют |
|---|---|---|
update_one с $set | Остаются как были | Почти всегда: точечная правка |
replace_one | Удаляются | Когда клиент прислал документ целиком и он — источник истины |
Ошибка «сохранили форму — половина полей пропала» — это почти всегда replace_one там, где нужен был $set.
§8Правка документа в Compass
В Compass документ правится прямо в списке: наведите на него курсор и нажмите карандаш. Поля меняются на месте, можно добавить новое или удалить существующее.
sandbox.products после $inc и $unset: отзывов стало 32, поля discount в документе больше нет — не null, а именно нетТот же документ можно поправить и мышью — прямо в списке:
$set. Рядом — Cancel, чтобы отказаться.шаг 3Правка мышью подходит для разбора и для единичного исправления; всё, что делается больше одного раза, должно быть запросом в коде.
Результат запроса из §4, где мы прибавили отзыв и сняли скидку:
sandbox.products после $inc и $unset: reviews: 32, а строки discount в документе больше нетПоля discount на кадре нет вовсе: оно не стало пустым и не превратилось в null, а исчезло из документа. Так работает $unset; поэтому такой товар найдётся по фильтру {"discount": {"$exists": False}}.
§9Частые ошибки
| Что написано | Что происходит | Как правильно |
|---|---|---|
update_one({...}, {"price": 100}) | ValueError: update only works with $ operators | {"$set": {"price": 100}} |
{"$set": {"payment": {"paid": True}}} | Вложенный документ заменён целиком, остальные его поля потеряны | {"$set": {"payment.paid": True}} |
Проверка результата по modified_count | «Не найдено» при сохранении без изменений | Проверять matched_count |
update_many({}, {...}) | Изменены все документы коллекции | Пустой фильтр — только осознанно; сначала count_documents |
| Прочитать, прибавить в коде, записать | При одновременной работе изменения теряются | $inc — сервер выполнит это неделимо |
| Что написано | Что происходит | Как правильно |
|---|---|---|
| Обновление без оператора | Invalid key 'price': update only works with $ operators and pipelines | Оборачивать в kvp("$set", …) |
kvp("$set", make_document(kvp("payment", …))) | Вложенный документ заменён целиком, остальные его поля потеряны | Путь через точку: kvp("payment.paid", true) |
Проверка результата по modified_count() | «Не найдено» при сохранении без изменений | Проверять matched_count() |
update_many(make_document(), …) | Изменены все документы коллекции | Пустой фильтр — только осознанно; сначала count_documents |
| Прочитать, прибавить в коде, записать | При одновременной работе изменения теряются | $inc — сервер выполнит это неделимо |
| Что написано | Что происходит | Как правильно |
|---|---|---|
UpdateOne(ctx, filter, bson.D{{"price", 100}}) | update document must contain key beginning with '$' | bson.D{{"$set", bson.D{{"price", 100}}}} |
$set со вложенным документом целиком | Вложенный документ заменён, остальные его поля потеряны | Путь через точку: {Key: "payment.paid", Value: true} |
Проверка результата по ModifiedCount | «Не найдено» при сохранении без изменений | Проверять MatchedCount |
UpdateMany(ctx, bson.D{}, …) | Изменены все документы коллекции | Пустой фильтр — только осознанно; сначала CountDocuments |
| Прочитать, прибавить в коде, записать | При одновременной работе изменения теряются | $inc — сервер выполнит это неделимо |
| Что написано | Что происходит | Как правильно |
|---|---|---|
update_one(filter, { "price" => 100 }) | WARN: Invalid update document provided. Updates documents must contain only atomic modifiers | { "$set" => { "price" => 100 } } |
$set со вложенным документом целиком | Вложенный документ заменён, остальные его поля потеряны | Путь через точку: "payment.paid" => true |
Проверка результата по modified_count | «Не найдено» при сохранении без изменений | Проверять matched_count |
update_many({}, …) | Изменены все документы коллекции | Пустой фильтр — только осознанно; сначала count_documents |
| Прочитать, прибавить в коде, записать | При одновременной работе изменения теряются | $inc — сервер выполнит это неделимо |
§10Проверьте себя
Результат: matched: 1, modified: 0. Что произошло?
Документ нашёлся, но менять было нечего: в поле уже лежало то же значение. Запрос при этом выполнен успешно.
Чем {"$set": {"delivery.city": "Москва"}} отличается от {"$set": {"delivery": {"city": "Москва"}}}?
Первый меняет одно поле внутри delivery, остальные его поля сохраняются. Второй заменяет весь вложенный документ — type и days будут потеряны.
Почему счётчик просмотров стоит увеличивать через $inc, а не читать и записывать из кода?
Потому что между чтением и записью значение может изменить другой процесс, и одно из увеличений потеряется. $inc выполняется на сервере одним неделимым действием.
Что делает upsert=True, если документ не найден, и откуда берётся его _id?
Создаёт новый документ из полей фильтра с точным равенством и полей обновления. Ключ берётся из фильтра, если он там задан; иначе сервер сгенерирует ObjectId. Номер созданного документа возвращается в upserted_id.
Как удалить поле из документа и чем это отличается от записи None?
Удаляет $unset: {"$unset": {"discount": ""}}. После него поля в документе нет — его найдёт {"discount": {"$exists": False}}. Запись None оставляет поле со значением null, и это разные состояния.
§11Шпаргалка
col.update_one(
{"_id": "p-101"},
{"$set": {"price": 2690}})
{"$inc": {"reviews": 1}}
{"$unset": {"discount": ""}}
col.update_many(
{"category": "аксессуары"},
{"$set": {...}})
col.update_one(query, upd,
upsert=True)
res.upserted_id
col.update_one(
make_document(kvp("_id", "p-101")),
make_document(kvp("$set",
make_document(kvp("price", 2690)))));
kvp("$inc", make_document(kvp("reviews", 1)))
kvp("$unset", make_document(kvp("discount", "")))
col.update_many(filter, update);
res->matched_count()
mongocxx::options::update options;
options.upsert(true);
col.update_one(filter, update, options);
col.UpdateOne(ctx,
bson.D{{Key: "_id", Value: "p-101"}},
bson.D{{Key: "$set", Value: bson.D{
{Key: "price", Value: 2690}}}})
{Key: "$inc", Value: bson.D{{Key: "reviews", Value: 1}}}
{Key: "$unset", Value: bson.D{{Key: "discount", Value: ""}}}
res, _ := col.UpdateMany(ctx, filter, update)
res.MatchedCount
col.UpdateOne(ctx, filter, update,
options.UpdateOne().SetUpsert(true))
res.UpsertedID
col.update_one(
{ "_id" => "p-101" },
{ "$set" => { "price" => 2690 } })
{ "$inc" => { "reviews" => 1 } }
{ "$unset" => { "discount" => "" } }
res = col.update_many(filter, update)
res.matched_count
col.update_one(query, upd, upsert: true)
res.upserted_id
Итог главы
Обновление — это два документа: фильтр «кого» и операторы «что». $set задаёт значение, $inc прибавляет неделимо, $unset убирает поле. update_one меняет первый подходящий документ, update_many — все, поэтому фильтр стоит сначала проверить счётчиком. Проверять результат нужно по matched_count: ноль изменений не значит «не найдено». Осталась последняя из четырёх операций — удаление.