Гид по технологиям

ACID-транзакции в MongoDB с Mongoose

5 min read Databases Обновлено 07 Jan 2026
ACID-транзакции в MongoDB с Mongoose
ACID-транзакции в MongoDB с Mongoose

Этот материал объясняет, как обеспечить свойства ACID (атомарность, согласованность, изоляция, надёжность) в MongoDB при помощи Mongoose и Node.js. Вы узнаете требования к окружению (реплика-сет или MongoDB Atlas), два способа реализации транзакций (ручное управление commit/abort и session.withTransaction), типичные ошибки и методику тестирования. Приведены примеры кода, чек‑листы для ролей и схема принятия решения.

Ноутбук с логотипами MongoDB и JavaScript

Зачем нужны транзакции в MongoDB

Транзакция — это набор операций с базой данных, которые должны выполниться как единое целое. Если одна операция из набора не проходит, все изменения откатываются. Это необходимо, когда вы обновляете несколько документов/коллекций и хотите сохранить целостность данных.

Кратко о терминах (одна строка):

  • Транзакция — единица работы, выполняемая атомарно.
  • Сессия — контекст, в котором выполняется транзакция.

ACID — что означают свойства

  • Атомарность: все операции в транзакции выполняются полностью или не выполняются вовсе.
  • Согласованность: после транзакции данные остаются в корректном состоянии согласно бизнес-правилам.
  • Изоляция: параллельные транзакции не вмешиваются друг в друга так, чтобы нарушить целостность.
  • Надёжность (durability): после фиксации (commit) данные сохраняются даже при сбоях.

Важно: MongoDB реализует транзакции поверх механизма репликации, поэтому транзакции доступны не на standalone‑инстансе.

Требования и совместимость

  • Транзакции для многодокументных операций доступны в MongoDB начиная с версии 4.0 для replica set.
  • Поддержка транзакций в шарди́рованных кластерах появилась в MongoDB 4.2.
  • Транзакции не работают на одиночных (standalone) серверах.
  • Проще всего получить подходящее окружение через MongoDB Atlas (по умолчанию replica set или sharded cluster).

Важно: не нужно придумывать механизм транзакций поверх неподдерживаемого окружения — это небезопасно.

Сценарий и схема данных для примера

Рассмотрим простое приложение для вакансий, где пользователь может создавать объявления о работе. Нам нужно создать вакансию и добавить ссылку на неё в массив объявлений автора — это классическая задача для транзакции.

Диаграмма схемы коллекций User и Job

Схема (идея):

  • users: { name, email, jobs: [ObjectId] }
  • jobs: { title, location, salary, poster: ObjectId }

Подключение Mongoose (пример)

import mongoose from 'mongoose'

let MONGO_URL = process.env.MONGO_URL || 'your-mongo-database-url';

