2. Attributs de type liste

  • Auteurs/trices : Malo REYNES, Lucas ROBIN, Fiona TUFFIN

Ce chapitre traite des attributs de type liste et des différents types de requêtes que l’on peut vouloir faire sur de tels attributs

2.1. Introduction

En MongoDB, un document est composé de couples clé/valeur. Une clé peut être considérée comme le nom d’une variable (attribut) à laquelle correspond une valeur pour un individu. L’attribut peut être de plusieurs types : chaîne de caractères, booléen, nombre, liste ou date. C’est aux attributs de type liste que nous nous intéressons ici. En MongoDB comme en python, une liste est, comme son nom l’indique, une série de valeurs, ces valeurs pouvant être de tous types. Une liste peut également contenir des sous-listes. Il est possible de réaliser plusieurs opérations sur une liste telles qu’obtenir sa taille, récupérer son minimum, son maximum, sa moyenne et autres. Il faut toutefois faire attention à certains “pièges” que nous exposerons.

Les exemples pour cette partie concernent les listes de notes des élèves de la collection notes de la base de données etudiants. On notera aussi que certains types de requêtes comme les sum() ou les avg() nécessitent des requêtes d’agrégation qui seront évoquées dans un chapitre suivant.

use etudiants
switched to db etudiants

2.2. Fonctionnement des listes en MongoDB

Afin de mieux appréhender les listes en MongoDB, nous allons suivre un exemple au cours du quel, étape par étape, nous expliquerons notre démarche. Nous souhaitons connaître les étudiants ayant toutes leurs notes supérieures ou égales à 12.

Remarque : les notes sont implémentées sous forme de liste dans la base de données (attribut notes). Parmi les 7 étudiants, un ne possède pas d’attribut notes et un autre à ce même attribut vide (liste contenant 0 élément). Nous allons donc traiter ces cas particuliers.

2.2.1. Opérateur $size

Introduisons tout d’abord un élément utile pour comprendre le fonctionnement des listes : l’opérateur $size. Il renvoie les documents dont la taille (nombre d’éléments de la liste) vérifie la condition donnée.

db.notes.find(
    {"notes": {$size: 2}}                     /*Listes de 2 éléments*/
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d7741"),
	"nom" : "Marc",
	"notes" : [
		1,
		5
	],
	"ddn" : ISODate("1993-01-01T00:00:00Z"),
	"sexe" : "M"
}

Attention, cet opérateur n’est pas compatible avec les intervalles de valeurs. On ne peut pas écrire le code suivant (qui renvoie une erreur) :

db.notes.find(
    {"notes": {$size: {$lte: 2}}}             /*Ne fonctionne pas, la taille doit être une valeur précise !!!*/
)
Error: error: {
	"ok" : 0,
	"errmsg" : "$size needs a number",
	"code" : 2,
	"codeName" : "BadValue"
}

Il faudra plutôt écrire :

