Справочник по MongoDB · модуль 1 · подключение и первые данные
Глава 1.3
Вставка
документов
Первая из четырёх операций над данными: положить документ в коллекцию. Разберём insert_one и insert_many, что возвращают эти методы, откуда берётся _id и что произойдёт, если в середине пачки попадётся плохой документ.
- Категория
- Операции над данными
- Уровень
- Начальный
- Связанные темы
- Документ и BSON, обновление, удаление
- Зачем это знать
- Класть данные так, чтобы их потом можно было найти и не задвоить
Как запускать примеры этой главы
docker compose run --rm reset или скрипт из главы 1.0, §6.mongodb-glava-1.3-python.zip, 60 КБmongodb-glava-1.3-ruby.zip, 60 КБmongodb-glava-1.3-go.zip, 65 КБmongodb-glava-1.3-cpp.zip, 63 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.lessons/ch1_3.py — пример вставляется в конец файлаlessons/ch1_3.rb — пример вставляется в конец файлаlessons/ch1_3.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch1_3.cpp — пример внутрь скобок в конце main, функции — выше maincd lessons, затем python ch1_3.pycd lessons, затем python3 ch1_3.py · в Docker: docker compose run --rm python lessons/ch1_3.pycd lessons, затем ruby ch1_3.rb · в Docker: docker compose run --rm ruby lessons/ch1_3.rbcd lessons, затем go run ch1_3.go · в Docker: docker compose run --rm go lessons/ch1_3.gocd lessons, затем g++ -std=c++17 ch1_3.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch1_3.cpp_id у вас будут другими: их выдаёт сервер в момент вставки.§1Песочница: где мы работаем
Начиная с этой главы мы меняем данные, поэтому работать будем в отдельной базе sandbox. В ней лежит копия коллекции товаров — те же 21 документ, что и в shop.products.
from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") box = client["sandbox"]["products"] # здесь можно ломать print(box.count_documents({})) # 21
// mongocxx::instance уже создан в заготовке: второй экземпляр программа не переживёт mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}}; auto box = client["sandbox"]["products"]; // здесь можно ломать std::cout << box.count_documents(make_document()); // 21
client, _ := mongo.Connect(options.Client(). ApplyURI("mongodb://localhost:27017")) box := client.Database("sandbox").Collection("products") // можно ломать count, _ := box.CountDocuments(ctx, bson.D{}) fmt.Println(count) // 21
require "mongo" client = Mongo::Client.new("mongodb://localhost:27017/") box = client.use("sandbox").database[:products] # здесь можно ломать puts box.count_documents({}) # 21
sandbox.products — та же 21 позиция, что и в shop.products, но эту копию можно менятьПравило работы: эталонные данные не трогаем. В проекте ту же роль играет отдельная база разработчика или контур тестирования — чтобы эксперимент не испортил то, на что смотрят остальные. Разъехавшаяся песочница восстанавливается одной командой из репозитория практик.
§2insert_one: один документ
Метод insert_one принимает обычный словарь Python и кладёт его в коллекцию как документ.
result = box.insert_one({ "sku": "SKU-AC-022", "title": "Коврик для мыши XL", "brand": "OEM", "category": "аксессуары", "price": 1290, })
auto result = box.insert_one(make_document( kvp("sku", "SKU-AC-022"), kvp("title", "Коврик для мыши XL"), kvp("brand", "OEM"), kvp("category", "аксессуары"), kvp("price", 1290)));
res, err := box.InsertOne(ctx, bson.D{ {Key: "sku", Value: "SKU-AC-022"}, {Key: "title", Value: "Коврик для мыши XL"}, {Key: "brand", Value: "OEM"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 1290}, })
result = box.insert_one({ "sku" => "SKU-AC-022", "title" => "Коврик для мыши XL", "brand" => "OEM", "category" => "аксессуары", "price" => 1290 })
db.products.insertOne({ sku: "SKU-AC-022", title: "Коврик для мыши XL", brand: "OEM", category: "аксессуары", price: 1290 })
Поля _id мы не указали — и сервер добавил его сам. Ни коллекцию, ни базу заранее создавать не пришлось: если бы sandbox.products не существовала, она появилась бы в этот момент.
§3Что возвращает вставка
Возвращается не документ, а результат операции — объект с двумя полезными полями.
print("подтверждено:", result.acknowledged) print("присвоенный _id:", result.inserted_id) print("тип:", type(result.inserted_id).__name__)
// результат — optional: пусто, если запись не подтверждена std::cout << "подтверждено: " << (result ? "да" : "нет") << std::endl; std::cout << "присвоенный _id: " << result->inserted_id().get_oid().value.to_string() << std::endl;
// подтверждение в Go — это отсутствие ошибки if err != nil { log.Fatal(err) } fmt.Println("присвоенный _id:", res.InsertedID)
puts "подтверждено: #{result.acknowledged?}" puts "присвоенный _id: #{result.inserted_id}" puts "тип: #{result.inserted_id.class}"
подтверждено: True присвоенный _id: 6aa7242b708f072aeb471236 тип: ObjectId
подтверждено: да присвоенный _id: 6aa72484d8d50a91a109a7f1
присвоенный _id: ObjectID("6aa72439598d2ddc60f4355e")
подтверждено: true присвоенный _id: 6aa7760de597bcf16516ccbe тип: BSON::ObjectId
False бывает только при особых настройках, когда программа сознательно не ждёт подтверждения.подтверждениеЧастая ошибка новичка — ожидать, что insert_one вернёт сам документ. Он его не возвращает: чтобы увидеть, что получилось, документ нужно прочитать обратно по ключу.
§4insert_many: пачка документов
Документы по одному в цикле не кладут: каждый вызов идёт до сервера и обратно. Для пачки есть insert_many, отправляющий всё одним обращением.
many = box.insert_many([ {"_id": "p-101", "title": "Подставка для ноутбука", "category": "аксессуары", "price": 2490}, {"_id": "p-102", "title": "Чехол для планшета", "category": "аксессуары", "price": 1890}, ]) print("вставлено:", len(many.inserted_ids), "→", many.inserted_ids) print("документов в песочнице:", box.count_documents({}))
std::vector<bsoncxx::document::value> batch; batch.push_back(make_document(kvp("_id", "p-101"), kvp("title", "Подставка для ноутбука"), kvp("category", "аксессуары"), kvp("price", 2490))); batch.push_back(make_document(kvp("_id", "p-102"), kvp("title", "Чехол для планшета"), kvp("category", "аксессуары"), kvp("price", 1890))); auto many = box.insert_many(batch); std::cout << "вставлено: " << many->inserted_count() << std::endl; std::cout << "документов в песочнице: " << box.count_documents(make_document()) << std::endl;
many, err := box.InsertMany(ctx, []any{ bson.D{{Key: "_id", Value: "p-101"}, {Key: "title", Value: "Подставка для ноутбука"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 2490}}, bson.D{{Key: "_id", Value: "p-102"}, {Key: "title", Value: "Чехол для планшета"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 1890}}, }) if err != nil { log.Fatal(err) } fmt.Println("вставлено:", len(many.InsertedIDs), "→", many.InsertedIDs) count, _ := box.CountDocuments(ctx, bson.D{}) fmt.Println("документов в песочнице:", count)
many = box.insert_many([ { "_id" => "p-101", "title" => "Подставка для ноутбука", "category" => "аксессуары", "price" => 2490 }, { "_id" => "p-102", "title" => "Чехол для планшета", "category" => "аксессуары", "price" => 1890 } ]) puts "вставлено: #{many.inserted_count} → #{many.inserted_ids}" puts "документов в песочнице: #{box.count_documents({})}"
вставлено: 2 → ['p-101', 'p-102'] документов в песочнице: 24
вставлено: 2 документов в песочнице: 24
вставлено: 2 → [p-101 p-102] документов в песочнице: 24
вставлено: 2 → ["p-101", "p-102"] документов в песочнице: 24
Двадцать четыре: к двадцати одному исходному товару добавился коврик из §2 и две новые позиции. Поле inserted_ids — список ключей в том же порядке, в каком шли документы.
Тысяча вызовов insert_one — это тысяча обращений к серверу, и основное время занимает дорога до него. Один insert_many на тысячу документов отправляет их пачками и работает на порядок быстрее. Так устроена загрузка данных из файла, которой восстанавливается стенд.
§5Свой _id и его уникальность
В примере выше мы задали ключи сами — "p-101" и "p-102". Так делают, когда у объекта уже есть естественный идентификатор: артикул товара, номер заказа, логин пользователя. Тогда за уникальность отвечает программа, а сервер проверяет её при каждой вставке.
from pymongo.errors import DuplicateKeyError try: box.insert_one({"_id": "p-101", "title": "Ещё одна подставка"}) except DuplicateKeyError as error: print(type(error).__name__) print(error.details["errmsg"])
#include <mongocxx/exception/bulk_write_exception.hpp> try { box.insert_one(make_document( kvp("_id", "p-101"), kvp("title", "Ещё одна подставка"))); } catch (const mongocxx::bulk_write_exception& error) { std::cout << "код ошибки: " << error.code().value() << std::endl; std::cout << error.what() << std::endl; }
_, err = box.InsertOne(ctx, bson.D{ {Key: "_id", Value: "p-101"}, {Key: "title", Value: "Ещё одна подставка"}, }) if mongo.IsDuplicateKeyError(err) { fmt.Println("дубликат ключа:", err) }
begin box.insert_one({ "_id" => "p-101", "title" => "Ещё одна подставка" }) rescue Mongo::Error::OperationFailure => error puts error.class puts error.message end
DuplicateKeyError
E11000 duplicate key error collection: sandbox.products index: _id_ dup key: { _id: "p-101" }
код ошибки: 11000
E11000 duplicate key error collection: sandbox.products index: _id_ dup key: { _id: "p-101" }: generic server error
дубликат ключа: write exception: write errors: [E11000 duplicate key error collection: sandbox.products index: _id_ dup key: { _id: "p-101" }]
Mongo::Error::OperationFailure
[11000]: E11000 duplicate key error collection: sandbox.products index: _id_ dup key: { _id: "p-101" } (on localhost:27017, legacy retry, attempt 1)
Код E11000 означает, что сервер не дал записать второй документ с тем же ключом. Читается сообщение целиком: коллекция sandbox.products, индекс _id_, значение "p-101". Тот же код вы увидите и при нарушении любого другого уникального индекса — например, если запретите повторяющиеся артикулы.
§6Порядок вставки: ordered
Поведение пачки, в которой десятый документ из ста оказался с повторяющимся ключом, задаётся параметром ordered. По умолчанию он равен True.
| Режим | Поведение | Когда выбирают |
|---|---|---|
ordered=Trueпо умолчанию |
Документы идут по порядку. На первой ошибке вставка прекращается: первые девять записаны, десятый и все следующие — нет | Когда порядок важен или когда лучше остановиться и разобраться |
ordered=False |
Сервер пытается вставить все документы, плохие пропускает, а в конце сообщает обо всех ошибках сразу | Загрузка данных из внешнего источника: пусть войдёт всё, что можно, а сбойные строки разберём потом |
from pymongo.errors import BulkWriteError batch = [ {"_id": "p-103", "title": "Кабель HDMI 2 м", "category": "аксессуары", "price": 690}, {"_id": "p-101", "title": "Подставка — повтор"}, # такой _id уже есть {"_id": "p-104", "title": "Салфетки для экрана", "category": "аксессуары", "price": 390}, ] try: box.insert_many(batch, ordered=False) except BulkWriteError as error: failed = error.details["writeErrors"] print(f"не вставлено: {len(failed)} из {len(batch)}") print("документов в песочнице:", box.count_documents({}))
std::vector<bsoncxx::document::value> batch; batch.push_back(make_document(kvp("_id", "p-103"), kvp("title", "Кабель HDMI 2 м"), kvp("category", "аксессуары"), kvp("price", 690))); batch.push_back(make_document(kvp("_id", "p-101"), kvp("title", "Подставка — повтор"))); // такой _id уже есть batch.push_back(make_document(kvp("_id", "p-104"), kvp("title", "Салфетки для экрана"), kvp("category", "аксессуары"), kvp("price", 390))); mongocxx::options::insert options; options.ordered(false); try { box.insert_many(batch, options); } catch (const mongocxx::bulk_write_exception& error) { std::cout << "часть документов не прошла: " << error.what() << std::endl; } std::cout << "документов в песочнице: " << box.count_documents(make_document()) << std::endl;
batch := []any{ bson.D{{Key: "_id", Value: "p-103"}, {Key: "title", Value: "Кабель HDMI 2 м"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 690}}, bson.D{{Key: "_id", Value: "p-101"}, {Key: "title", Value: "Подставка — повтор"}}, // такой _id уже есть bson.D{{Key: "_id", Value: "p-104"}, {Key: "title", Value: "Салфетки для экрана"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 390}}, } options := options.InsertMany().SetOrdered(false) _, err := box.InsertMany(ctx, batch, options) var bulkErr mongo.BulkWriteException if errors.As(err, &bulkErr) { fmt.Printf("не вставлено: %d из %d\n", len(bulkErr.WriteErrors), len(batch)) } count, _ := box.CountDocuments(ctx, bson.D{}) fmt.Println("документов в песочнице:", count)
batch = [
{ "_id" => "p-103", "title" => "Кабель HDMI 2 м", "category" => "аксессуары", "price" => 690 },
{ "_id" => "p-101", "title" => "Подставка — повтор" }, # такой _id уже есть
{ "_id" => "p-104", "title" => "Салфетки для экрана", "category" => "аксессуары", "price" => 390 },
]
begin
box.insert_many(batch, ordered: false)
rescue Mongo::Error::BulkWriteError => error
failed = error.result["writeErrors"]
puts "не вставлено: #{failed.size} из #{batch.size}"
end
puts "документов в песочнице: #{box.count_documents({})}"
не вставлено: 1 из 3 документов в песочнице: 26
часть документов не прошла: E11000 duplicate key error collection: sandbox.products index: _id_ dup key: { _id: "p-101" }: generic server error
документов в песочнице: 26
не вставлено: 1 из 3 документов в песочнице: 26
не вставлено: 1 из 3 документов в песочнице: 26
В пачке три документа, и второй повторяет _id товара из §4. Два записаны, один отклонён — в песочнице стало на два документа больше. Исключение означает, что записались не все документы, — часть их уже лежит в коллекции. Это учитывают при повторном запуске загрузчика.
§7Вставка руками в Compass
Тот же документ можно положить мышью: в коллекции есть кнопка ADD DATA → Insert document. Compass откроет окно, в котором документ пишется как JSON.
_id уже заполнено новым ObjectId — его можно оставить или заменить своим.шаг 2Проверка JSON до отправки — тот же контроль, что и в строке фильтра: база не получит заведомо неразбираемый документ. После вставки счётчик коллекции увеличивается на единицу, а новый документ появляется в списке — с _id типа ObjectId, если вы не задали ключ сами.
§8Загрузка файла целиком
Данные редко вводят руками. Обычно это файл — выгрузка из другой системы, экспорт коллекции, набор для стенда. Способов два.
mongoimport --uri "mongodb://localhost:27017" \
--db sandbox --collection products \
--file stend/seed/sandbox.products.json --jsonArray --drop
from bson import json_util # понимает {"$date": …} в файле with open("../stend/seed/sandbox.products.json", encoding="utf-8") as handle: docs = json_util.loads(handle.read()) box.delete_many({}) # повтор без задвоения box.insert_many(docs) print("документов в песочнице:", box.count_documents({}))
std::ifstream file{"../stend/seed/sandbox.products.json"};
std::stringstream buffer;
buffer << file.rdbuf();
auto array = bsoncxx::from_json(buffer.str());
std::vector<bsoncxx::document::value> docs;
for (const auto& item : array.view())
docs.push_back(bsoncxx::document::value{item.get_document().value});
box.delete_many(make_document()); // повтор без задвоения
box.insert_many(docs);
std::cout << "документов в песочнице: " << box.count_documents(make_document()) << std::endl;
raw, _ := os.ReadFile("../stend/seed/sandbox.products.json") var docs []any bson.UnmarshalExtJSON(raw, false, &docs) box.DeleteMany(ctx, bson.D{}) // повтор без задвоения box.InsertMany(ctx, docs) count, _ := box.CountDocuments(ctx, bson.D{}) fmt.Println("документов в песочнице:", count)
# BSON::ExtJSON понимает {"$date": …} в файле, обычный JSON.parse — нет docs = BSON::ExtJSON.parse(File.read("../stend/seed/sandbox.products.json")) box.delete_many({}) # повтор без задвоения box.insert_many(docs) puts "документов в песочнице: #{box.count_documents({})}"
документов в песочнице: 21
документов в песочнице: 21
документов в песочнице: 21
документов в песочнице: 21
Путь в коде начинается с ../, потому что пример запускается из папки lessons, а файл лежит в stend/seed того же репозитория. Файл записан в Extended JSON, поэтому читается не обычным разбором JSON, а функцией драйвера: иначе даты из {"$date": …} попадут в базу вложенными документами.
Ключ --drop у mongoimport и delete_many({}) в коде делают одно и то же: очищают коллекцию перед загрузкой. Без этого повторный запуск удвоит данные. Обычно числа в работах расходятся именно по этой причине: загрузку выполнили дважды.
В Compass тому же соответствует ADD DATA → Import JSON or CSV file. Файл может быть массивом документов или потоком объектов по одному на строку — Compass поймёт оба формата.
§9Частые ошибки
| Что написано | Что происходит | Как правильно |
|---|---|---|
insert_one([...]) со списком | TypeError: document must be an instance of dict | Список — это insert_many |
doc = insert_one({...}) | В переменной результат операции, а не документ | Читать обратно: find_one({"_id": result.inserted_id}) |
| Повторный запуск загрузчика | Документов вдвое больше, числа в отчётах не сходятся | Очищать коллекцию или задавать свой _id, чтобы повтор упёрся в E11000 |
insert_one в цикле на тысячу документов | Работает, но медленно: каждый вызов — обращение к серверу | Собрать список и вставить одним insert_many |
| Тот же словарь вставлен дважды | DuplicateKeyError: драйвер дописал _id в исходный словарь | Копировать словарь или создавать новый для каждой записи |
| Что написано | Что происходит | Как правильно |
|---|---|---|
insert_one с уже использованным document::value | Компилируется, но документ уходит в перемещённом состоянии | Собирать документ заново или передавать view() |
| Результат не проверен на пустоту | Разыменование пустого optional — неопределённое поведение | if (result) result->inserted_id() |
| Повторный запуск загрузчика | Документов вдвое больше, числа в отчётах не сходятся | Очищать коллекцию или задавать свой _id, чтобы повтор упёрся в E11000 |
insert_one в цикле на тысячу документов | Работает, но медленно: каждый вызов — обращение к серверу | Собрать std::vector<document::value> и вставить insert_many |
| Исключение не поймано | Программа падает на первом дубликате ключа | catch (const mongocxx::bulk_write_exception&) и разбор code().value() |
| Что написано | Что происходит | Как правильно |
|---|---|---|
InsertMany с одним документом вместо среза | invalid documents: must provide a non-empty slice | Первым аргументом идёт срез, даже если документ один |
| Ошибка вставки не проверена | Документ не записан, программа идёт дальше | if err != nil сразу после вызова |
| Повторный запуск загрузчика | Документов вдвое больше, числа в отчётах не сходятся | Очищать коллекцию или задавать свой _id, чтобы повтор упёрся в E11000 |
InsertOne в цикле на тысячу документов | Работает, но медленно: каждый вызов — обращение к серверу | Собрать срез и вставить одним InsertMany |
err != nil считается полным провалом | При ordered=false часть документов уже записана | Разобрать mongo.BulkWriteException и посмотреть WriteErrors |
| Что написано | Что происходит | Как правильно |
|---|---|---|
insert_one([...]) со списком | Список записывается как один документ или вызывает ошибку драйвера | Список — это insert_many |
doc = insert_one({...}) | В переменной результат операции, а не документ | Читать обратно: col.find({ "_id" => result.inserted_id }).first |
| Повторный запуск загрузчика | Документов вдвое больше, числа в отчётах не сходятся | Очищать коллекцию или задавать свой _id, чтобы повтор упёрся в E11000 |
rescue Mongo::Error::OperationFailure на пачке | Исключение проходит мимо: частичный отказ — это BulkWriteError | rescue Mongo::Error::BulkWriteError, список неудач в error.result["writeErrors"] |
insert_one в цикле на тысячу документов | Работает, но медленно: каждый вызов — обращение к серверу | Собрать массив и вставить одним insert_many |
Последняя строка таблицы требует пояснения. PyMongo дописывает сгенерированный _id прямо в переданный словарь. Поэтому for _ in range(3): box.insert_one(template) вставит один документ, а на втором обороте упадёт с DuplicateKeyError: в template уже лежит _id от первой вставки.
§10Проверьте себя
Мы вставили документ в коллекцию, которой не существовало. Что произошло?
Коллекция создалась сама в момент вставки — а если не существовало и базы, то и база. Отдельной команды на создание не требуется.
Что вернёт insert_one и как получить сам вставленный документ?
Вернёт объект результата с полями acknowledged и inserted_id. Документ читается обратно: box.find_one({"_id": result.inserted_id}).
В пачке из 100 документов десятый нарушает уникальность. Сколько документов окажется в базе при ordered=True и при ordered=False?
При ordered=True — девять: вставка останавливается на первой ошибке. При ordered=False — девяносто девять: сервер пропустит сбойный документ и вставит остальные, а в исключении перечислит все неудачи.
Загрузчик запустили дважды, и товаров стало 42 вместо 21. Что делать и как не повторить?
Очистить коллекцию и загрузить заново: delete_many({}) или mongoimport --drop. Чтобы повтор был безопасным, задавайте документам собственный _id — тогда вторая загрузка остановится на E11000 и копий не создаст.
Зачем на стенде курса отдельная база sandbox?
Чтобы упражнения не меняли данные, на которых построены примеры книги и ответы к практикам. В проекте у этого приёма то же название и та же цель: отдельный контур для экспериментов.
§11Шпаргалка
res = col.insert_one({...})
res.inserted_id
# mongosh: insertOne({...})
res = col.insert_many([...])
res.inserted_ids
# insertMany([...])
col.insert_many(
docs, ordered=False)
# ловить BulkWriteError
col.delete_many({})
col.insert_many(docs)
# или mongoimport --drop
auto res = col.insert_one(
make_document(kvp("price", 1290)));
res->inserted_id()
std::vector<bsoncxx::document::value> batch;
auto many = col.insert_many(batch);
many->inserted_count()
mongocxx::options::insert options;
options.ordered(false);
// ловить bulk_write_exception
col.delete_many(make_document());
col.insert_many(docs);
res, err := col.InsertOne(ctx,
bson.D{{Key: "price", Value: 1290}})
res.InsertedID
many, err := col.InsertMany(ctx, docs)
many.InsertedIDs
col.InsertMany(ctx, docs,
options.InsertMany().SetOrdered(false))
// errors.As(&mongo.BulkWriteException{})
col.DeleteMany(ctx, bson.D{})
col.InsertMany(ctx, docs)
res = col.insert_one({ ... })
res.inserted_id
res = col.insert_many([...])
res.inserted_ids
res.inserted_count
col.insert_many(docs, ordered: false)
# rescue Mongo::Error::OperationFailure
col.delete_many({})
col.insert_many(docs)
Итог главы
insert_one кладёт один документ и возвращает его ключ, insert_many — список документов одним обращением к серверу. База и коллекция создаются сами при первой записи, а _id либо задаёте вы, либо генерирует сервер. Уникальность ключа проверяется всегда: повтор даёт ошибку E11000 — это защита от задвоения. Данные в базе есть — в следующей главе научимся их доставать.