let connection;
const connectDb = async () => {
  try {
    await mongoose.connect(MONGO_URL, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    console.log("CONNECTED TO DATABASE");
    connection = mongoose.connection;
  } catch (err) {
    console.error("DATABASE CONNECTION FAILED!");
    console.error(err.message);
    process.exit(1); // закрываем приложение при ошибке соединения
  }
};

Сохраняем объект соединения в переменной connection — он понадобится для запуска сессий транзакции.

Схемы коллекций и создание пользователя

const userSchema = new mongoose.Schema({
    name: String,
    email: String,
    jobs: [mongoose.Schema.Types.ObjectId]
});

const jobSchema = new mongoose.Schema({
    title: String,
    location: String,
    salary: String,
    poster: mongoose.Schema.Types.ObjectId
});

const userCollection = mongoose.model('user', userSchema);
const jobCollection = mongoose.model('job', jobSchema);

Функция для создания пользователя:

const createUser = async (user) => {
    const newUser = await userCollection.create(user);
    console.log("User added to database");
    console.log(newUser);
}

Вариант 1 — ручное управление транзакцией (commit/abort)

Ниже пример функции, которая создаст вакансию и добавит её в список вакансий пользователя в одной транзакции.

const createJob = async (job) => {
  const { userEmail, title, location, salary } = job;

  // получаем пользователя из БД
  const user = await userCollection.findOne({ email: userEmail });

  // начинаем сеанс транзакции
  const session = await connection.startSession();

  // выполняем все операции в try-catch
  try {
    await session.startTransaction();

    // создаём вакансию
    const newJob = await jobCollection.create(
      [
        {
          title,
          location,
          salary,
          poster: user._id,
        },
      ],
      { session }
    );

    console.log("Created new job successfully!");
    console.log(newJob[0]);

    // добавляем id вакансии в список пользователя
    const newJobId = newJob[0]._id;

    const addedToUser = await userCollection.findByIdAndUpdate(
      user._id,
      { $addToSet: { jobs: newJobId } },
      { session }
    );

    console.log("Successfully added job to user's jobs list");
    console.log(addedToUser);

    await session.commitTransaction();

    console.log("Successfully carried out DB transaction");
  } catch (e) {
    console.error(e);
    console.log("Failed to complete database operations");
    await session.abortTransaction();
  } finally {
    await session.endSession();
    console.log("Ended transaction session");
  }
};

Заметка: create() внутри транзакции часто возвращает массив созданных документов, поэтому мы берём newJob[0]._id.

Вариант 2 — использование session.withTransaction

Метод withTransaction упрощает код: вы передаёте callback, и MongoDB сам выполнит commit/abort в зависимости от результата.

const createJob = async (job) => {
  const { userEmail, title, location, salary } = job;

  // получаем пользователя из БД
  const user = await userCollection.findOne({ email: userEmail });

  // начинаем сеанс транзакции
  const session = await connection.startSession();

  // выполняем запросы внутри withTransaction
  try {
    const transactionSuccess = await session.withTransaction(async () => {
      const newJob = await jobCollection.create(
        [
          {
            title,
            location,
            salary,
            poster: user._id,
          },
        ],
        { session }
      );

      console.log("Created new job successfully!");
      console.log(newJob[0]);

      // добавляем id вакансии в список пользователя
      const newJobId = newJob[0]._id;
      const addedToUser = await userCollection.findByIdAndUpdate(
        user._id,
        { $addToSet: { jobs: newJobId } },
        { session }
      );

      console.log("Successfully added job to user's jobs list");
      console.log(addedToUser);
    });

    if (transactionSuccess) {
      console.log("Successfully carried out DB transaction");
    } else {
      console.log("Transaction failed");
    }
  } catch (e) {
    console.error(e);
    console.log("Failed to complete database operations");
  } finally {
    await session.endSession();
    console.log("Ended transaction session");
  }
};

withTransaction сам вызывает commit или abort, если callback бросил исключение или вернул неудачный результат.

Демонстрация запуска

const mockUser = {
  name: "Timmy Omolana",
  email: "jobposter@example.com",
};

const mockJob = {
  title: "Sales Manager",
  location: "Lagos, Nigeria",
  salary: "$40,000",
  userEmail: "jobposter@example.com", // email созданного пользователя
};

const startServer = async () => {
  await connectDb();
  await createUser(mockUser);
  await createJob(mockJob);
};

startServer()
  .then()
  .catch((err) => console.log(err));

Вывод выполнения транзакций в консоли

Когда транзакции подойдут, а когда нет

Подходящая ситуация:

  • Множественные связанные изменения в разных коллекциях, которые должны быть атомарными.
  • Критичные финансовые операции, перевод баланса между кошельками и т. п.

Когда транзакции не нужны или вредны:

  • Простые операции над одним документом, где достаточно атомарных операций MongoDB (updateOne, $addToSet и т. п.).
  • Высоконагруженные пути, где накладные расходы транзакций ухудшат производительность.
  • Когда окружение — одиночный MongoDB (standalone): транзакции не поддерживаются.

Совет: чаще всего разумнее моделировать данные так, чтобы минимизировать количество междокументных транзакций.

Ментальные модели и эвристики

  • «Минимизируй область действия транзакции»: выполняйте только те операции, которые требуют атомарности.
  • «Сессия — это контейнер»: храните всю логику изменения данных внутри сессии.
  • «Предполагаем худшее»: всегда обрабатывайте ошибки и корректно закрывайте сессию (finally → endSession()).

Мини‑методология внедрения транзакций

  1. Оцените, действительно ли нужна транзакция.
  2. Подготовьте окружение (реплика‑сет/Atlas, версия MongoDB >= 4.0).
  3. Напишите unit и интеграционные тесты, которые проверяют откат при ошибке.
  4. Внедрите логирование шагов транзакции.
  5. Нагрузочное тестирование, чтобы понять влияние на latency.
  6. Мониторинг отказов и лагов репликации.

Чек‑листы по ролям

Разработчик:

  • Проверил, что операция изменяет несколько документов.
  • Использует session и передаёт { session } во все запросы.
  • Обрабатывает исключения и всегда завершает сессию.

DevOps:

  • Окружение — replica set или sharded cluster.
  • MongoDB версии не ниже 4.0 (реплика‑сет), 4.2 для шардинга.
  • Настроен мониторинг репликации и задержек.

QA:

  • Есть тесты на откат транзакции при исключении.
  • Есть интеграционные сценарии с реальной БД (staging).
  • Нагрузочное тестирование витального пути.

Критерии приёмки

  • Транзакция успешно коммитится при нормальном выполнении всех операций.
  • При ошибке хотя бы одной операции все изменения откатываются.
  • Сессия всегда завершается (session.endSession вызывается в finally).
  • Логирование содержит шаги: startTransaction, commit/abort, endSession.

Тесты и кейсы приёма

  • Создать вакансию и убедиться, что запись появилась в jobs и id добавлен в users.jobs.
  • Симулировать ошибку в середине транзакции (например, бросить исключение) и проверить, что вакансия не создана и в users.jobs нет ссылки.
  • Проверить поведение при потере соединения с первичным узлом реплика‑сета.

Диаграмма принятия решения

flowchart TD
  A[Нужно ли изменить несколько коллекций?] -->|Нет| B[Не используем транзакцию]
  A -->|Да| C[Работает ли окружение как replica set или sharded cluster?]
  C -->|Нет| D[Сначала перейти на подходящее окружение]
  C -->|Да| E[Используем транзакции]
  E --> F{Нужен fine-grained control?}
  F -->|Да| G[Ручное startTransaction/commit/abort]
  F -->|Нет| H[Используем session.withTransaction]

Альтернативные подходы

  • Сведение данных в один документ, чтобы избежать междокументных транзакций (denormalization).
  • Саги (saga pattern) для распределённых транзакций: последовательность компенсирующих операций.
  • Optimistic concurrency control (версионирование документов) для конфликтов при параллельных обновлениях.

Безопасность и конфиденциальность

  • Логи транзакций не должны содержать PII в открытом виде. Используйте маскирование в логах.
  • Если вы храните персональные данные, проверьте соответствие требованиям защиты данных (например, GDPR) при репликации и бэкапах.

Замечания по производительности

Транзакции увеличивают задержку (latency) и нагрузку на сеть/репликацию — тестируйте критичные пути. Для редко выполняемых критичных операций это приемлемо; для массовых операций лучше оптимизировать модель данных.

Резюме

  • Транзакции в MongoDB доступны с версии 4.0 (реплика‑сет) и 4.2 (шардинг).
  • Используйте session.startTransaction/commit/abort или session.withTransaction в зависимости от потребности в гибком управлении.
  • Всегда проверяйте окружение, покрывайте сценарии тестами и контролируйте производительность.

Важно

Перед внедрением транзакций проанализируйте, нельзя ли переработать модель данных так, чтобы уменьшить число междокументных операций.

Полезные шаги для дальнейших исследований: реализуйте небольшое приложение (например, финтех‑кошелёк или блог) и протестируйте сценарии rollback при ошибках и при failover реплика‑сета.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство