Archives par étiquette : agregation

MongoDB – lookup et l’agrégation

Cette étape de recherche (lookup) permet d’effectuer l’équivalent d’une jointure externe dans un système de gestion de bases de données relationnelles (SGBDR) et ne peut opérer que sur des collections non shardées situées dans la même base de données.
Look up

Syntaxe de lookup

{
    $lookup:
    {
        from: < collection à joindre >,
        localField: < champ dans les documents de la collection de départ >,
        foreignField: < champ dans les documents de la collection à joindre>,
        as: < nom du tableau qui sera ajouté aux documents du jeu de résultat >
    }
}

Dans chacun des documents reçus en entrée, l’étape $lookup va rajouter un tableau contenant des données en provenance de la collection sur laquelle la jointure a été requise avant de passer ces documents à la prochaine étape du pipeline le cas échéant. Le terme de jointure n’est pas toujours très approprié notamment parce qu’il est possible d’effectuer des requêtes décorrélées, c’est à dire sans clause d’égalité.

Afin de voir $lookup en situation, nous allons utiliser trois collections qui stockeront des élèves, leurs devoirs individuels et les projets sur lesquels ils travaillent à plusieurs.

Elèves, au travail !

Les travaux individuels

Commençons par les travaux individuels de nos élèves, voici les collections impliquées, aucun index n’a été mis pour garder les choses les plus simples possibles mais évidemment qu’il faudra y songer !

db.eleves.insertMany([
    {"nom": "Sébastien Ferrandez", "code": "NAT123"},
    {"nom": "Evelyne Durand", "code": "NAT125"},
    {"nom": "Christian Ton", "code": "NAT120"},
    {"nom": "Claire Annela", "code": "NAT127"}
])

db.devoirs.insertMany([
    {"code": "NAT123", "matiere": "SVT", "note": 12},
    {"code": "NAT123", "matiere": "Maths", "note": 10},
    {"code": "NAT125", "matiere": "Maths", "note": 11.75},
    {"code": "NAT120", "matiere": "Français", "note": 18},
    {"code": "NAT127", "matiere": "Latin", "note": 19}
])

Nous allons partir d’eleves pour aller vers devoirs; dans les deux cas notre nom de champ de « jointure » est code:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   }
])

Voici le résultat produit par ce pipeline, il est assez difficilement lisible mais nous avons l’idée générale : les notes sont « raccrochées » à l’élève !

{
	"_id": ObjectId("5d6e6115d9a18feb0291b605"),
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b60e"),
		"code": "NAT123",
		"matiere": "SVT",
		"note": 12
	}, {
		"_id": ObjectId("5d6e6174d9a18feb0291b60f"),
		"code": "NAT123",
		"matiere": "Maths",
		"note": 10
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b606"),
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b610"),
		"code": "NAT125",
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b607"),
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b611"),
		"code": "NAT120",
		"matiere": "Français",
		"note": 18
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b608"),
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b612"),
		"code": "NAT127",
		"matiere": "Latin",
		"note": 19
	}]
}

Ajoutons une étape project pour rendre cet affichage plus « digeste »…Nous éliminons les id et les champs redondants:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   },
   {
     $project: {
         "_id": 0,
         "detail_notes._id": 0,
         "detail_notes.code": 0
     }
   }
])

Voilà qui est nettement mieux:

{
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"matiere": "SVT",
		"note": 12
	}, {
		"matiere": "Maths",
		"note": 10
	}]
} {
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"matiere": "Français",
		"note": 18
	}]
} {
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"matiere": "Latin",
		"note": 19
	}]
}

Les projets

Voici notre collection projets, elle contient les codes des élèves impliqués ainsi que la note obtenue:

db.projets.insertMany([
    {"codes": ["NAT123", "NAT125"], "matiere": "Dessin", "note": 15}
])

Le champ codes est un tableau ici, pour que chaque élève puisse récupérer sa note, il va falloir éclater ce tableau à l’aide d’unwind avant d’utiliser lookup dessus, nous allons partir de projets cette fois:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Voilà le jeu de résultat lié:

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}]
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}]
}

Nous allons rajouter une étape addFields pour que le tableau contenant un seul document (eleve) devienne un document tout simple, voici le pipeline final:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $addFields: {
       "eleve": { $arrayElemAt: ["$eleve", 0] }
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Et voilà l’travail !

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}
}