- Introduction
- Présentation de MongoDB
- Lancement de Docker
- Passons à Mongo
- Création d'une base de données
- Introduction aux collections
- Types de données dans MongoDB
- Opérations CRUD sur les collections
- TP Blanc
- Tableaux et sous-documents
- Agrégats
- Recherches textuelles et par Regex
- Recherche Géospatiale
- Introduction à la mise en place de cluster mongodb
- Pour aller plus loins
Dans ce TP, nous allons nous familiariser avec l'utilisation de MongoDB, toujours avec Docker comme support pour faire fonctionner notre palteforme.
MongoDB est un SGBD NoSQL orienté documents. Contrairement à Redis, il ne permet pas de stockage sous la forme clé-valeur, ici, nous parlerons de collections. Une collection est un regroupement de données. On pourrait assimiler les collections à des tables en SQL si l'on mettait de côté un peu de rigueur.
MongoDB est écrit en C++. Il a été conçu par des anciens de chez DoubleClick, une entreprise de publicité en ligne rachetée par Google, pour un besoin spécifique : la montée en charge.
Pour ce nouveau projet, nous allons de nouveau utiliser Docker avec l'utilitaire docker-compose. Notre infrastructure comporte trois conteneurs :
- Un conteneur mongodb en mode base de données,
- Un conteneur mongodb qui nous servira à avoir la ligne de commande,
- Un conteneur Mongo Express, un utilitaire en NodeJS + Express pour visualiser notre base comme on le ferait sur MySQL avec un phpMyAdmin.
Afin de démarrer notre infrastructure, comme pour le TP précédent, nous allons éxécuter la commande suivante :
docker-compose up -d
Une fois l'infrastructure démarrée, vous pouvez vous pouvez ouvrir la ligne de commande avec la commande suivante :
docker-compose run mgcli
En fin de séance, n'oubliez pas d'éteindre l'infrastructure avec la commande appropriée :
docker-compose down
Attention : Cette fois-ci, notre docker est configuré pour faire persister les données ! Si vous corrompez votre base, éteignez votre environement docker, et supprimez le contenu du répertoire mongo/data
. De même, si vous changez de poste, pensez à copier votre répertoire mongo/data
.
Mongo va nous permettre de stocker des données avec une représentation en documents au sein de collections, qui sont stockées dans des bases de données. Ainsi, un serveur mongo va posséder une à plusieurs bases, qui vont posséder une à plusieurs colelctions, qui elles-même vont posséder un ou plusieurs documents. On pourrait définir les équivalences suivantes :
SGBD Relationnel | MongoDB |
---|---|
Base de données | Base de données |
Table | Collection |
Enregistrement (Ligne) | Document |
Colone | Champ |
À tout moment, si vous avez besoin d'afficher l'aide de MongoDB (la liste des commandes disponibles), vous pouvez, dans un shell mongo, éxécuter la commande suivante :
db.help();
Vous aurez peut-être remarqué que la syntaxe de la ligne de commande est identique à JavaScript. Nous allons en effet manipuler notre base à travers des commandes ressemblant à du JavaScript, et elle nous répondra avec des retours ressemblants à du JSON.
Contrairement aux SGBD relationnels que vous avez étudié précédement, ils n'y a pas de commande de création de base de données à proprement parler. Pour créer une base, vous devez vous positionner sur celle-ci et y créer au moins un document.
> use mabase;
switched to db mabase
> db;
mabase
> show dbs;
admin 0.000GB
config 0.000GB
local 0.000GB
Dans l'exemple au dessus, nous avons créé une base mabase
, nous sommes bien psitionné dessus comme l'indique la seconde commande, mais pourtant, lorsque l'on liste les bases disponibles, elle n'y figure pas.
Les collections vont faire office de tables dans MongoDB. Contrairement aux tables SQL, les collections n'ont pas de déclaration. Un document peut parfaitement n'avoir aucune colone de commune avec les autres documents de sa collection.
Nous allons créer une collection vide afin d'y stocker des documents ultérieurement. Avant celà, il est important de se positionner sur la base iut
afin d'y créer notre collection.
use iut;
À ce stade, notre base n'est pas encore créée, car elle ne contient pas de collection. Contrairement aux bases de données, qui doivent avoir du contenu pour exister, on peut créer une collection sans qu'elle ne contienne de documents. Nous allons commencer par créer une collection salles
vide dans notre base iut
.
db.createCollection('salles');
// Retourne : { "ok" : 1 }
Si nous éxécutons maintenant la commande show dbs
, nous verrons que notre base iut
est désormais créée. Nous avons également notre collection salles
de créée.
Il n'est pas nécéssaire de crééer une collection de cette façon, on peut la créer implicitement en y insérant des données :
use iut
// Retourne : switched to db iut
show collections
// Retourne :
// salles
db.profs.insert({nom: "Baumann", prenom: "Maxime", statut: "Vacataire"})
// Retourne : WriteResult({ "nInserted" : 1 })
show collections
// Retourne :
// profs
// salles
Ici, en ajoutant un document dans la collection profs
alors que celle-ci n'existe pas déclenche la création de celle-ci avant d'y insérer le document.
Pour supprimer une collection, il faut utiliser la commande .drop()
précédée par le nom de la collection. Par exemple, pour supprimer notre collection profs
:
show collections
// Retourne :
// profs
// salles
db.profs.drop()
// Retourne : true
show collections
// Retourne :
// salles
Avant de commencer à créer de documents, nous allons jeter un oeil aux différents types de données que MongoDB met à notre disposition pour structurer nos documents :
- Des châines de caractères, elles doivent être valide en UTF-8,
- Des entiers, sur 32 ou 64 bits (
int
oulong
), - Des booléens,
- Des nombres décimaux 64 ou 128 bits (
double
oudecimal
), - Des tableaux,
- Des objets,
- Des dates,
- Des timestamps,
- Du binaire,
- Du JavaScript,
- Des expressions régulières,
- Des valeurs nulles (
null
).
Ceci implique que nos documents peuvent contenir des sous-documents, des tableaux, etc, et ça implique aussi de repenser la structuration de nos données par rapport à un SGBD relationnel.
Mongo nous permet de faire des opérations CRUD (Create, Read, Update, Delete). Nous allons faires quelques opérations CRUD sur notre collection salles
de notre base iut
.
Afin d'insérer un document dans une collection, il faut appeler la commande insert()
sur cette dernière. Ainsi, pour insérer un document dans une collection profs
, nous ferions ainsi :
db.profs.insert({nom: "Baumann", prenom: "Maxime", statut: "Vacataire"})
On peut faire de l'insertion de multiples documents à l'aide de insertMany()
ainsi que d'un seul document avec insertOne()
. insert()
sert à faire de l'insertion peut importe le nombre (1 ou plus). Dans les cas d'insertion multiple, il faut systématiquement passer un tableau de document comme premier paramètre.
À vous de jouer !
Dans la collection
salles
, insérez les données suivantes :
nom postes places TP1 15 28 TP2 16 32 TP3 15 26 TD1 0 30 TD2 0 28 TD3 0 26
Afin de récupérer des documents, nous allons utiliser la commande find()
sur notre collection. Par exemple, pour récupérer les documents de la collection salles
que nous venons de créer, il faudrait faire db.salles.find()
. Notez qu'il existe également une commande findOne()
qui retourne un seul document correspondant aux critères choisis.
Comme avec un SGBD relationnel, nous pouvons faire une projection des champs qui nous intéresse. Par exemple, si nous souhaitons récupérer uniquement le champ nom
, il faut passer la projection en second paramètre : db.salles.find({},{_id: 0, nom: 1})
.
Dans ce dernier exemple, {_id: 0}
indique que nous ne souhaitons pas récupérer l'id de notre élément dans la projection.
Avec Mongo, les projections supportent quelques opérateurs, majoritairement dédiés aux array
. Nous les étudierons ultérieurement.
Nous avons la possibilité de faire des requêtes avec des critères, sur un champ, un champ de sous-document (nous verrons celà plus tard), sur des plages de valeurs, etc.
La première chose que nous allons voir sont les critères sur l'égalité. Pour sélectionner un document avec un champ dont la valeur est égale à une valeur, il faut passer cette égalité comme premier paramètre de la commande find()
.
Par exemple, si nous voulons récupérer les salles qui possèdent 0
postes
: db.salles.find({postes:0})
.
On va également pouvoir faire des conditions sur des valeurs plus petites ou plus grandes. Par exemple, si je veux récupérer les salles avec plus de 10 places, mais avec moins que 30 places : db.salles.find({places: {$gt: 10, $lt: 30}})
.
Il existe d'autres opérateurs similaires à $gt
ou $lt
, nous les expérimenterons au fur et à mesure. En voici la liste complète :
Opérateur | Description |
---|---|
$eq |
Match les valeurs égales à une valeur spécifiée |
$gt |
Match les valeurs strictement supéreieures à une valeur spécifiée |
$gte |
Match les valeurs supérieures ou égales à une valeur spécifiée |
$in |
Match les valeurs présentes dans un ensemble |
$lt |
Match les valeurs strictement inférieures à une valeur spécifiée |
$lte |
Match les valeurs inférieures ou égales à une valeur spécifiée |
$ne |
Match les valeurs différentes à la valeur sépcifiée |
$nin |
Match les valeurs absentes d'un ensemble |
On peut également utiliser des opérateurs logiques pour composer des requêtes plus complexes. Par défaut, lorsqu'on ne précise pas d'opérateur logique, il s'agit d'un opérateur $and
implicite. Avant d'aller plus loin, voici la liste complète des opérateurs logiques :
Opérateur | Syntaxe |
---|---|
$and |
{ $and: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] } |
$not |
{ field: { $not: { <operator-expression> } } } |
$nor |
{ $nor: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } |
$or |
{ $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } |
Il existe également des opérateurs pour vérifier l'existence ou le type de certains champs au sein de documents.
Opérateur | Syntaxe |
---|---|
$exists |
{ field: { $exists: <boolean> } } |
$type |
{ field: { $type: <BSON type> } } ou { field: { $type: [ <BSON type1> , <BSON type2>, ... ] } } |
Il existe encore d'autres types d'opérateurs, mais nous les étudierons plus tard.
À vous de jouer !
À l'aide des opérateurs vus précédement, écrivez les requêtes suivantes :
- Récupérez les salles contenant 15 postes ou moins,
- Récupérez les salles sans postes et avec 28 places ou plus,
- Récupérez les salles sans postes et avec 26 places, ou avec 15 postes et 28 places ou moins,
- Récupérez les salles n'ayant pas moins de 15 postes ou ayant 15 postes ou moins et 28 places.
Lors d'une requête de sélection sur une collection, on peut stocker le curseur du résultat dans une variable JavaScript.
var resultat = db.salles.find()
var document = resultat.hasNext() ? resultat.next() : null
if (document) { printjson(document.nom) }
// Retourne "TP1"
On peut ainsi programmer quelques éléments sur le curseur en JavaScript directement depuis la cli.
Par défaut, le curseur nous renvoie tout les éléments correspondants à nos critères sans garantie sur l'ordre en sortie et sur un format en une ligne par document. On peut modifier ce comportement à l'aide de différentes commandes.
Commande | Description |
---|---|
pretty() |
Permet d'afficher le résultat du cuseur de façon plus lisible |
sort() |
Permet de trier la sortie du curseur sur un ou plusieur champs |
skip() |
Permet de passer un nombre de résultats défini |
limit() |
Permet de limiter le nombre de résultats |
Pour modifier des documents, Mongo nous met à disposition la commande update()
, avec deux variantes, updateOne()
et updateMany()
. Ces trois commandes partagent la même syntaxe et fonctionnent avec les mêmes principes. En premier paramètre, on va avoir la requête permettant de sélectionner le ou les éléments à mettre à jour, en second paramètre, la modification à apporter, et en dernier paramètre, des options.
db.collection.update(<query>, <update>, <options>)
La partie <query>
fonctionne avec les mêmes opérateurs que pour les requêtes de sélection de données. Vous pouvez vous baser sur cette partie du cours pour composer vos différents filtres afin de sélectionner les documents à mettre à jour.
La partie <update>
, elle, dispose de ses propores opérateurs. Ceux-ci décrivent des opérations atomiques. Pour le moment, nous ne verrons que les opérateurs de mise à jour simples.
Opérateur | Action |
---|---|
$currentDate |
Met à jour un champ avec la date courante |
$inc |
Incrémente un champ avec la valeur donnée |
$min |
Met à jour un champ si la nouvelle valeur est plus petite que la valeur courante |
$max |
Met à jour un champ si la nouvelle valeur est plus grande que la valeur courante |
$mul |
Multiplie la valeur d'un champ avec la valeur donnée |
$rename |
Renome un champ dans un document |
$set |
Remplace la valeur d'un champ |
$setOnInsert |
Remplace la valeur d'un champ dans le cas d'une création de document |
$unset |
Supprime un champ d'un document |
L'opérateur $currentDate
permet de mettre à jour un champ en lui donnant la valeur de la date courante.
Syntaxe : { $currentDate: { <field>: <typeSpecification>, ... } }
.
<typeSpecification>
peut prendre deux valeurs :
true
, qui stockera la date courante avec un typeDate
- Un document, soit
{ $type: "timestamp" }
soit{ $type: "date" }
qui définissent explicitement le type. Ce document est sensible à la casse.
L'opérateur $inc
permet d'incrémenter la valeur d'un champ avec un certain pas.
Syntaxe : { $inc: { <field>: <amount>, ... } }
Les opérateurs $min
et $max
permettent de mettre à jour la valeur d'un champ seulement si elle est respectivement plus petite ou plus grande que la valeur du champ avant mise à jour. Si le champ n'existe pas, il est créé et prend la valeur indiquée.
Syntaxe : { $min: { <field>: <value>, ... } }
et { $max: { <field>: <value>, ... } }
L'opérateur $mul
multiplie le contenu d'un champ par la valeur indiquée. Si le champ n'existe pas, il est créé et prend la valeur 0
.
Syntaxe : { $mul: { <field>: <number>, ... } }
L'opérateur $rename
permet de renommer un champ. Si le champ à renommer n'existe pas, il ne se passe rien. Si le champ destination existe déjà, il est supprimé et remplacé.
Syntaxe : {$rename: { <field>: <newName>, ... } }
Les opérateurs $set
et $setonInsert
permettent d'assigner une valeur à un champ. Si la champ n'existe pas, il est créé avec la valeur assignée. Dans le cas de $setOnInsert
, si aucun nouveau document n'est créé, il ne fait rien.
Syntaxe : { $set: { <field>: <value>, ... } }
et { $setOnInsert: { <field>: <value>, ... } }
L'opérateur $unset
permet de supprimer un champ d'un document. Il doit prendre une valeur, mais peut importe la valeur transmise, il supprimera le champ. On met la valeur ""
par convention.
Syntaxe : { $unset: { <field>: "", ... } }
Nous ne regarderons qu'une seule option pour le moment, l'option upsert
. Cette option, quand renseignée à true
, permet de créer un document correspondant à la valeur du paramètre <update>
si aucun document ne remplissait les exigences du paramètre <query>
.
db.salles.updateOne({nom: "TD3"}, {$set: {places: 28}})
Enfin, nous pouvons supprimer des documents de MongoDB à l'aides des commandes deleteOne()
et deleteMany()
. Elles prennent en premier paramètre une <query>
, de la même façon que pour les requêtes de récupération et de mise à jour/
db.salles.updateMany({nom: "TD4"}, {$set: {nom: "TD4", places: 28, postes: 12}}, {upsert: true})
// Retourne :
//{
// "acknowledged" : true,
// "matchedCount" : 0,
// "modifiedCount" : 0,
// "upsertedId" : ObjectId("5bc3740142b6e9f33bce2178")
//}
db.salles.deleteOne({nom: "TD4"})
À vous de jouer !
Pour ce second exercice type TP noté, vous avez le droit à la documentation de MongoDB et de me demander de l'aide. Le projet sera à rendre au plus tard le dimanche 4 novembre à 12h à mon adresse mail.
Le rendu sera sous forme de document texte contenant la suite des commandes que vous aurez éxécutés. Pour chaque question, vous ferez un encart rappelant le numéro de la question comme sur l'exemple suivant :
Question 1.
.Question 1 : Créez une collection
people
dans une base de donnéesanalytics
.Question 2 : À l'aide d'une commande d'insertion, insérez les données suivantes :
technicalId firstname lastname age children 1 Clément Charpentier clement.charpentier@gmail.com 25 2 2 Lauriane Rodriguez lauriane.rodriguez@gmail.com 32 3 3 Amélie Martin amelie.martin@gmail.com 31 1 4 Lola Brunet lola.brunet@gmail.com 27 2 5 Timéo Blanchard timeo.blanchard@gmail.com 43 3 6 Corentin Deschamps corentin.deschamps@gmail.com 15 0 7 Hugo Bernard hugo.bernard@gmail.com 19 0 8 Davy Roy davy.roy@gmail.com 23 0 9 Bruno Robin bruno.robin@gmail.com 35 2 10 Yasmine Maillard yasmine.maillard@gmail.com 47 3 Question 3 : À l'aide d'une méthode de mise à jour de données, insérez les données suivantes :
technicalId firstname lastname age children 11 Zacharis Dufour zacharis.dufour@gmail.com 53 3 12 Renaud Marechal renaud.marechal@gmail.com 48 2 13 Lilou Germain lilou.germain@gmail.com 29 1 14 Maryam Louis maryam.louis@gmail.com 36 2 15 Amandine Adam amandine.adam@gmail.com 21 0 16 Lana Daniel lana.daniel@gmail.com 27 2 17 Clotilde Vincent clotilde.vincent@gmail.com 18 0 18 Benjamin Morin benjamin.morin@gmail.com 36 1 19 Kilian Le gall kilian.le-gall@gmail.com 17 0 20 Noë Germain noe.germain@gmail.com 27 0 Question 4 : Récupérez les personnes de plus de 27 ans et ayant au moins 3 enfants. Passez les deux premiers résultats.
Question 5 : Renommez le champ
technicalId
enid
.Question 6 : Ajoutez un an à toutes les personnes de moins de 20 ans.
Question 7 : Récupérez les prénoms des 5 personnes ayant le plus d'enfants.
Question 8 : Ajoutez un champ
cars
avec une valeur par défaut de2
aux personnes de plus de 30 ans.Question 9 : Sur les documents avec un id technique inférieure ou égal à 10, ajoutez un champ
lastUpdate
avec pour valeur le timestamp courant.Question 10 : Supprimez les personnes ayant entre 25 et 35 ans n'ayant qu'un seul enfant, ainsi que les personnes de plus de 40 ans avec 2 enfants.
Question 11 : Récupérez les 5 personnes les plus agées, par nombre d'enfants décroissant. Rendez le résultat plus lisible.
Question 12 : Supprimez le champ
Question 13 : Récupérez 5 personnes ayant un email en triant par email de façon décroissante et nom de famille croissant, à condition que les personnes aient entre 15 et 20 ans ou 25 et 30 ans.
Question 14 : Récupérez les personnes ayant un email et des voitures et trier par nombre d'enfant décroissant et âge croissant.
Question 15 : Supprimer les personnes ayant deux enfants.
// Question 1 : Créez une collection `people` dans une base de données `analytics`.
use analytics
db.createCollection('people')
// Question 2 : À l'aide d'une commande d'insertion, insérez les données suivantes :
db.people.insertMany([
{ technicalId: 1, firstname: 'Clément', lastname: 'Charpentier', email: 'clement.charpentier@gmail.com', age: 25, children: 2 },
{ technicalId: 2, firstname: 'Lauriane', lastname: 'Rodriguez', email: 'lauriane.rodriguez@gmail.com', age: 32, children: 3 },
{ technicalId: 3, firstname: 'Amélie', lastname: 'Martin', email: 'amelie.martin@gmail.com', age: 31, children: 1 },
{ technicalId: 4, firstname: 'Lola', lastname: 'Brunet', email: 'lola.brunet@gmail.com', age: 27, children: 2 },
{ technicalId: 5, firstname: 'Timéo', lastname: 'Blanchard', email: 'timeo.blanchard@gmail.com', age: 43, children: 3 },
{ technicalId: 6, firstname: 'Corentin', lastname: 'Deschamps', email: 'corentin.deschamps@gmail.com', age: 15, children: 0 },
{ technicalId: 7, firstname: 'Hugo', lastname: 'Bernard', email: 'hugo.bernard@gmail.com', age: 19, children: 0 },
{ technicalId: 8, firstname: 'Davy', lastname: 'Roy', email: 'davy.roy@gmail.com', age: 23, children: 0 },
{ technicalId: 9, firstname: 'Bruno', lastname: 'Robin', email: 'bruno.robin@gmail.com', age: 35, children: 2 },
{ technicalId: 10, firstname: 'Yasmine', lastname: 'Maillard', email: 'yasmine.maillard@gmail.com', age: 47, children: 3 }
])
// Question 3 : À l'aide d'une méthode de mise à jour de données, insérez les données suivantes :
db.people.updateMany({ technicalId: 11 }, { $set: { firstname: 'Zacharis', lastname: 'Dufour', email: 'zacharis.dufour@gmail.com', age: 53, children: 3 } }, { upsert: true })
db.people.updateMany({ technicalId: 12 }, { $set: { firstname: 'Renaud', lastname: 'Marechal', email: 'renaud.marechal@gmail.com', age: 48, children: 2 } }, { upsert: true })
db.people.updateMany({ technicalId: 13 }, { $set: { firstname: 'Lilou', lastname: 'Germain', email: 'lilou.germain@gmail.com', age: 29, children: 1 } }, { upsert: true })
db.people.updateMany({ technicalId: 14 }, { $set: { firstname: 'Maryam', lastname: 'Louis', email: 'maryam.louis@gmail.com', age: 36, children: 2 } }, { upsert: true })
db.people.updateMany({ technicalId: 15 }, { $set: { firstname: 'Amandine', lastname: 'Adam', email: 'amandine.adam@gmail.com', age: 21, children: 0 } }, { upsert: true })
db.people.updateMany({ technicalId: 16 }, { $set: { firstname: 'Lana', lastname: 'Daniel', email: 'lana.daniel@gmail.com', age: 27, children: 2 } }, { upsert: true })
db.people.updateMany({ technicalId: 17 }, { $set: { firstname: 'Clotilde', lastname: 'Vincent', email: 'clotilde.vincent@gmail.com', age: 18, children: 0 } }, { upsert: true })
db.people.updateMany({ technicalId: 18 }, { $set: { firstname: 'Benjamin', lastname: 'Morin', email: 'benjamin.morin@gmail.com', age: 36, children: 1 } }, { upsert: true })
db.people.updateMany({ technicalId: 19 }, { $set: { firstname: 'Kilian', lastname: 'Le gall', email: 'kilian.le-gall@gmail.com', age: 17, children: 0 } }, { upsert: true })
db.people.updateMany({ technicalId: 20 }, { $set: { firstname: 'Noë', lastname: 'Germain', email: 'noe.germain@gmail.com', age: 27, children: 0 } }, { upsert: true })
// Question 4 : Récupérez les personnes de plus de 27 ans et ayant au moins 3 enfants. Passez les deux premiers résultats.
db.people.find({ age: { $gt: 27 }, children: { $gte: 3 } })
// Question 5 : Renommez le champ `technicalId` en `id`.
db.people.updateMany({}, { $rename: { 'technicalId' : 'id' } })
// Question 6 : Ajoutez un an à toutes les personnes de moins de 20 ans.
db.people.updateMany({ age: { $lt: 20 } }, { $inc: { age: 1 } })
// Question 7 : Récupérez les prénoms des 5 personnes ayant le plus d'enfants.
db.people.find({}, { _id: 0, firstname: 1 }).sort({ children: -1 }).limit(5)
// Question 8 : Ajoutez un champ `cars` avec une valeur par défaut de `2` aux personnes de plus de 30 ans.
db.people.updateMany({ age: { $gt: 30 } }, { $set: { cars: 2 } })
// Question 9 : Sur les documents avec un id technique inférieure ou égal à 10, ajoutez un champ `lastUpdate` avec pour valeur le timestamp courant.
db.people.updateMany({ id: { $lte: 10 } }, { $currentDate: { lastUpdate: { $type: 'timestamp' } } })
// Question 10 : Supprimez les personnes ayant entre 25 et 35 ans n'ayant qu'un seul enfant, ainsi que les personnes de plus de 40 ans avec 2 enfants.
db.people.deleteMany({ $or: [{ age: { $gt: 25, $lt: 35 }, children: 1 }, { age: { $gt: 40 }, children: 2 }] })
// Question 11 : Récupérez les 5 personnes les plus agées, par nombre d'enfants décroissant. Rendez le résultat plus lisible.
db.people.find().sort({ age: -1, children: -1}).limit(5).pretty()
// Question 12 : Supprimez le champ `email` des personnes entre 30 et 40 ans.
db.people.updateMany({ age: { $gt: 30, $lt: 40 } }, { $unset: { email: '' } })
// Question 13 : Récupérez 5 personnes ayant un email en triant par email de façon décroissante et nom de famille croissant, à condition que les personnes aient entre 15 et 20 ans ou 25 et 30 ans.
db.people.find({ email: { $exists: true }, $or: [{ age: { $gt: 15, $lt: 20 } }, { age: { $gt: 25, $lt: 30 } }] }).sort({ email: 1, lastname: -1 })
// Question 14 : Récupérez les personnes ayant un email et des voitures et trier par nombre d'enfant décroissant et âge croissant.
db.people.find({ email: { $exists: true }, cars: { $exists: true } }).sort({ children: -1, age: 1 })
// Question 15 : Supprimer les personnes ayant deux enfants.
db.people.deleteMany({ children: 2 })
Jusqu'à présent, nous avons utilisé des objets mongo assez simple, avec une seule dimension dans les champs. Nous allons à présent voir la capacité de mongo à gérer des tableaux ainsi que des sous-documents.
Mongo offre la possibilité de traiter les tableaux comme n'importe quel type avec des opérateurs dédiés aux opérations CRUD. Nous pouvons donc définir des champs de type tableaux dans nos différents documents.
Les tableaux se notent entre crochets ([]
), et peuvent être de tout types.
Si nous reprennons notre exemple avec les salles de l'IUT, nous pouvons faire la requête suivante pour ajouter une salle TP4
avec quelques postes
:
db.salles.insert({ nom: 'TP4', places: 26, postes: ['TP4-01', 'TP4-02', 'TP4-03', 'TP4-04'] })
// Retourne : WriteResult({ "nInserted" : 1 })
db.salles.find({ nom: 'TP4' }).pretty()
// Retourne :
// {
// "_id" : ObjectId("5bddd862d43bed336676b6bf"),
// "nom" : "TP4",
// "places" : 26,
// "postes" : [
// "TP4-01",
// "TP4-02",
// "TP4-03",
// "TP4-04"
// ]
// }
Afin de récupérer finement des données dans un tableau, mongo nous met à disposition de nouveaux opérateurs.
Opérateur | Action |
---|---|
$all |
Match les documents dont les tableaux contenant toutes les valeurs spécifiées |
$elemMatch |
Match les documents dont les tableaux pour lesquels au moins un élément remplit les critères de sélection |
$size |
Match les documents dont les tableaux pour lesquels la longueur correspond à la valeur entrée |
Ces opérateurs ne font pas tout, il y a également un nouvel élément syntaxique : la notation .
. Le .
permet d'accéder à des sous-élements d'un tableau. Nous l'illustrerons plus tard quand nous travaillerons sur les sous-documents, qui utilisent la même syntaxe.
L'opérateur $all
permet de récupérer les document dont le champ de tableau désigné contient toutes les valeurs spécifiées. Si il en manque une seule, le document n'est pas pris, si elles sont toutes présentes et d'autres valeurs avec, le document est pris.
Syntaxe : { <field>: { $all: [ <value1> , <value2> ... ] } }
On peut reproduire son comportement à l'aide de l'opérateur $and
étudié précédement.
db.salles.find({ postes: { $all: ['TP4-01', 'TP4-03'] } })
db.salles.find({ $and: [{ postes: 'TP4-01' }, { postes: 'TP4-03' }] })
// Ces deux commandes sont équivalentes
L'opérateur $elemMatch
permet de récupérer les documents dont le champ de tableau désigné match les critères de sélection (query).
Syntaxe : { <field>: { $elemMatch: { <query1>, <query2>, ... } } }
Exemple :
db.test.insert({ titre: 'Suite quelconque', valeurs: [2, 4, 6, 8, 10, 12, 14] })
db.test.insert({ titre: 'Autre suite', valeurs: [20, 40, 60, 80, 100, 120, 140] })
db.test.find({ valeurs: { $elemMatch: { $gte: 10, $lt: 20 } } })
// Retourne le premier document : le second ne contient pas de valeur supérieures ou égales à 10 et inférieures à 20.
db.test.find({ valeurs: { $elemMatch: { $gt: 10, $lte: 20 } } })
// Retourne les deux documents, ils conetienent biens des valeurs plus grandes que 10 et inférieures ou égales à 20 dans le tableau valeurs.
L'opérateur $size
retourne les documents dont le champ donné est de la taille spécifiée.
Syntaxe : { <field>: { $size: <fieldSize> } }
Mongo propose différents opérateurs de projection, nous allons nous intéressé à seulement trois d'entre eux qui nous seront utile pour les traitements sur les tableaux.
Opérateur | Action |
---|---|
$ |
Permet de projeter le premier élément de tableau remplissant la condition de recherche |
$elemMatch |
Permet de projeter le premier élément de tableau remplissant la condition de recherche en paramètre |
$slice |
Permet de projeter une partie du tableau |
Permet de projeter le premier élément de tableau remplissant la condition de recherche.
Syntaxes :
db.collection.find({ <array>: <value>, ... }, { "<array>.$": 1 })
db.collection.find({ <array.field>: <value>, ... }, { "<array>.$": 1 })
Permet de projeter le premier élément de tableau remplissant la condition de recherche en paramètre.
Syntaxe : db.collection.find({ <field>: <value> }, { <field>: { $elemMatch: <query> } })
Permet de projeter une partie du tableau.
Syntaxe : db.collection.find({ <field>: <value> }, { <array>: { $slice: <count> } })
Remarque : Si le <count>
est positif, la projection affichera ce nombre d'éléments en partant du début du tableau, si il est négatif, il le fera en partant de la fin du tableau.
Pour mettre à jour les tableaux, mongo nous offre de nouveau plusieurs opérateurs.
Opérateur | Action |
---|---|
$ |
Agit comme un placeholder pour mettre à jour le premier élément du tableau satisfaisant la condition |
$[] |
Agit comme un placeholder pour mettre à jour tous les éléments du tableau satisfaisant la condition |
$[<identifier>] |
Agit comme un placeholder pour mettre à jour tous les éléments de tableaux satisfaisant la clause arrayFilters pour le document |
$addToSet |
Ajoute un élément au tableau si il n'existe pas déjà |
$pop |
Retire le premier ou le dernier élément du tableau |
$pull |
Retire tous les éléments du tableau satisfaisant la condition |
$push |
Ajoute un élément dans un tableau |
$pullAll |
Retire toutes les valeurs du tableau spécifié si elles sont présente dans la liste donnée en paramètre |
Pour ces trois opérateurs, nous allons directement nous reporter à la documentation de mongo :
L'opérateur $addToSet
ajoute l'élément passé en paramètre dans le tableau, à condition qu'il ne soit pas déjà présent. Si l'élément en paramètre est un tableau, par défaut, il sera ajouté en tant que sous-tableau de notre tableau.
Syntaxe : { $addToSet: { <field1>: <value1>, ... } }
L'opérateur $pop
retire le premier ou le dernier élément du tableau spécifié. Si le champ n'est pas un tableau, la commande échouée, si le tableau est vide suite à l'opération, il reste en tant que tableau vide.
Syntaxe : { $pop: { <field>: <-1 | 1>, ... } }
Si on passe -1
, le premier élément est retiré, si on passe 1
, le dernier est retiré.
L'opérateur $pull
supprime toutes les occurences des éléments correspondant aux critères de sélection dans un tableau.
Syntaxe : { $pull: { <field1>: <value|condition>, <field2>: <value|condition>, ... } }
Notes :
- Si la valeur a supprimer est un tableau, il faut que le critère de sélection soit exactement le même tableau, incluant l'ordre des éléments.
- Si la valeur est un document, le document devra comporter les mêmes champs, peut importe leur ordre.
L'opérateur $push
permet d'ajouter l'élément en paramètre dans un tableau. Si l'élément en paramètre est un tableau, par défaut, il est inséré en tant que sous-tableau.
Syntaxe : { $push: { <field1>: <value1>, ... } }
L'opérateur $pullAll
permet de supprimer toutes les valeurs listées.
Syntaxe : { $pullAll: { <field1>: [ <value1>, <value2> ... ], ... } }
En plus des opérateurs, nous avons également différents modifiers qui vont permettre de changer le comportement des opérateurs, pricipalement de l'opérateur $push
.
Opérateur | Action |
---|---|
$each |
Modifie les opérateurs $push et $addToSet pour permettre l'insertion de plusieurs valeurs |
$position |
Modifie l'opérateur $push pour permettre l'insertion à une position précise du tableau |
$slice |
Modifie l'opérateur $push pour permettre de limiter la taille finale du tableau |
$sort |
Modifie l'opérateur $push pour trier les éléments du tableau à l'update |
Modifie les opérateurs $push
et $addToSet
pour permettre l'insertion de plusieurs valeurs.
Syntaxes :
{ $addToSet: { <field>: { $each: [ <value1>, <value2> ... ] } } }
{ $push: { <field>: { $each: [ <value1>, <value2> ... ] } } }
Modifie l'opérateur $push
pour permettre l'insertion à une position précise du tableau, avec un index basé sur 0. Si on prend des valeurs négatives, il faut compter en positions avant la fin du tableau. Par exemple, -2
signifie "deux cases avant la dernière case du tableau".
Syntaxe :
{
$push: {
<field>: {
$each: [ <value1>, <value2>, ... ],
$position: <num>
}
}
}
Remarque : Par défaut, $push
ajoute les éléments en fin de tableau, c'est pour celà qu'il n'est pas possible de faire cette action avec ce modifier. Le modifier $position
requiert la présence du modifier $each
.
Modifie l'opérateur $push
pour permettre de limiter la taille finale du tableau. Si on passe on valeur positive, mongo gardera ce nombre d'éléments du tableau en partant du début. Avec une valeur négative, on gardera les derniers éléments du tableau.
Syntaxe :
{
$push: {
<field>: {
$each: [ <value1>, <value2>, ... ],
$slice: <num>
}
}
}
Remarque : Le modifier $slice
requiert la présence du modifier $each
.
Modifie l'opérateur $push
pour trier les éléments du tableau à l'update
Syntaxe :
{
$push: {
<field>: {
$each: [ <value1>, <value2>, ... ],
$sort: <sort specification>
}
}
}
Remarques :
- Pour
<sort specification>
:- Pour faire du tri sur les éléments, quelque soient leur type, il faut passer
1
ou-1
pour faire des tris croissant ou décroissant. - Si les éléments sont des documents, pour tirer sur un champ en particulier, la syntaxe de
<sort specification>
est la suivante :{ <field>: 1 }
ou{ <field>: -1 }
- Pour faire du tri sur les éléments, quelque soient leur type, il faut passer
- Le modifier
$sort
requiert la présence du modifier$each
. - Dans certaines versions de mongo, le modifier
$slice
est requis. Ce n'est pas notre cas.
À l'aide de la notation .
dans les noms de champs, nous allons pouvoir effectuer toutes sortes de requêtes sur des sous-documents et des tableaux de sous-documents.
Tout ce que nous avons vu jusque ici reste entièrement valable pour ces deux éléments.
À vous de jouer !
Dans cet exercice, nous allons entièrement remodelé notre base iut. Dans un premier temps, supprimez entièrement la base iut (via la commande ou l'
IHM
), nous allons ensuite la recréer au propre.Afin de garder notre base relativement simple, nous allons faire les suppositions suivantes :
- Il n'y a que trois
salles
TP de cinqpostes
chacunes- Il n'y a que quatre
enseignants
- Il n'y a que deux
groupes
de dixétudiants
chacuns, enbinômes
- Il n'y a que deux créneaux de cours (matin et après-midi) par jour
- La semaine de cours dure du mardi au jeudi
Pour cet exercice, nous ne modéliserons que la collection
salles
, et le nécéssaire des sous-documents.Les
salles
sont caractérisées par les éléments suivants :
- Un nom
- Des
cours
- Des
postes
Les
postes
sont caractérisés par les éléments suivants :
- Un nom, sous la forme
<nom de la salle TP>-<identifiant à un chiffre>
- Des
OS
(simples chaînes de caractères)- Une
salle
Les
enseignants
sont caractérisés par les éléments suivants :
- Un prénom
- Un nom
- Des
matières
(simples chaînes de caractères)- Des
cours
Les
groupes
sont caractérisés par les éléments suivants :
- Un nom
- Des
étudiants
- Une promotion (nombre entier)
Les
étudiants
sont caractérisés par les éléments suivants :
- Un prénom
- Un nom
- Un
groupe
- Des
cours
- Un
binôme
Les
binômes
sont caractérisés par les éléments suivants :
- Un nom
- Des
étudiants
Les
cours
sont caractérisés par les éléments suivants :
- Un
enseignant
- Un
groupe
- Une
matière
- Une
salle
- Un jour (simple chaîne de caractères)
- Une heure de début (nombre entier)
- Une heure de fin (nombre entier)
Question 1 : Créez trois salles, respectivement nommées TP01, TP02 et TP03.
Question 2 : Ajoutez des postes dans les salles de sorte que :
- La TP01 comporte des postes sous Windows 7 et Ubuntu 14
- La TP02 comporte des postes sous Windows 7 et Centos 7
- La TP03 comporte des postes sous Windows 10 et Ubuntu 14
Question 3 : Placez un cours de
programmation réseau
en TP01 le mercredi de 10h à 13h avecDenis Desroches
et le groupe 2, composé des étudiants suivants :
- Alice Duffet
- Michèle Roy
- Alexis Boivin
- Romain Fluet
- Vincent Fugère
- Coralie Briard
Question 4 : Un étudiant rejoint la promo tardivement, ajoutez le au groupe 2 en triant les étudiants par nom de famille croissant et prénom décroissant : Mathias Fluet.
Question 5 : Les horraires du cours changent, il a maintenant lieu de 9h à 12h.
Question 6 : Récupérez les postes sous Ubuntu 14, en en prenant 3 à partir du second
Question 7 : Récupérez les salles ayant des postes sous Windows 7
Question 8 : Le poste 3 de la salle 1 tombe en panne, retirez-le
Question 9 : Ajoutez les 4 étudiants suivants dans le groupe 2, en veillant à la capacité maximale de la classe :
- Emmeline Faure
- Martin Guimond
- Eugène Quiron
- Gauthier Guibord
Question 10 : Immaginez maintenant que l'on gère l'emploi du temps d'une année de cette façon, quel(s) problème(s) y voyez-vous ?
// Question 1
// On se place sur la base
use iut
// On créée notre collection
db.createCollection('salles')
// On insère les TP01 et TP02
db.salles.insertMany([
{ nom: 'TP01' },
{ nom: 'TP02' }
])
// On insère la TP03 avec un champ poste initialisé à tableau vide, ceci afin de vous montrer comment travailler avec quelques opérateurs particuliers
db.salles.insert({ nom: 'TP03', postes: [] })
// Question 2
// On update notre TP01
db.salles.update({ nom: 'TP01' }, {
$set: { postes: [
{
nom: 'TP01-01',
OS: [
'Windows 7',
'Ubutnu 14'
]
},
{
nom: 'TP01-02',
OS: [
'Windows 7',
'Ubutnu 14'
]
},
{
nom: 'TP01-03',
OS: [
'Windows 7',
'Ubutnu 14'
]
},
{
nom: 'TP01-04',
OS: [
'Windows 7',
'Ubutnu 14'
]
},
{
nom: 'TP01-05',
OS: [
'Windows 7',
'Ubutnu 14'
]
}
]}
})
// Puis on update notre TP02
db.salles.update({ nom: 'TP02' }, {
$set: { postes: [
{
nom: 'TP02-01',
OS: [
'Windows 7',
'Centos 7'
]
},
{
nom: 'TP02-02',
OS: [
'Windows 7',
'Centos 7'
]
},
{
nom: 'TP02-03',
OS: [
'Windows 7',
'Centos 7'
]
},
{
nom: 'TP02-04',
OS: [
'Windows 7',
'Centos 7'
]
},
{
nom: 'TP02-05',
OS: [
'Windows 7',
'Centos 7'
]
}
]}
})
// On va update notre TP03 en deux temps : on va d'abord ajouter les postes,
// mais avec un OS initialisé sur un tableau vide, sauf pour le premier poste
// Notez que l'ordre des poste n'est pas correct, mais que l'on effectue un tri
// et que l'on limite la taille du tableau
db.salles.update({ nom: 'TP03' }, {
$push: {
postes: {
$each: [
{
nom: 'TP03-01',
OS: [
'Windows 10',
'Ubuntu 14'
]
},
{
nom: 'TP03-02',
OS: []
},
{
nom: 'TP03-06',
OS: []
},
{
nom: 'TP03-07',
OS: []
},
{
nom: 'TP03-03',
OS: []
},
{
nom: 'TP03-04',
OS: []
},
{
nom: 'TP03-05',
OS: []
},
{
nom: 'TP03-08',
OS: []
},
{
nom: 'TP03-09',
OS: []
},
{
nom: 'TP03-10',
OS: []
}
],
$slice: 5,
$sort: {
nom: 1
}
}
}
})
// Une fois nos postes ajoutés, on va utiliser l'opérateur `$[]` afin de placer les OS dans les postes
// Notez l'utilisation de `$addToSet` pour éviter d'insérer les valeurs si elles sont déjà présentes
db.salles.update({ nom: 'TP03' }, {
$addToSet: {
'postes.$[].OS': {
$each: [
'Windows 10',
'Ubuntu 14'
]
}
}
})
// On aurait pu procéder comme ceci :
// db.salles.update({ nom: 'TP03' }, {
// $addToSet: {
// 'postes.$[poste].OS': {
// $each: [
// 'Windows 10',
// 'Ubuntu 14'
// ]
// }
// }
// }, {
// arrayFilters: [
// {
// 'poste.nom': { $ne: 'TP03-01' }
// }
// ]
// })
// Question 3
db.salles.update({ nom: 'TP01' }, {
$set: {
cours: [
{
enseignant: {
prenom: 'Denis',
nom: 'Desroches'
},
groupe: 2,
matiere: 'Programmation réseau',
jour: 'mercredi',
debut: 10,
fin: 13,
etudiants: [
{
prenom: 'Alice',
nom: 'Duffet'
},
{
prenom: 'Michèle',
nom: 'Roy'
},
{
prenom: 'Alexis',
nom: 'Boivin'
},
{
prenom: 'Romain',
nom: 'Fluet'
},
{
prenom: 'Vincent',
nom: 'Fugère'
},
{
prenom: 'Coralie',
nom: 'Briard'
}
]
}
]
}
})
// Question 4
db.salles.update({
nom: 'TP01'
}, {
$push: {
'cours.0.etudiants': {
$each: [
{
prenom: 'Mathias',
nom: 'Fluet'
}
],
$sort: {
nom: 1,
prenom: -1
}
}
}
})
// Question 5
db.salles.update({
nom: 'TP01'
}, {
$set: {
'cours.0.debut': 9,
'cours.0.fin': 12
}
})
// Question 6
db.salles.find({
'postes.OS': 'Ubuntu 14'
}, {
_id: 0,
'postes.nom': 1,
postes: {
$slice: [2, 3]
}
})
// Question 7
db.salles.find({
'postes.OS': 'Windows 7'
}, {
_id: 0,
'nom': 1
})
// Question 8
db.salles.update({
nom: 'TP01'
}, {
$pull: {
'postes': {
nom: 'TP01-03'
}
}
})
// Question 9
db.salles.update({
nom: 'TP01'
}, {
$push: {
'cours.0.etudiants': {
$each: [
{
prenom: 'Emmeline',
nom: 'Faure'
},
{
prenom: 'Martin',
nom: 'Guimond'
},
{
prenom: 'Eugène',
nom: 'Quiron'
},
{
prenom: 'Gauthier',
nom: 'Guibord'
},
],
$slice: 10
}
}
})
MongoDB nous met à disposition un pipeline d'agrégation très complet. Il va nous permettre de faire de nombreuses tâches de recoupage de données, de filtrage, de calculs et de jointures.
La première façon d'agréger des données avec MongoDB est l'utilisation de Map-Reduce.
Cette commande va nous permettre d'envoyer des fonctions JavaScript au moteur de MongoDB afin de faire des traitements sur les données.
Bien que très avantageux niveau syntaxique pour un développeur, cette façon de faire reste assez lente sur de très grands volumes de données. C'est pour celà que Mongo a implémenter sa propre pipeline d'agrégation.
Pour les agrégats, nous allons utiliser la commande aggregate()
sur nos collections, ou directement sur notre base.
En mongo, un agrégat est divisé en différentes phases, appelées stages
, qui consistent chacune en un opération d'agrégation de données. Il existe actuellement 23 stages différents pour effecuter ces opérations. La liste complète des stages est disponible dans la documentation.
En plus de ces stages, nous avons à disposition un peu plus de 200 opérateurs, détaillés dans la documentation. Une partie d'entre eux possède plusieurs définitions selon le type d'objets sur lequel effectuer des traitements, d'autres sont des opérateurs très similaires à tout ceux que nous avons pu voir jusqu'à présent, et une grande partie consistent en des fonctions mathématiques ou de traitements de tableaux.
Dans le cadre de ce cours, nous n'allons par voir tous ces stages et opérateurs de façon exhaustive, cependant, nous allons nous familiariser avec les opérateurs suivants :
$addFields
$bucket
$count
$facet
$limit
$lookup
$match
$out
$project
$skip
$sort
$sortByCount
$unwind
Je mettrai cette partie en ligne ultérieurement, en complément du cours.
Je mettrai cette partie en ligne ultérieurement, en complément du cours.
Je mettrai cette partie en ligne ultérieurement, en complément du cours.
Nous avons commencé à nous familiariser avec MongoDB et ses fonctionnalités de base au long de ce cours. Ceci nous a permis d'avoir une première approche d'un SGBD NoSQL orienté documents. Il existe de nombreux autre SGBD NoSQL basés sur ce même principe, avec des différences sur le langages de requêtage, des différences sur la gestion des données en mode répartit, sur la gestion du volume, etc. Les "concurents" les plus connus sont Couchbase Server, CouchDB et CosmosDB (disponible en tant que service Cloud Microsoft Azure).