Вставка документов в MongoDB: insertOne и insertMany
✓ Все примеры выполнены на MongoDB 8.2.6, драйвер mongodb 7.5.0, Node.js v22.23.1 — июль 2026
Содержание
Вставка — первая операция, с которой начинают работу с MongoDB, и самая простая из всех. Разберём её целиком, включая то, о чём обычно узнают уже в продакшене.
Весь код выполнен на живом сервере MongoDB 8.2.6.
insertOne: один документ
import { MongoClient } from 'mongodb';
const client = new MongoClient(uri);
await client.connect();
const users = client.db('shop').collection('users');
const r1 = await users.insertOne({ name: 'Аня', age: 30, city: 'Москва' });
console.log('acknowledged :', r1.acknowledged);
console.log('insertedId :', r1.insertedId, '| тип:', r1.insertedId.constructor.name);
acknowledged : true
insertedId : new ObjectId('6a59e5dd7368400e383caa62') | тип: ObjectId
Ни базы shop, ни коллекции users заранее не существовало — они созданы этой же операцией. Отдельный createCollection нужен, только если у коллекции должны быть особые настройки: валидатор схемы, ограниченный размер.
Что вернулось:
acknowledged— сервер подтвердил запись. Это проwriteConcern: по умолчанию база дожидается подтверждения, иfalseздесь означает режим «отправил и забыл»;insertedId— идентификатор нового документа.
-
1
Драйвер генерит
_idНа вашей машине, до отправки. ПоэтомуinsertedIdизвестен сразу -
2
Отправка на серверОдин сетевой раундтрип. У
insertManyон тоже один — на весь массив -
3
Проверка уникальностиДубликат ключа →
MongoServerError, code 11000 -
4
Подтверждение записи
acknowledged: true— сервер подтвердил (writeConcern)
Кто на самом деле создаёт _id
Неочевидная деталь, которая экономит запросы: _id генерирует драйвер у вас на машине, ещё до отправки. Не сервер.
Следствие практическое: id известен сразу и не требует «вставить, потом прочитать обратно». Это отличие от привычного SQL-автоинкремента, где номер выдаёт база.
Задать _id можно и самому — тогда он не обязан быть ObjectId:
const r2 = await users.insertOne({ _id: 'user-42', name: 'Борис', age: 25 });
console.log('свой _id:', r2.insertedId);
свой _id: user-42
Строка, число, объект — подойдёт что угодно, лишь бы было уникально. Осмысленный _id вроде артикула товара избавляет от лишнего индекса: _id индексирован всегда.
Ошибка 11000: дубликат ключа
try {
await users.insertOne({ _id: 'user-42', name: 'Двойник' });
} catch (err) {
console.log('ошибка :', err.constructor.name, '| code:', err.code);
console.log('сообщение:', err.message.split('\n')[0]);
}
ошибка : MongoServerError | code: 11000
сообщение : E11000 duplicate key error collection: shop.users index: _id
Код 11000 — единственный, который стоит запомнить наизусть: он же прилетит при нарушении любого уникального индекса, не только _id.
Проверять нужно err.code === 11000, а не текст: формулировка меняется между версиями сервера, код — нет.
Типичный сценарий — регистрация с уникальным email. И здесь есть развилка: ловить 11000 или сначала проверить, есть ли такой пользователь? Ловить. Проверка «сначала find, потом insert» содержит гонку: между двумя запросами кто-то успеет вставить того же пользователя. Уникальный индекс — единственная надёжная защита, а 11000 — его штатный ответ.
Третий вариант, часто самый удобный, — updateOne с upsert: true: он либо обновит существующий документ, либо создаст новый, одной атомарной операцией.
insertMany: пачкой
const r3 = await users.insertMany([
{ name: 'Вера', age: 28, city: 'Казань' },
{ name: 'Глеб', age: 35, city: 'Москва' },
{ name: 'Дина', age: 41, city: 'Пермь' },
]);
console.log('вставлено :', r3.insertedCount);
console.log('всего в коллекции:', await users.countDocuments());
вставлено : 3
всего в коллекции: 5
Разница с циклом принципиальная, и она не в красоте кода. insertMany — это одна отправка на сервер. Цикл из insertOne — это N отправок, каждая со своим сетевым раундтрипом. На локальной базе разница незаметна, на удалённой — линейна по числу документов.
Та же логика, что и с appendFile в цикле против одного дескриптора: дорого не действие, дорого повторение накладных расходов.
Ограничения, о которые спотыкаются:
- 16 МБ на документ. Упёрлись — почти всегда это сигнал, что данные стоило разделить; для больших бинарных объектов есть GridFS;
- батч не бесконечен. Очень большие массивы драйвер разобьёт сам, но осмысленный размер пачки — тысячи документов, не миллионы.
ordered: что будет, если один документ упадёт
По умолчанию insertMany работает в режиме ordered: true: документы вставляются по порядку, и на первой же ошибке всё останавливается. Документы до неё останутся в базе, после — нет.
await users.insertMany(docs, { ordered: false });
С ordered: false драйвер попробует вставить все документы, а в конце сообщит обо всех ошибках сразу. Это то, что нужно при импорте данных: одна битая запись из тысячи не должна останавливать загрузку.
Схемы нет: вставить можно что угодно
await users.insertOne({ name: 'Егор', любимыйЦвет: 'синий', теги: ['a', 'b'] });
наборы полей у документов:
name, age, city
name, age
name, age, city
name, любимыйЦвет, теги
Документ с полями, которых нет ни у кого больше, лёг в ту же коллекцию — и это штатное поведение. Никакого ALTER TABLE, никаких миграций.
Обратная сторона та же, что всегда: опечатка в имени поля — не ошибка, а новое поле. База не защитит; ограничить форму можно только валидатором коллекции или Mongoose. Подробнее — в разборе что такое MongoDB.
writeConcern: что значит «записано»
За полем acknowledged: true стоит настройка, о которой стоит знать, — writeConcern. Она отвечает на вопрос: в какой момент сервер считает запись состоявшейся.
| Значение | Когда вернётся ответ | Риск |
|---|---|---|
w: 0 |
сразу после отправки | запись может не дойти вообще |
w: 1 |
по умолчанию — записал основной узел | падение узла до репликации теряет данные |
w: 'majority' |
подтвердило большинство узлов | почти нет |
j: true |
запись легла в журнал на диск | переживает падение процесса |
await users.insertOne(doc, { writeConcern: { w: 'majority', j: true } });
Именно отсюда растёт старая репутация «MongoDB теряет данные». В ранних версиях по умолчанию стояло w: 0 — драйвер не ждал подтверждения вообще, и запись действительно могла молча исчезнуть. Бенчмарки того времени показывали фантастическую скорость ровно по этой причине.
Сегодня по умолчанию w: 1 — сервер отвечает, только записав данные. Это и есть тот acknowledged: true в выводе выше. Для реплики стоит подумать про majority: без него запись, принятая упавшим основным узлом, может не дожить до нового.
Размен прямой: чем строже writeConcern, тем медленнее вставка и тем меньше шансов потерять данные. Для аналитики сойдёт w: 1, для платежей — majority с j: true.
Что было на этой странице в 2017 году
Первая версия вышла 30 июля 2017 года и описывала вставку через методы, которых сегодня уже нет: insert() — без суффикса.
Разница не косметическая. Старый insert() принимал и один документ, и массив, и поведение зависело от того, что именно вы передали. Оттуда же родом update(), который по умолчанию менял только первый подходящий документ, — и об этом постоянно забывали.
Явные insertOne и insertMany появились именно поэтому: имя метода теперь говорит, сколько документов будет затронуто, и угадывать не нужно. Это редкий случай, когда переименование убрало целый класс ошибок.
Второе отличие эпохи — колбэки:
db.collection('users').insert({ name: 'Аня' }, function(err, result) {
if (err) throw err;
console.log(result);
});
Промис-версии в драйвере тогда только появлялись. Сегодня драйвер промисный целиком, и вставка — обычный await.
Смежные темы: все CRUD-операции — выборка, обновление, удаление и индексы; что такое MongoDB — когда её брать, а когда нет; промисы. Полный список — в справочнике по Node.js.
Частые вопросы
Нужно ли создавать базу и коллекцию заранее?
createCollection нужен только ради особых настроек — например, валидатора схемы или ограниченной коллекции.Кто генерирует `_id` — сервер или драйвер?
insertedId известен сразу, а вставку можно делать, уже зная будущий id.Что делать при ошибке 11000?
err.code === 11000, а не по тексту сообщения. Часто вместо обработки ошибки правильнее updateOne с upsert: true — тогда гонки между проверкой и вставкой нет.