Mongoose : relations et agrégation
MongoDB n’a pas de jointures SQL : deux modèles de relation existent.
| Modèle | Principe | Quand |
|---|---|---|
| Incorporation | Les données liées sont dans le même document | Lues ensemble, taille bornée, pas de partage (lignes d’une commande) |
| Référence | Un champ contient l’identifiant d’un autre document | Données partagées ou volumineuses (client d’une commande) |
Références et populate
Section intitulée « Références et populate »const schemaCommande = new mongoose.Schema({ numero: String, client: { type: mongoose.Schema.Types.ObjectId, ref: 'Client', required: true, }, totalCentimes: Number,});const Commande = mongoose.model('Commande', schemaCommande);
const commande = await Commande.create({ numero: 'C-1', client: alice._id, totalCentimes: 2500,});Remplacer l’identifiant par le document lié :
const complete = await Commande.findById(commande._id).populate('client');console.log(complete.client.nom);
await Commande.find().populate('client', 'nom email'); // seulement certains champsawait Commande.find().populate({ path: 'client', select: 'nom', match: { actif: true },});populate exécute une requête supplémentaire par chemin. Limiter les champs (select) et éviter les populate en cascade sur de grandes listes.
Populate d’un tableau de références
Section intitulée « Populate d’un tableau de références »const schemaClient = new mongoose.Schema({ nom: String, favoris: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }],});
const client = await Client.findById(id).populate('favoris', 'nom prix');Populate virtuel
Section intitulée « Populate virtuel »Relation inverse sans stocker de tableau d’identifiants dans le parent :
schemaClient.virtual('commandes', { ref: 'Commande', localField: '_id', foreignField: 'client',});schemaClient.set('toJSON', { virtuals: true });
const client = await Client.findById(id).populate('commandes');console.log(client.commandes.length);Pour qu’une relation inverse reste rapide, indexer le champ de référence (client: { …, index: true }).
Choisir : incorporer ou référencer
Section intitulée « Choisir : incorporer ou référencer »| Question | Incorporer | Référencer |
|---|---|---|
| Les données sont toujours lues avec le parent ? | Oui | Non |
| Le nombre d’éléments est borné et petit ? | Oui | Non (liste qui croît sans limite) |
| Les données sont partagées entre plusieurs documents ? | Non | Oui |
| Faut-il les modifier indépendamment ? | Non | Oui |
| Taille maximale d’un document : 16 Mo | À surveiller | Sans objet |
Un tableau qui grossit sans limite (commentaires, journaux) ne doit pas être incorporé.
Supprimer et garder la cohérence
Section intitulée « Supprimer et garder la cohérence »MongoDB n’a pas de clé étrangère : supprimer un client ne supprime pas ses commandes. Gérer la cohérence explicitement :
schemaClient.pre( 'deleteOne', { document: true, query: false }, async function () { await Commande.deleteMany({ client: this._id }); },);
const client = await Client.findById(id);await client?.deleteOne(); // déclenche le hookAgrégation
Section intitulée « Agrégation »Le pipeline d’agrégation transforme les documents par étapes :
const parClient = await Commande.aggregate([ { $match: { statut: 'payee' } }, { $group: { _id: '$client', total: { $sum: '$totalCentimes' }, nombre: { $sum: 1 }, }, }, { $sort: { total: -1 } }, { $limit: 10 }, { $lookup: { from: 'clients', localField: '_id', foreignField: '_id', as: 'client', }, }, { $unwind: '$client' }, { $project: { _id: 0, nom: '$client.nom', total: 1, nombre: 1 } },]);| Étape | Rôle |
|---|---|
$match |
Filtrer (le placer tôt : il utilise les index) |
$group |
Regrouper et calculer ($sum, $avg, $min, $max, $push) |
$sort, $limit, $skip |
Trier, limiter |
$lookup |
Jointure avec une autre collection (nom de collection, pas de modèle) |
$unwind |
Déplier un tableau |
$project |
Choisir et calculer des champs |
$facet |
Plusieurs agrégations en un appel |
Ventes par mois :
const ventes = await Commande.aggregate([ { $match: { passeeLe: { $gte: new Date('2026-01-01') } } }, { $group: { _id: { $dateToString: { format: '%Y-%m', date: '$passeeLe' } }, total: { $sum: '$totalCentimes' }, }, }, { $sort: { _id: 1 } },]);Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
populate ne remplace rien |
Mauvais ref (nom du modèle), valeur absente, ou match qui exclut le document |
$lookup renvoie un tableau vide |
from doit être le nom de la collection (clients), types différents (chaîne et ObjectId) |
| Agrégation sans résultat avec un identifiant | Chaîne au lieu d’ObjectId |
Document trop volumineux (BSONObj size) |
Tableau incorporé qui croît : passer en références |
populate très lent |
Pas d’index sur le champ référencé, trop de documents : limiter et paginer |
MissingSchemaError: Schema hasn't been registered for model "Client" |
Modèle non importé avant le populate |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune propre à Ubuntu. Certains opérateurs d’agrégation dépendent de la version du serveur MongoDB (voir MongoDB : installation).