Mongoose в Express: модель, валидация и CRUD

Коротко о Mongoose
Mongoose — библиотека для Node.js, которая добавляет схемы и удобные API для работы с коллекциями MongoDB. Она помогает гарантировать согласованность данных и упрощает валидацию, методы модели, хуки и работу с транзакциями.
Определение в одну строку: Mongoose — это слой между вашим кодом и MongoDB, превращающий «свободные» документы в предсказуемые объекты с правилами.
Важно: Mongoose не отменяет необходимость проектирования модели данных — он помогает его реализовать и поддерживать.
Что вы узнаете из этого руководства
- Как установить и подключить Mongoose в проект Express
- Как определять схемы и модели (валидация, типы, дефолты, индексы)
- Как выполнять CRUD-операции и работать с транзакциями
- Лучшие практики, отладка и сценарии, где Mongoose может не подходить
- Альтернативы и быстрый чек-лист для команды
Установка и подключение
Установите Mongoose как зависимость проекта:
npm install mongooseПример простого подключения (файл index.js):
// index.js
const mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1:27017/example")
.then(() => console.log("Connected to database successfully"))
.catch(err => console.error("DB connection error:", err));
// Дополнительно: обработка событий соединения
mongoose.connection.on('disconnected', () => console.warn('MongoDB disconnected'));Советы по подключению:
- При подключении к продакшен-кластерам используйте строку подключения из переменных окружения и не храните пароли в коде.
- Можно передавать опции (если нужно): useNewUrlParser и useUnifiedTopology уже установлены по умолчанию в новых версиях, но иногда полезно явно указывать другие опции.
- Для отказоустойчивости используйте replica set URI.
Создание схемы и модели: базовые элементы
Схема определяет поля документа, типы, валидации и настройки. Модель — это класс с методами для работы с коллекцией.
Пример модели User (userModel.js):
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: 100
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
age: {
type: Number,
validate: {
validator: function (value) {
return value > 0;
},
message: () => "Please enter a valid age"
}
}
}, {
timestamps: true // createdAt и updatedAt
});
const User = mongoose.model("User", userSchema);
module.exports = User;Пояснения по свойствам схемы:
- required: обязательно для сохранения. Можно указать булево или сообщение об ошибке.
- unique: создает уникальный индекс (обратите внимание: unique не является валидатором на уровне документа, это индекс базы данных).
- default: можно задать значение по умолчанию.
- enum: ограничивает набор допустимых значений.
- trim/lowercase/uppercase: трансформации строки перед сохранением.
- timestamps: автоматически добавляет createdAt и updatedAt.
Встроенные валидации и кастомные валидаторы
Mongoose поддерживает встроенные валидаторы (required, min/max, match для RegExp) и кастомные функции-валидаторы.
Пример кастомного валидатора email:
email: {
type: String,
required: true,
match: [/^\S+@\S+\.\S+$/, 'Email is invalid']
}Или с внешней библиотекой (например, validator.js):
const validator = require('validator');
email: {
type: String,
validate: {
validator: (v) => validator.isEmail(v),
message: 'Email is invalid'
}
}Виртуальные свойства, методы и статики
- Виртуалы (virtuals) позволяют добавлять вычисляемые поля, которые не сохраняются в БД.
- Методы (schema.methods) доступны экземплярам документов.
- Статические методы (schema.statics) доступны самой модели.
Пример:
userSchema.virtual('isAdult').get(function() {
return this.age >= 18;
});
userSchema.methods.sayHello = function() {
return `Hello, ${this.name}`;
};
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email });
};Мидлвары (хуки)
Mongoose поддерживает pre/post хуки для операций save, validate, remove, updateOne и т.д. Это удобно для хеширования паролей, логирования и других побочных эффектов.
Пример хука для хеширования пароля (синхронный псевдокод):
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await hashPassword(this.password);
next();
});Важно: не делайте тяжелых долгих операций в хуках без обработки ошибок и таймаутов.
CRUD: практические примеры и нюансы
Примечание: в примерах предполагается, что модель User импортирована: const User = require(‘./userModel’);
Создание документов
- Через новый экземпляр и save():
let user = new User({ name, email, age });
user.save()
.then(() => console.log('User created successfully'))
.catch(err => console.error(err));- Через create():
User.create({ name, email, age }, (err, data) => {
if (err) throw new Error('Internal Server Error');
console.log(`User created successfully: ${data}`);
});- insertMany для пакетной записи (быстрее для больших объёмов):
User.insertMany([
{ name, email, age },
{ name_1, email_1, age_1 }
], { ordered: false })
.then(result => console.log('Inserted:', result))
.catch(err => console.error(err));Советы:
- Для больших вставок используйте bulkWrite или insertMany с ordered: false, чтобы ошибки отдельных документов не прерывали всю операцию.
- Обрабатывайте ошибки дублей (E11000) при уникальных индексах.
Чтение документов
- Получить все документы:
User.find({})
.then(data => console.log(data))
.catch(err => console.error(err));- Фильтрация и операторы MongoDB:
User.find({ age: { $gte: 18 } })
.then(data => console.log(data))
.catch(err => console.error(err));- По id или по одному документу:
User.findById(id)
.then(doc => console.log(doc))
.catch(err => console.error(err));
User.findOne({ email: 'johnson@example.com' })
.then(user => {
if (!user) { /* not found */ }
});Оптимизации:
- Для быстрых выборок без документов Mongoose-объектов используйте .lean() — вернёт plain JS-объекты и быстрее.
- Используйте проекции ({ name: 1, email: 1 }) чтобы вернуть только необходимые поля.
- Пагинация: skip/limit или лучше — курсоры и диапазонные запросы по индексируемым полям.
Обновление документов
- findByIdAndUpdate:
User.findByIdAndUpdate(id, req.body, { new: true, runValidators: true }, (err, doc) => {
if (err) { /* handle error */ }
// doc — обновлённый документ
});Параметры:
new: true — вернуть обновлённый документ
runValidators: true — запустить валидацию по схеме при update
useFindAndModify — опция устарела в новых версиях; используйте методы модели
updateOne/updateMany и $set, $inc и другие операторы обновления позволяют делать частичные изменения без перезаписи документа.
Удаление документов
User.findByIdAndDelete(id, (error, result) => {
if (error) { /* handle error */ }
// result — удалённый документ
});Или deleteMany для массовых удалений.
Транзакции и сессии
Для атомарных операций на нескольких документах используйте транзакции MongoDB (требуется Replica Set).
Пример сессии и транзакции:
const session = await mongoose.startSession();
session.startTransaction();
try {
await User.create([{ name: 'A' }], { session });
await AnotherModel.updateOne({ _id: id }, { $inc: { count: 1 } }, { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}Важно: транзакции увеличивают задержку и нагрузку. Используйте их только когда необходимые гарантии целостности.
Производительность и индексы
- Индексы — основной инструмент ускорения чтения. Создавайте индексы по полям, которые используются в фильтрах и сортировках.
- Используйте compound-индексы для часто комбинируемых полей.
- Мониторьте explain() запроса в MongoDB, чтобы понимать, какие индексы используются.
- Для аналитики и больших агрегаций лучше использовать агрегат-пайплайны MongoDB или отдельные инструменты обработки данных.
Пример создания индекса в схеме:
userSchema.index({ email: 1 });Логирование и обработка ошибок
- Ловите ошибки подключения и регистрируйте их в логах.
- Обрабатывайте ошибки валидации (ValidationError) и ошибки уникальности (E11000) отдельно, чтобы возвращать понятные ответы клиенту.
Пример базовой обработки ошибок:
try {
await user.save();
} catch (err) {
if (err.name === 'ValidationError') {
// вернуть 400 и сообщение клиенту
} else if (err.code === 11000) {
// ошибочный дубль уникального индекса
} else {
// 500 и лог
}
}Когда Mongoose — не лучший выбор
- Скрипты для миграции/ETL или аналитические задачи, где важна максимальная скорость и минимальные накладные расходы, иногда выгоднее делать с помощью нативного MongoDB-драйвера.
- Очень динамичные документы без предсказуемой структуры — если у вас нет пользы от схем, Mongoose добавит излишнюю сложность.
- Если требуется ORM для нескольких баз данных (SQL + NoSQL), рассмотрите универсальные решения.
Альтернативные инструменты
- Официальный MongoDB Node.js Driver — тоньше и быстрее для простых операций, даёт полный контроль.
- Prisma — ORM с поддержкой MongoDB в ранних версиях, ориентирован на типизацию и миграции (проверять совместимость версий).
- TypeORM / MikroORM — альтернативы, если вам нужна единая модель данных для SQL и NoSQL.
Быстрый чек-лист по внедрению Mongoose в проект Express
Для разработчика:
- Установил и настроил Mongoose через переменные окружения
- Определил схемы с нужными полями, индексами и валидацией
- Добавил runValidators при обновлениях
- Использует .lean() там, где важна производительность
Для DevOps:
- Использует реплика сет для транзакций
- Настроил резервное копирование и мониторинг MongoDB
- Управляет переменными окружения и секретами
Для QA:
- Тесты валидации модели (валидные/невалидные данные)
- Тесты ошибок уникального индекса
- Интеграционные тесты с in-memory MongoDB или тестовым кластером
Ментальная модель и эвристики
- Представляйте Mongoose как контракт на данные: схема — контракт, модель — реализация.
- Используйте валидацию на уровне модели для защиты бизнеса; не полагайтесь только на клиентскую валидацию.
- Выбирайте транзакции только для критических операций согласованности.
Решение: когда выбрать Mongoose или драйвер
flowchart TD
A[Нужно ли иметь схему?] -->|Да| B[Используйте Mongoose]
A -->|Нет| C[Рассмотрите нативный драйвер]
B --> D{Потребность в транзакциях}
D -->|Да| E[Replica Set + транзакции]
D -->|Нет| F[Стандартные CRUD + индексы]
C --> G[Оптимизированные скрипты/аналитика]Шпаргалка по часто используемым методам
- create(doc)
- insertMany(docs, options)
- find(filter, projection)
- findById(id)
- findOne(filter)
- findByIdAndUpdate(id, update, { new, runValidators })
- findByIdAndDelete(id)
- updateOne(filter, update)
- deleteMany(filter)
- startSession() / транзакции
Критерии приёмки
- Приложение корректно подключается к MongoDB и восстанавливает соединение при разрыве.
- Все модели имеют схему и ключевые поля индексированы.
- Обновления проходят валидацию (runValidators=true).
- Массовые операции обрабатывают ошибки дублей и возвращают понятные ответы.
Итог и рекомендации
Mongoose даёт баланс между гибкостью MongoDB и предсказуемостью схем. Он упрощает валидацию, добавляет методы и хуки, но добавляет уровень абстракции. Используйте Mongoose, если вам важны схемы, чистые модели и удобные API. Для простых или высокопроизводительных задач рассмотрите нативный драйвер.
Важно: планируйте индексы заранее, тестируйте обновления с runValidators и отслеживайте производительность.
Ключевые выводы:
- Mongoose структурирует данные и упрощает CRUD в Express-приложениях.
- Схемы, валидация, хуки и методы помогают поддерживать контроль над данными.
- Транзакции доступны, но требуют Replica Set и дают дополнительные накладные расходы.
Похожие материалы
Как полностью переустановить ChromeOS на Chromebook
Идеальные скриншоты в Windows
Проверка скорости Wi‑Fi: тесты и устранение проблем
Виджеты и гаджеты для Windows 10
Коды BSOD Windows 10/11: как найти и исправить