Вставка документов в 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 — идентификатор нового документа.
Что происходит при insertOne
  1. 1 Драйвер генерит _idНа вашей машине, до отправки. Поэтому insertedId известен сразу
  2. 2 Отправка на серверОдин сетевой раундтрип. У insertMany он тоже один — на весь массив
  3. 3 Проверка уникальностиДубликат ключа → MongoServerError, code 11000
  4. 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 — тогда гонки между проверкой и вставкой нет.
Есть ли ограничение на размер документа?
Да, 16 МБ. Если упираетесь — обычно это признак, что данные стоило разделить; для больших бинарных объектов есть GridFS.
Можно ли вставлять документы с разными полями в одну коллекцию?
Да, схемы нет — MongoDB примет любую форму. Ограничить это можно только валидатором коллекции или Mongoose.