← Formation BDD

🦦 BD06 — Mongoose & Node.js

Mongoose 8 Durée : ~2h ODM

1. Schema & Model

const mongoose = require('mongoose');

// Connexion
await mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000
});

// Définir un schema
const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Le nom est requis'],
    trim: true,
    minlength: 2,
    maxlength: 100
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, 'Email invalide']
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  age: { type: Number, min: 0, max: 150 },
  tags: [String],
  address: {
    street: String,
    city: String,
    country: { type: String, default: 'FR' }
  },
  active: { type: Boolean, default: true }
}, {
  timestamps: true,    // createdAt, updatedAt auto
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// Index
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' });

// Créer le modèle
const User = mongoose.model('User', userSchema);
module.exports = User;

2. Validation personnalisée

const productSchema = new mongoose.Schema({
  name: String,
  price: {
    type: Number,
    validate: {
      validator: (v) => v >= 0,
      message: 'Le prix doit être positif'
    }
  },
  sku: {
    type: String,
    validate: {
      validator: async function(sku) {
        // Validation async — vérifier unicité
        const count = await this.constructor.countDocuments({ sku });
        return count === 0;
      },
      message: 'SKU déjà utilisé'
    }
  }
});

// Utilisation
try {
  const product = new Product({ name: 'Test', price: -5 });
  await product.save();
} catch (err) {
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => e.message);
    console.error('Erreurs :', errors);
  }
}

3. Hooks (Middleware)

const bcrypt = require('bcrypt');

// pre save — hasher le mot de passe
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// post save — envoyer email de bienvenue
userSchema.post('save', async function(doc) {
  if (doc.isNew) {
    await sendWelcomeEmail(doc.email);
  }
});

// pre find — exclure les utilisateurs inactifs par défaut
userSchema.pre(/^find/, function(next) {
  this.find({ active: { $ne: false } });
  next();
});

// pre remove — nettoyer les données liées
userSchema.pre('deleteOne', { document: true }, async function(next) {
  await Order.deleteMany({ userId: this._id });
  next();
});

4. Virtuals

// Propriété calculée non persistée en base
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

userSchema.virtual('fullName').set(function(name) {
  const parts = name.split(' ');
  this.firstName = parts[0];
  this.lastName = parts.slice(1).join(' ');
});

// Virtual populate — orders d'un user
userSchema.virtual('orders', {
  ref:         'Order',
  localField:  '_id',
  foreignField: 'userId'
});

// Utilisation
const user = await User.findById(id);
console.log(user.fullName); // 'Alice Dupont'

// Nécessaire pour inclure les virtuals en JSON
userSchema.set('toJSON', { virtuals: true });

5. Populate (références)

const orderSchema = new mongoose.Schema({
  userId:   { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  items: [{
    productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
    qty:   Number,
    price: Number
  }],
  status: { type: String, default: 'pending' }
});

// Populate simple
const order = await Order.findById(orderId).populate('userId', 'name email');

// Populate imbriqué
const orders = await Order.find({ status: 'pending' })
  .populate('userId', 'name email')
  .populate('items.productId', 'name price')
  .lean();

// populate conditionnel (populate + match)
const users = await User.find().populate({
  path: 'orders',
  match: { status: 'completed' },
  options: { sort: { createdAt: -1 }, limit: 5 }
});

6. .lean() & Performance

// .lean() retourne des POJO (objets JS purs), ~2x plus rapide
// ⚠ Pas de méthodes de document, pas de virtuals, pas de getters
const users = await User.find({ active: true }).lean();
// users est un tableau de plain objects — idéal pour API lectures seules

// Sélection de champs
const light = await User.find().select('name email -_id').lean();

// Curseur pour grandes collections
const cursor = User.find({ active: true }).cursor();
for await (const user of cursor) {
  await processUser(user);
}

// countDocuments vs estimatedDocumentCount
const total   = await User.countDocuments({ active: true }); // exact
const approx  = await User.estimatedDocumentCount();          // rapide

7. Discriminators (héritage)

// Schema de base
const notifSchema = new mongoose.Schema({
  userId:  { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  read:    { type: Boolean, default: false },
  createdAt: { type: Date, default: Date.now }
}, { discriminatorKey: 'type' });

const Notification = mongoose.model('Notification', notifSchema);

// Sous-types
const EmailNotif = Notification.discriminator('email',
  new mongoose.Schema({ subject: String, body: String })
);
const PushNotif = Notification.discriminator('push',
  new mongoose.Schema({ title: String, icon: String })
);

// Usage
await EmailNotif.create({ userId, subject: 'Bienvenue!', body: '...' });
const all = await Notification.find({ userId }).lean();