db.notes.find(
    {$or : 
        [{"notes": {$size: 2}},
        {"notes": {$size: 1}},
        {"notes": {$size: 0}}]
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d773f"),
	"nom" : "Hélène",
	"notes" : [
		13
	],
	"ddn" : ISODate("1995-03-05T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7741"),
	"nom" : "Marc",
	"notes" : [
		1,
		5
	],
	"ddn" : ISODate("1993-01-01T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d7740"),
	"nom" : "Sophie",
	"notes" : [ ],
	"ddn" : ISODate("1996-09-12T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Exemple : on veut connaitre les notes de l’étudiant nommé Paul.

db.notes.find(
    {"nom": "Paul"}, 
    {"notes": true}
)
{ "_id" : ObjectId("56011920de43611b917d773d"), "notes" : [ 10, 12 ] }

Une liste de 2 notes est retournée. Plus précisement, c’est le contenu de l’attribut notes qui est donné.

2.3. Particularité du travail sur des listes

Les listes sont des objets particuliers pour lesquels des questions particulières se posent. Nous nous penchons ici sur deux de ces questions spécifiques aux listes.

2.3.1. “au moins un élément” ou “tous les éléments” ?

Exemple : on veut savoir si l’étudiant Paul a eu au moins une note égale à 12. Pour ce faire, on ajoute une condition sur les notes.

db.notes.find(
    {"nom": "Paul", "notes": 12}
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}

La requête ressort un seul document : celui correspondant à l’étudiant Paul de notre collection. Cela signifie donc que Paul a eu au moins une note égale à 12. Néanmoins, l’objet retourné par la requête est l’ensemble du document. Toutes les notes de Paul sont données, même celles différentes de 12.

Voyons maintenant ce qui se passe lorsqu’on recherche les notes supérieures ou égales à 12.

db.notes.find(
    {"notes": {$gte: 12}}
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d773f"),
	"nom" : "Hélène",
	"notes" : [
		13
	],
	"ddn" : ISODate("1995-03-05T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d773c"),
	"nom" : "Jean",
	"notes" : [
		1,
		5,
		7,
		10,
		12,
		14,
		3
	],
	"ddn" : ISODate("1995-05-25T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Avec cette requête, nous obtenons 4 éléments correspondant aux 4 étudiants qui ont eu au moins une note supérieure ou égale à 12.

Nous voudrions maintenant ressortir les individus qui n’ont que des notes supérieures ou égales à 12. Pour cela, nous pouvons retirer tous les étudiants ayant eu des notes en dessous de 12. Pour ce faire, nous utilisons l’opérateur logique $not qui retire les documents ne réalisant pas la condition demandée.

db.notes.find(
    {"notes":{$not: {$lt: 12}}}              /*Une condition : on enlève les étudiants qui ont au moins une note plus petite que 12*/
)
{
	"_id" : ObjectId("56011920de43611b917d773e"),
	"nom" : "Michel",
	"ddn" : ISODate("1995-02-13T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d773f"),
	"nom" : "Hélène",
	"notes" : [
		13
	],
	"ddn" : ISODate("1995-03-05T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7740"),
	"nom" : "Sophie",
	"notes" : [ ],
	"ddn" : ISODate("1996-09-12T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Problème : la requête nous renvoie également les étudiants qui n’ont pas eu de note. C’est logique : si Sophie n’a pas de note, on ne peut pas dire qu’elle ait déjà eu moins que 12. Nous allons donc retirer les étudiants sans notes.

Pour se faire, nous utilisons l’opérateur logique $nor en listant les éléments à ne pas prendre en compte. Nous ne voulons pas que la liste notes soit vide ou qu’elle comporte ne serait-ce qu’une note inférieure à 12.

db.notes.find(
    {$nor: 
        [{"notes": {$lt: 12}},               /*1ère condition : on retire ceux qui ont des notes en dessous de 12*/
        {"notes": {$size: 0}}]               /*2nde condition : on retire ceux qui n'ont pas de notes*/
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773e"),
	"nom" : "Michel",
	"ddn" : ISODate("1995-02-13T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d773f"),
	"nom" : "Hélène",
	"notes" : [
		13
	],
	"ddn" : ISODate("1995-03-05T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Nouveau problème : le document correspondant à l’étudiant Michel est renvoyé parce qu’il n’a pas d’attribut notes. Dans ces conditions, on remarque que les listes vides ou inexistantes sont retournées par les requêtes. Il est important de les enlever en rajoutant une condition dans le $nor.

Il faut donc retirer les étudiants qui ont une liste notes vide mais aussi ceux qui n’ont pas de liste du tout.

db.notes.find(
    {$nor: 
        [{"notes": {$exists: false}},         /*1ère condition : on retire les documents ne possédant pas de liste "notes"*/
        {"notes": {$lt: 12}},                 /*2ème condition : on retire ceux qui ont des notes en dessous de 12*/
        {"notes": {$size: 0}}]                /*3ème condition : on retire ceux qui n'ont pas de notes*/
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773f"),
	"nom" : "Hélène",
	"notes" : [
		13
	],
	"ddn" : ISODate("1995-03-05T00:00:00Z"),
	"sexe" : "F"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Cette fois, on ne retourne plus que 2 étudiants qui n’ont que des notes au-dessus de 12.

2.3.2. Cas des conditions multiples

Lorsque nous faisons des requêtes sur un attribut d’un autre type qu’une liste, un seul élement est soumis à l’ensemble de nos conditions. Dans l’exemple ci-dessous, la clé nom renvoie une chaine de caractères, qui est un élément unique. Cet élément est soumis à deux conditions afin d’obtenir les noms qui commencent par la lettre M.

db.notes.find(                                /*Cette requête nous renvoie les noms dont la première lettre est comprise >= à M et < à N, donc M*/
    {"nom": 
        {$gte: "M", $lt: "N"}
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773e"),
	"nom" : "Michel",
	"ddn" : ISODate("1995-02-13T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d7741"),
	"nom" : "Marc",
	"notes" : [
		1,
		5
	],
	"ddn" : ISODate("1993-01-01T00:00:00Z"),
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d7742"),
	"nom" : "Marc",
	"notes" : [
		15
	],
	"ddn" : ISODate("1993-03-06T00:00:00Z"),
	"sexe" : "M"
}

Avec les listes, c’est différent. Chaque élément contenu dans la liste est testé par les conditions. Voyons le fonctionnement d’une requête sur une liste avec plusieurs conditions :

db.notes.find(
    {"notes": 
        {$gt: 13, $lte: 10}
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773c"),
	"nom" : "Jean",
	"notes" : [
		1,
		5,
		7,
		10,
		12,
		14,
		3
	],
	"ddn" : ISODate("1995-05-25T00:00:00Z"),
	"sexe" : "M"
}

Cette requête teste pour chaque élément de la liste un à un :

  • La condition >13;

  • La condition 10;

Si chacune des conditions est vérifiée au moins une fois, la liste complète est renvoyée. En clair, si au moins un élément de la liste est >13 et au moins un élément est 10, les conditions sont considérées comme validées.

Ainsi, à la liste “[1,5,7,10,12,14,3]” correspond :

  • [F,F,F,F,F,T,F] pour la première condition

  • [T,T,T,T,F,F,T] pour la seconde Les conditions sont toutes respectées au moins une fois, la liste complète est donc renvoyée.

Ainsi, nous ne testons pas simultanément les deux conditions sur chaque nombre. Aucun nombre x ne vérifie x >13 et x 10. Cela est contre-intuitif, il faut faire attention.

Mais alors, comment pouvons-nous justement tester une double condition sur chaque élement de la liste ? Pour cela, nous allons faire appel à l’opérateur $elemMatch.

2.3.3. Opérateur $elemMatch

Avec $elemMatch, on retourne les documents dont au moins un élément de la liste vérifie toutes les conditions.

2.3.3.1. Cas de conditions simultanement non réalisables

Testez votre intuition ! D’après vous, que ressortira cette requête ?

db.notes.find(
    {"notes": 
        {$elemMatch: 
            {$gt: 13, $lte: 10}
        }
    }
)

Contrairement à la requête précedente, cette requête teste les élements de la liste un à un. Ainsi, aucun élement ne vérifie ces deux conditions simultanement. Aucun élément n’est donc retourné par cette requête.

2.3.3.2. Cas de conditions simultanement réalisables

Avec la requête suivante, nous cherchons à savoir quels étudiants ont au moins une note comprise entre 9 et 13.

db.notes.find(
    {"notes": 
        {$elemMatch: 
            {$gt: 9, $lt: 13}
        }
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}
{
	"_id" : ObjectId("56011920de43611b917d773c"),
	"nom" : "Jean",
	"notes" : [
		1,
		5,
		7,
		10,
		12,
		14,
		3
	],
	"ddn" : ISODate("1995-05-25T00:00:00Z"),
	"sexe" : "M"
}

Attention : on renvoie bien ici les listes dont au moins une valeur vérifie l’ensemble des conditions ! Les notes validant les deux conditions sont 10, 11 et 12. Par exemple, la liste [1,3,8,11,15] sera retournée mais la liste [1,3,8,15] ne le sera pas.

Comment obtenir les étudiants dont toutes les notes vérifient les conditions simultanement ? Cela est réalisable grâce à l’opérateur $nor vu plus tôt :

db.notes.find(
    {$nor: 
        [{"notes": {$exists: false}},         /*1ère condition : on retire les documents ne possédant pas de liste "notes"*/
        {"notes": {$size: 0}},                /*2ème condition : on retire ceux qui n'ont pas de notes*/
        {"notes": {$lte: 9}},                 /*3ème condition : on retire les étudiants qui ont des notes en dessous de 10*/
        {"notes": {$gte: 13}}]                /*4ème condition : on retire ceux qui ont des notes au dessus de 12*/
    }
)
{
	"_id" : ObjectId("56011920de43611b917d773d"),
	"nom" : "Paul",
	"notes" : [
		10,
		12
	],
	"sexe" : "M"
}

2.4. Récapitulatif

  • Je souhaite que toutes les conditions soient vérifiées au moins une fois par les éléments de ma liste, comment faire ?

Code “classique”.

Sans $elemMatch, si les conditions sont vérifiées une à une, que ce soit par un élément de la liste ou grâce à plusieurs éléments distincts, alors le document est retourné.

  • Je souhaite que toutes les conditions soient simultanement vérifiées par au moins un élément de ma liste, comment faire ?

Utilisation de l’opérateur $elemMtach.

Avec $elemMatch, on regarde tous les éléments de la liste un par un et on retourne le document si et seulement si au moins un élément est capable de vérifier toutes les conditions à lui tout seul.

  • Je souhaite que toutes les conditions soient vérifiées par tous les éléments de ma liste, comment faire ?

Utilisation de l’opérateur $nor.

Avec $nor, on liste les conditions que nous ne souhaitons pas retourner. Ainsi, on ne récupère pas les éléments qui valident des conditions. Il faut notamment penser à retirer les éléments vides avec {$size: 0} et les éléments inexistants avec {$exists: false}.