🦦 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();