MongoDB: основы и Mongoose

MongoDB — документоориентированная NoSQL база данных, хранящая данные в BSON-документах (похоже на JSON); Mongoose — ODM-библиотека для Node.js, добавляющая схемы, валидацию и удобный API поверх MongoDB.

Зачем нужно

MongoDB хорошо подходит для гибких, часто меняющихся структур данных: нет необходимости в миграциях при добавлении полей, хранение вложенных объектов и массивов естественно. Mongoose добавляет типизацию и валидацию на уровне приложения, что компенсирует отсутствие схемы в MongoDB.

Где используется

  • Хранение пользовательских профилей с произвольными атрибутами
  • Каталоги товаров (разные атрибуты для разных категорий)
  • Логи и события (append-only коллекции)
  • Прототипирование — быстрый старт без миграций

Основной контент

Установка и подключение

npm install mongoose
// db/connect.js
const mongoose = require('mongoose');

async function connectDB() {
  await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
  console.log('MongoDB connected');
}

mongoose.connection.on('error', (err) => console.error('MongoDB error:', err));
mongoose.connection.on('disconnected', () => console.warn('MongoDB disconnected'));

module.exports = connectDB;

Определение схемы и модели

// models/User.js
const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema(
  {
    name: { type: String, required: true, trim: true },
    email: { type: String, required: true, unique: true, lowercase: true },
    age: { type: Number, min: 0, max: 150 },
    role: { type: String, enum: ['user', 'admin'], default: 'user' },
    tags: [String],               // массив строк
    address: {                    // вложенный объект
      city: String,
      country: { type: String, default: 'RU' }
    },
    createdAt: { type: Date, default: Date.now() }
  },
  { timestamps: true }  // автоматически добавит createdAt, updatedAt
);

// Индексы
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' }); // full-text search

// Метод экземпляра
userSchema.methods.toPublicJSON = function  {
  const obj = this.toObject;
  delete obj.password;
  return obj;
};

// Статический метод
userSchema.statics.findByEmail = function (email) {
  return this.findOne({ email });
};

const User = mongoose.model('User', userSchema);
module.exports = User;

CRUD операции

const User = require('./models/User');

// Create
const user = await User.create({ name: 'Alice', email: 'alice@example.com' });

// Read
const users = await User.find({ role: 'admin' });
const user = await User.findById('64a1b2c3d4e5f6789abcdef0');
const user = await User.findOne({ email: 'alice@example.com' });

// Проекция и сортировка
const users = await User.find({})
  .select('name email -_id') // включить/исключить поля
  .sort({ createdAt: -1 })
  .limit(10)
  .skip(20);

// Update
await User.findByIdAndUpdate(id, { name: 'Bob' }, { new: true, runValidators: true });
await User.updateMany({ role: 'user' }, { $set: { active: true } });

// Delete
await User.findByIdAndDelete(id);
await User.deleteMany({ active: false });

Populate — связи между коллекциями

const postSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: 'User' } // ссылка
});

const Post = mongoose.model('Post', postSchema);

// Заполнить связанные документы
const posts = await Post.find.populate('author', 'name email');

Частые ошибки

  • Не закрывать соединение в тестах — Mongoose держит соединение открытым; afterAll( => mongoose.connection.close())
  • Использовать _id как строку без приведения типаUser.findById('abc') — mongoose сам конвертирует, но User.findOne({ _id: 'abc' }) требует new mongoose.Types.ObjectId('abc')
  • Не настроить runValidators при update — по умолчанию валидация схемы не выполняется при findByIdAndUpdate
  • Хранить всё в одной коллекции — MongoDB плохо справляется с JOIN-запросами; нормализовывать данные с умом

Связанные темы

Ресурсы