Aller au contenu

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)
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 champs
await 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.

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');

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 }).

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é.

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 hook

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 } },
]);
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).