Logo OFPPT

ISMO - Institut Spécialisé dans les Métiers de l'Offshoring

OFPPT - Office de la Formation Profesionnelle et de la Promotion du Travail

Maîtrise de MongoDB : De la modélisation NoSQL au Framework d'Agrégation

Dernière mise à jour : Novembre 2025

Partie 1 : Fondamentaux de MongoDB et du NoSQL

Chapitre 1 : Introduction aux bases de données NoSQL

Bienvenue dans ce parcours dédié à MongoDB. Alors que les bases de données relationnelles (SGBDR) ont longtemps dominé le paysage informatique, l'ère du Big Data, avec ses fameux "3V" (Volume, Vélocité, Variété), a mis en lumière leurs limites. C'est dans ce contexte qu'a émergé le mouvement NoSQL (Not Only SQL), proposant de nouvelles manières de stocker et d'interroger les données, mieux adaptées aux architectures distribuées et aux données non structurées.

Flexibilité du Schéma

Contrairement à la structure rigide des tables SQL, les bases NoSQL comme MongoDB utilisent un schéma flexible. Il n'est pas nécessaire de définir la structure de toutes les données à l'avance, ce qui permet des développements plus agiles et une meilleure adaptation aux changements.

Scalabilité Horizontale

Les bases NoSQL sont conçues pour être distribuées sur plusieurs serveurs. Pour gérer une charge plus importante, il suffit d'ajouter de nouvelles machines (scalabilité horizontale), une approche souvent plus économique et résiliente que l'augmentation de la puissance d'un unique serveur (scalabilité verticale).

Chapitre 2 : Le modèle orienté document (JSON/BSON)

MongoDB est une base de données orientée document. Elle ne stocke pas les données dans des tables avec des lignes et des colonnes, mais dans des collections de documents. Un document est une structure de données composée de paires clé-valeur, similaire au format JSON (JavaScript Object Notation).

En interne, MongoDB utilise BSON (Binary JSON), une représentation binaire du JSON qui est plus performante et supporte des types de données supplémentaires comme les dates, les entiers de 32/64 bits, les timestamps ou les données binaires.

2.1. Anatomie d'un document MongoDB

Un document est un ensemble de paires clé-valeur, délimité par des accolades `{}`. Les clés sont des chaînes de caractères et les valeurs peuvent être de différents types BSON.

{
    "_id": ObjectId("635ab3a..."),
    "nom": "Radi",
    "prenom": "Abdessalam",
    "genre": "homme",
    "nbMedailles": Int32(4), // Exemple de type BSON
    "sport": {
        "description": "marathon",
        "olympique": true
    },
    "disciplines": [ "1500m", "5000m", "marathon" ]
}

Chapitre 3 : Installation et Outils de l'écosystème

Pour travailler avec MongoDB, plusieurs composants sont nécessaires. L'écosystème MongoDB fournit une suite d'outils puissants pour gérer vos bases de données.

3.1. Les composants clés

  • MongoDB Server (`mongod`) : Le processus principal de la base de données. C'est le démon qui gère les données, les requêtes et les accès.
  • MongoDB Shell (`mongosh`) : Une interface en ligne de commande interactive pour administrer la base et exécuter des requêtes en JavaScript.
  • MongoDB Compass : Une interface graphique (GUI) qui permet de visualiser, d'interroger et d'analyser vos données de manière intuitive.
  • Database Tools : Une suite d'outils en ligne de commande pour l'import/export de données (`mongoimport`, `mongoexport`) et la sauvegarde/restauration (`mongodump`, `mongorestore`).

Chapitre 4 : Les opérations d'écriture (Insert, BulkWrite)

Créer des données est la première étape fondamentale. MongoDB offre des commandes flexibles pour insérer un ou plusieurs documents à la fois, ainsi qu'une méthode puissante pour exécuter plusieurs opérations d'écriture en un seul lot.

4.1. Insérer un seul document (`insertOne`)

Pour insérer un unique document dans une collection, on utilise la méthode `insertOne()`. Si la collection n'existe pas, MongoDB la crée automatiquement.

db.Sportif.insertOne({
    _id: "sp11",
    nom: "Ziyech",
    prenom: "Hakim",
    genre: "homme",
    sport: { description: "football", olympique: true }
})

4.2. Insérer plusieurs documents (`insertMany`)

Pour insérer une liste de documents en une seule opération, on utilise `insertMany()`, ce qui est beaucoup plus performant que d'appeler `insertOne()` en boucle. La méthode prend un tableau de documents en argument.

db.Sportif.insertMany([
    { _id: "sp12", nom: "Hakimi", prenom: "Achraf", genre: "homme", sport: { description: "football", olympique: true } },
    { _id: "sp13", nom: "Bounou", prenom: "Yassine", genre: "homme", sport: { description: "football", olympique: true } }
])

4.3. Opérations en masse (`bulkWrite`)

La méthode `bulkWrite()` est l'outil le plus puissant pour les opérations d'écriture. Elle permet de combiner des insertions, des mises à jour et des suppressions en une seule requête envoyée au serveur, réduisant ainsi la latence réseau et améliorant considérablement les performances.

Elle prend en argument un tableau d'opérations. Chaque opération est un objet spécifiant le type d'action (`insertOne`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `replaceOne`) et ses paramètres.

db.Sportif.bulkWrite([
    { 
        insertOne: { 
            document: { _id: "sp14", nom: "Amrabat", prenom: "Sofyan", genre: "homme" } 
        } 
    },
    { 
        updateOne: {
            filter: { nom: "Ziyech" },
            update: { $set: { "sport.description": "football pro" } }
        }
    },
    {
        deleteOne: {
            filter: { _id: "sp9" } // On supprime le sportif "Belafrikh"
        }
    }
])

Ateliers Pratiques : Chapitre 4

Créons notre base de données et insérons notre premier jeu de données complet.

Exercice 1 : Créer une base et une collection

En utilisant `mongosh`, basculez sur une nouvelle base de données `DBSportifs`. La commande `use` suffit, la base sera créée physiquement lors de la première insertion.

// 1. Se connecter à une base de données (la crée si elle n'existe pas)
use DBSportifs

// 2. Vérifier la base de données actuelle
db

Exercice 2 : Insérer le jeu de données initial

Téléchargez le fichier de données des sportifs : dbs/sportif.json.
Utilisez la commande `insertMany()` pour insérer tous les sportifs de ce fichier dans votre collection `Sportif`.

db.Sportif.insertMany([
  {
    "_id": "sp1", "nom": "Radi", "prenom": "Abdessalam", "genre": "homme",
    "sport": { "description": "marathon", "olympique": true }
  },
  {
    "_id": "sp2", "nom": "Larbi", "prenom": "Benmbarek", "genre": "homme",
    "sport": { "description": "football", "olympique": true }
  },
  {
    "_id": "sp3", "nom": "ELGourch", "prenom": "Mohamed", "genre": "homme",
    "sport": { "description": "cyclisme", "olympique": true }
  },
  {
    "_id": "sp4", "nom": "Bidouane", "prenom": "Nezha", "genre": "femme",
    "sport": { "description": "athletisme", "olympique": true }
  },
  {
    "_id": "sp5", "nom": "ELGaraa", "prenom": "Najat", "genre": "femme",
    "sport": { "description": "athletisme", "olympique": true }
  },
  {
    "_id": "sp6", "nom": "Rabii", "prenom": "Mohamed", "genre": "homme",
    "sport": { "description": "box", "olympique": true }
  },
  {
    "_id": "sp7", "nom": "El Guerrouj", "prenom": "Hicham", "genre": "homme",
    "sport": { "description": "athletisme", "olympique": true }, "nbMedailles": 3
  },
  {
    "_id": "sp8", "nom": "Abissourour", "prenom": "Sara", "genre": "femme",
    "sport": { "description": "volley ball", "olympique": true }
  },
  {
    "_id": "sp9", "nom": "Belafrikh", "prenom": "Amine", "genre": "homme",
    "sport": { "description": "Muay Thai", "olympique": false }
  },
  {
    "_id": "sp10", "nom": "Moutawakil", "prenom": "Naoual", "genre": "femme",
    "nbMedailles": 4, "sport": { "description": "athletisme", "olympique": true }
  }
])

Exercice 3 : Pratiquer `bulkWrite`

Exécutez les opérations suivantes en une seule commande `bulkWrite` :
1. Insérer un nouveau sportif : `_id: "sp15", nom: "Saibari", prenom: "Ismael", genre: "homme"`.
2. Mettre à jour Hicham El Guerrouj (`sp7`) pour corriger son sport en "athletisme" (il était noté "box" par erreur).
3. Augmenter le nombre de médailles de Nezha Bidouane (`sp4`) de 1 (elle n'en avait pas, il faut donc lui en mettre 1).
4. Supprimer le sportif `sp2` (Larbi Benmbarek).

db.Sportif.bulkWrite([
    {
        insertOne: {
            document: { _id: "sp15", nom: "Saibari", prenom: "Ismael", genre: "homme", sport: { description: "football", olympique: true } }
        }
    },
    {
        updateOne: {
            filter: { _id: "sp7" },
            update: { $set: { "sport.description": "athletisme" } }
        }
    },
    {
        updateOne: {
            filter: { _id: "sp4" },
            update: { $set: { nbMedailles: 1 } }
        }
    },
    {
        deleteOne: {
            filter: { _id: "sp2" }
        }
    }
])

Ateliers Sommatifs (Partie 1)

Vérifions les acquis de cette première partie sur les fondamentaux et l'insertion de données.

Exercice 1 : Vérification des données

Après avoir exécuté tous les exercices précédents, combien de documents votre collection `Sportif` contient-elle ? Utilisez une commande pour le vérifier.

// Compte le nombre total de documents dans la collection.
db.Sportif.countDocuments()

// Ou, une méthode plus ancienne mais toujours fonctionnelle :
db.Sportif.find().count()

Exercice 2 : Opération d'écriture complexe

Créez une nouvelle collection `Equipements`. En une seule commande `bulkWrite`, effectuez les actions suivantes :
1. Insérez 3 équipements : `{ _id: "eq1", type: "Velo", marque: "B-Twin" }`, `{ _id: "eq2", type: "Gants de Box", marque: "Everlast" }`, `{ _id: "eq3", type: "Chaussures de course", marque: "Nike" }`.
2. Immédiatement après, modifiez l'équipement `eq1` pour ajouter un champ `modele: "Rockrider"`.

db.Equipements.bulkWrite([
    { insertOne: { document: { _id: "eq1", type: "Velo", marque: "B-Twin" } } },
    { insertOne: { document: { _id: "eq2", type: "Gants de Box", marque: "Everlast" } } },
    { insertOne: { document: { _id: "eq3", type: "Chaussures de course", marque: "Nike" } } },
    { updateOne: {
        filter: { _id: "eq1" },
        update: { $set: { modele: "Rockrider" } }
    }}
])

Partie 2 : Interrogation Avancée des Données

Chapitre 5 : Lire les données avec find()

La commande la plus fondamentale pour lire des données dans MongoDB est `find()`. Elle permet de retrouver des documents au sein d'une collection. Utilisée sans argument, elle retourne tous les documents. Pour affiner la recherche, on lui passe un document de filtrage en premier argument.

5.1. Syntaxe de base

// Retrouver tous les documents
db.collection.find({})

// Retrouver les documents correspondant à un critère
db.collection.find({ champ: "valeur" })

// Exemple : trouver tous les sportifs de genre "homme"
db.Sportif.find({ "genre": "homme" })

// Interroger un champ dans un sous-document
db.Sportif.find({ "sport.description": "cyclisme" })

Ateliers Pratiques : Chapitre 5

Utilisons la collection `Sportif` pour mettre en pratique les requêtes de base avec `find()`.

Exercice 1

Quels sont les sportifs de genre « femme » ?

db.Sportif.find({genre: "femme"})

Exercice 2

Quel(s) sont les sportifs qui pratiquent le cyclisme ?

db.Sportif.find({"sport.description": "cyclisme"})

Chapitre 6 : Opérateurs de requête et filtrage avancé

Pour construire des requêtes complexes, MongoDB offre une panoplie d'opérateurs de requête, reconnaissables à leur préfixe `$`.

6.1. Opérateurs de comparaison

Ils permettent de filtrer les documents en comparant la valeur d'un champ.

  • `$eq` : égal à (souvent implicite).
  • `$ne` : non égal à.
  • `$gt` : supérieur à (greater than).
  • `$gte` : supérieur ou égal à.
  • `$lt` : inférieur à (less than).
  • `$lte` : inférieur ou égal à.
  • `$in` : la valeur est dans un tableau de possibilités.
  • `$nin` : la valeur n'est dans aucun élément du tableau.
// Sportifs ayant plus de 2 médailles
db.Sportif.find({ "nbMedailles": { $gt: 2 } })

// Sportifs pratiquant le 'box' ou le 'cyclisme'
db.Sportif.find({ "sport.description": { $in: [ "box", "cyclisme" ] } })

6.2. Opérateurs logiques

Ils combinent plusieurs conditions.

  • `$and` : ET logique (souvent implicite en séparant les champs par une virgule).
  • `$or` : OU logique.
  • `$not` : Inverse une condition.
// Sportifs de genre "femme" OU qui ont plus de 3 médailles
db.Sportif.find({
    $or: [
        { "genre": "femme" },
        { "nbMedailles": { $gt: 3 } }
    ]
})

6.3. Opérateur d'élément (`$exists`)

Cet opérateur permet de sélectionner les documents en fonction de la présence ou de l'absence d'un champ. C'est très utile dans un schéma flexible où tous les documents n'ont pas forcément les mêmes champs.

// Trouver les sportifs qui ont un champ "nbMedailles"
db.Sportif.find({ "nbMedailles": { $exists: true } })

// Trouver les sportifs qui N'ONT PAS de champ "nbMedailles"
db.Sportif.find({ "nbMedailles": { $exists: false } })

Ateliers Pratiques : Chapitre 6

Appliquons ces opérateurs de filtrage à notre collection `Sportif`.

Exercice 1

Afficher les sportifs qui ont exactement 4 médailles.

db.Sportif.find({nbMedailles: 4})

Exercice 2

Afficher les sportifs qui pratiquent les sports suivants : box, athletisme et cyclisme.

db.Sportif.find({"sport.description": {$in: ["box", "athletisme", "cyclisme"]}})

Exercice 3

Afficher les sportifs qui n'ont pas de médailles (champ `nbMedailles` manquant).

db.Sportif.find({nbMedailles: {$exists: false}})

Chapitre 7 : Projection, Tri et Pagination

Après avoir filtré les documents, il est souvent nécessaire de formater le résultat : choisir les champs à afficher (projection), trier les documents et n'en retourner qu'un sous-ensemble (pagination).

7.1. Projection : Choisir les champs à afficher

La projection est le second argument de la méthode `find()`. C'est un document où l'on spécifie les champs à inclure (avec la valeur `1`) ou à exclure (avec la valeur `0`). Par défaut, le champ `_id` est toujours inclus, sauf si on l'exclut explicitement.

// Afficher uniquement le nom et prénom des sportifs, sans leur _id
db.Sportif.find(
    {}, // Filtre vide pour tout sélectionner
    { "nom": 1, "prenom": 1, "_id": 0 } // Document de projection
)

7.2. Tri (`sort`) et Pagination (`limit`, `skip`)

Les méthodes `sort()`, `limit()` et `skip()` se "chaînent" après un `find()` pour manipuler le curseur de résultat.

// Afficher les 3 sportifs ayant le plus de médailles, triés par ordre décroissant
db.Sportif.find({ "nbMedailles": { $exists: true } })
    .sort({ "nbMedailles": -1 }) // -1 pour décroissant, 1 pour croissant
    .limit(3) // Ne retourner que 3 documents

Ateliers Pratiques : Chapitre 7

Formatons les résultats de nos requêtes.

Exercice 1

Afficher les sportifs (nom et prenom) triés par ordre alphabétique des noms.

db.Sportif.find({}, {nom:1, prenom:1, _id:0}).sort({nom:1})

Exercice 2

Afficher les deux premières femmes sportives de la base de données (tous les champs).

db.Sportif.find({genre: "femme"}).limit(2)

Exercice 3

Afficher les noms et prénoms des sportifs qui ont plus que deux médailles.

db.Sportif.find({nbMedailles: {$gt: 2}}, {nom:1, prenom:1, _id:0})

Ateliers Sommatifs (Partie 2)

Appliquons tous les concepts de recherche et de filtrage sur un jeu de données plus riche.

Exercices Inversés : Analysez la Requête

Le travail ici est inversé. Pour chaque requête ci-dessous, analysez sa syntaxe et décrivez précisément le résultat qu'elle produit. Essayez de taper la requête vous-même pour bien mémoriser la syntaxe. Cliquez ensuite sur 'Voir la solution' pour comparer votre analyse.

Requête 1
db.restaurants.find( { "borough" : "Brooklyn" } ).count()

Explication : Cette requête compte et retourne le nombre total de documents (restaurants) qui se trouvent dans le quartier 'Brooklyn'.

Requête 2
db.restaurants.find(
    { "borough" : "Brooklyn",
      "cuisine" : "Italian",
      "name" : /pizza/i }
)

Explication : Affiche les documents complets des restaurants qui remplissent trois conditions : être dans le quartier 'Brooklyn', proposer une cuisine 'Italian', ET dont le nom contient le mot "pizza". L'option `/i` rend la recherche insensible à la casse (Pizza, pizza, PiZzA...).

Requête 3 (Projection)
db.restaurants.find(
    {"borough":"Manhattan",
     "grades.score":{$lt : 10}
    },
    {"name":1,"grades.score":1, "_id":0})

Explication : Recherche les restaurants de 'Manhattan' qui ont au moins une note (`score`) inférieure à 10. Pour les documents correspondants, elle n'affiche que les champs `name` et `grades.score`, tout en masquant explicitement le champ `_id`.

Requête 4 (elemMatch)
db.restaurants.find({
    "grades" : {
           $elemMatch : {
               "grade" : "C",
               "score" : {$lt :40}
           }
    }
})

Explication : Recherche les restaurants qui ont au moins un élément dans leur tableau `grades` qui satisfait les deux conditions SIMULTANÉMENT : le `grade` est "C" ET le `score` est inférieur à 40 pour cette même évaluation. C'est plus précis qu'une simple requête AND qui pourrait trouver un restaurant avec un grade "C" et un score faible sur deux évaluations différentes.

Requête 5 (Index de tableau)
db.restaurants.find(
    {"grades.0.grade":"C"},
    {"name":1, "borough":1, "_id":0}
)

Explication : Affiche les noms et les quartiers des restaurants dont la toute première évaluation (à l'index 0 du tableau `grades`) a un `grade` égal à "C".

Requête 6 (Distinct)
db.restaurants.distinct("grades.grade")

Explication : Cette requête ne retourne pas de documents, mais un tableau contenant la liste de toutes les valeurs uniques (sans doublons) trouvées dans le champ `grade` à l'intérieur du tableau `grades` pour toute la collection.

Ateliers Sommatifs (Suite) : Pratique sur `movies` et `dblp`

Mettons en pratique les techniques d'interrogation sur de nouveaux jeux de données.

Exercices sur la collection `movies`

Question 1 : Écrivez la requête pour trouver les films dont le nom contient le mot « forest ».

db.movies.find({ title: /forest/i })

Question 2 : Écrivez la requête pour trouver les films dans lesquels participe l’acteur « Tom Hanks ».

db.movies.find({ "cast": "Tom Hanks" })

Question 3 : Écrivez la requête pour trouver les films de France produits avant 1990 (inclus).

db.movies.find({ countries: "France", year: { $lte: 1990 } })

Question 4 : Comptez le nombre de films qui ont un rating imdb supérieur ou égal à 7.

db.movies.countDocuments({ "imdb.rating": { $gte: 7 } })

Question 5 : Trouvez les films dont le titre commence par « The » (insensible à la casse).

db.movies.find({ 'title': { $regex: /^The/i } })

Question 6 : Triez les films de « Tom Hanks » par score imdb dans l’ordre décroissant.

db.movies.find({ "cast": "Tom Hanks" }).sort({ "imdb.rating": -1 })

Exercices sur la collection `dblp`

Question 1 : Écrivez la requête pour lister tous les livres (type « Book ») publiés depuis 2014.

db.dblp.find({ "year": { "$gte": 2014 }, "type": "Book" })

Question 2 : Listez tous les auteurs distincts de la collection.

db.dblp.distinct('authors')

Question 3 : Triez les publications de « Toru Ishida » par titre de livre (croissant) et par page de début (croissant), en affichant uniquement le titre et les pages.

db.dblp.find(
    { 'authors': 'Toru Ishida' },
    { "title": 1, "pages": 1, "_id": 0 }
).sort({ "title": 1, "pages.start": 1 })

Partie 3 : Manipulation et Agrégation

Chapitre 8 : Mise à Jour et Suppression de données

Au-delà de la lecture et de la création, la modification et la suppression (Update, Delete) sont des opérations cruciales. MongoDB fournit des méthodes précises pour mettre à jour des champs spécifiques, remplacer des documents entiers, ou en supprimer un ou plusieurs.

8.1. Mise à jour de champs (`updateOne`, `updateMany`)

Pour modifier des champs spécifiques sans toucher au reste du document, on utilise `updateOne()` (modifie le premier document trouvé) ou `updateMany()` (modifie tous les documents correspondants). Ces méthodes utilisent des opérateurs de mise à jour comme `$set` pour modifier une valeur, `$inc` pour l'incrémenter, ou `$unset` pour supprimer un champ.

// Mettre à jour le nbMedailles du sportif "Rabii Mohamed"
db.Sportif.updateOne(
   { "nom" : "Rabii", "prenom": "Mohamed" },
   { $set: { "nbMedailles" : 2 } }
)

// Ajouter un champ "nationalite" à TOUS les sportifs
db.Sportif.updateMany(
   {}, // Un filtre vide cible tous les documents
   { $set: { "nationalite": "marocaine" } }
)

8.2. Remplacement de document (`replaceOne`)

Contrairement à `updateOne` qui modifie des champs, `replaceOne` supprime le document existant (correspondant au filtre) et le remplace par un nouveau, tout en conservant l'`_id` d'origine. Tous les anciens champs qui ne sont pas dans le nouveau document sont perdus.

// Remplacer entièrement les données de "Radi Abdessalam"
db.Sportif.replaceOne(
    { _id: "sp1" },
    { nom: "Radi", prenom: "Abdessalam", genre: "homme", sport: "Marathon Pro", retraité: true }
)
// Le champ "sport" n'est plus un objet, et le champ "olympique" a disparu.

8.3. Suppression de documents (`deleteOne`, `deleteMany`)

La suppression suit une logique similaire avec `deleteOne()` (supprime le premier document trouvé) et `deleteMany()` (supprime tous les documents correspondants).

// Supprimer le sportif qui pratique le "Muay Thai"
db.Sportif.deleteOne({ "sport.description": "Muay Thai" })

// Supprimer tous les sportifs dont le nom est "ELGourch"
db.Sportif.deleteMany({ "nom": "ELGourch" })

Ateliers Pratiques : Chapitre 8

Manipulons les données de notre collection `Sportif`.

Exercice 1

Mettre à jour le champ nbMedailles du sportif « Rabii Mohamed » pour y mettre la valeur 2.

db.Sportif.updateOne(
  {nom: "Rabii", prenom: "Mohamed"},
  {$set: {nbMedailles: 2}}
)

Exercice 2

Pour tous les sportifs, ajouter un champ « nationalité » avec la valeur « marocaine ».

db.Sportif.updateMany(
  {},
  {$set: {nationalite: "marocaine"}}
)

Exercice 3

Supprimer le sportif qui s'appelle `Belafrikh`.

db.Sportif.deleteOne({nom: "Belafrikh"})

Chapitre 9 : Introduction au Framework d'Agrégation

Pour les analyses de données complexes qui vont au-delà de la simple recherche, MongoDB propose le Framework d'Agrégation. Il fonctionne comme un "pipeline" (un tuyau) où les documents d'une collection passent à travers une série d'étapes (les "stages"). Chaque étape transforme les documents (filtrage, regroupement, tri, etc.) et les transmet à l'étape suivante.

Syntaxe de base

La méthode `aggregate()` prend en argument un tableau contenant la liste des étapes du pipeline.

db.collection.aggregate([
  { // Étape 1 : par exemple, un filtre $match },
  { // Étape 2 : par exemple, un regroupement $group },
  { // Étape 3 : par exemple, un tri $sort }
])

Chapitre 10 : Regroupement avec `$group` et `$match`

Deux des étapes les plus utilisées dans un pipeline d'agrégation sont `$match` pour le filtrage en amont et `$group` pour le regroupement et le calcul.

10.1. `$match` : Filtrer les documents

L'étape `$match` filtre les documents pour ne passer que ceux qui correspondent à la condition. Sa syntaxe est identique à celle d'une requête `find()`. Il est recommandé de placer `$match` le plus tôt possible dans le pipeline pour réduire le volume de données à traiter.

10.2. `$group` : Regrouper et calculer

L'étape `$group` regroupe les documents en fonction d'une clé (`_id`) et permet d'appliquer des accumulateurs sur chaque groupe pour calculer des sommes (`$sum`), des moyennes (`$avg`), des maximums (`$max`), des minimums (`$min`), etc.

// Calculer le nombre total de médailles par genre
db.Sportif.aggregate([
    // Étape 1: Filtrer pour ne garder que les sportifs ayant des médailles
    { $match: { "nbMedailles": { $exists: true } } },
    
    // Étape 2: Regrouper par genre et calculer la somme des médailles
    { $group: {
        _id: "$genre", // La clé de regroupement est le champ 'genre'
        "totalMedailles": { $sum: "$nbMedailles" } 
    }}
])

Ateliers Pratiques : Chapitre 10

Effectuons nos premières agrégations sur la collection `Sportif`.

Exercice 1

Calculer la somme des médailles par sport.

db.Sportif.aggregate([
  {$group: {_id: "$sport.description", totalMedailles: {$sum: "$nbMedailles"}}}
])

Exercice 2

Afficher le maximum de médailles par sport.

db.Sportif.aggregate([
  {$group: {_id: "$sport.description", maxMedailles: {$max: "$nbMedailles"}}}
])

Chapitre 11 : Décomposer les tableaux avec `$unwind`

Que faire si l'on veut regrouper des informations qui se trouvent à l'intérieur d'un tableau ? Par exemple, compter le nombre de films par acteur ? L'étape `$unwind` est la solution. Elle prend un document contenant un tableau et crée une copie du document pour chaque élément du tableau.

Exemple : Calculer le nombre de films par acteur

// Document original dans la collection 'movies'
// { title: "Forrest Gump", cast: ["Tom Hanks", "Robin Wright"] }

db.movies.aggregate([
    // Étape 1: Décomposer le tableau 'cast'
    { $unwind: "$cast" },
    // Résultat intermédiaire :
    // { title: "Forrest Gump", cast: "Tom Hanks" }
    // { title: "Forrest Gump", cast: "Robin Wright" }

    // Étape 2: Regrouper par acteur
    {
        $group: {
            _id: "$cast",
            total_films: { $sum: 1 }
        }
    },

    // Étape 3: Trier par le nombre de films décroissant
    { $sort: { total_films: -1 } }
])

Ateliers Sommatifs (Partie 3)

Combinons la modification de données et les pipelines d'agrégation pour des analyses complexes.

Exercice 1 : Modification de données

Dans la collection `movies`, ajoutez un nouveau film, puis modifiez son année et enfin, supprimez-le.

// 1. Ajouter un nouveau film
db.movies.insertOne({
    title: "Mon Film Test",
    year: 2023,
    cast: ["Acteur Inconnu"]
})

// 2. Modifier l'année du film ajouté
db.movies.updateOne(
    { title: "Mon Film Test" },
    { $set: { year: 2024 } }
)

// 3. Supprimer le film
db.movies.deleteOne({ title: "Mon Film Test" })

Exercice 2 : Agrégation simple

Dans la collection `movies`, affichez le nombre de films produits par pays, trié par ordre décroissant du nombre de films.

db.movies.aggregate([
   { $unwind : '$countries' },
   { $group : { '_id' : '$countries', count : { $sum : 1 } } },
   { $sort: { count: -1 } }
])

Exercices Inversés : Analysez le Pipeline d'Agrégation

Analysez chaque pipeline d'agrégation ci-dessous sur la collection `restaurants`. Décrivez le rôle de chaque étape et le résultat final produit. L'utilisation de variables pour stocker les étapes est une bonne pratique pour la clarté et la réutilisation.

Agrégation 1
varMatch = { $match : { "grades.0.grade":"C"} };
varProject = { $project : {"name":1, "borough":1, "_id":0}};
varSort = { $sort : {"name":1} };

db.restaurants.aggregate( [ varMatch, varProject, varSort ] );

Explication : Ce pipeline affiche les noms et les quartiers des restaurants dont le premier grade est 'C', et trie les résultats par nom par ordre alphabétique croissant. Les étapes sont stockées dans des variables pour une meilleure lisibilité et pour permettre de les réutiliser facilement.

Agrégation 2
db.restaurants.aggregate([
  {   $group: { _id: { cuisine: "$cuisine", borough: "$borough" } }  },
  {   $project: { _id: 0, cuisine: "$_id.cuisine", borough: "$_id.borough" } },
  {   $sort:  {  borough: 1, cuisine: 1 }  }
])

Explication : Cette agrégation retourne la liste de toutes les combinaisons uniques de "cuisine" et "quartier" qui existent dans la collection.
Étape 1 (`$group`): Regroupe les documents par couple unique de cuisine et quartier.
Étape 2 (`$project`): Reformate la sortie pour afficher `cuisine` et `borough` comme des champs de premier niveau, au lieu d'être imbriqués dans `_id`.
Étape 3 (`$sort`): Trie les résultats par quartier, puis par cuisine.

Agrégation 3
varMatch = { $match : { "grades.0.grade":"C"} };
varGroup = { $group : {"_id" : "$borough", "total" : {$sum : 1} } };

db.restaurants.aggregate( [ varMatch, varGroup ] );

Explication : Cette agrégation compte, pour chaque quartier, le nombre de restaurants dont le premier grade est "C".
Étape 1 (`$match`): Filtre et ne conserve que les restaurants dont le premier grade est 'C'.
Étape 2 (`$group`): Regroupe les restaurants restants par quartier (`_id: "$borough"`) et, pour chaque groupe, calcule le nombre de documents (`$sum: 1`).

Agrégation 4
varUnwind = {$unwind : "$grades"};
varGroup = { $group : {"_id" : "$borough", "moyenne" : {$avg : "$grades.score"} } };
varSort = { $sort : { "moyenne" : -1 } };

db.restaurants.aggregate( [ varUnwind, varGroup, varSort ] );

Explication : Calcule le score moyen de toutes les évaluations pour chaque quartier, puis trie les résultats pour afficher les quartiers avec les meilleures moyennes en premier.
Étape 1 (`$unwind`): Décompose le tableau `grades` de chaque restaurant, créant une copie du restaurant pour chaque évaluation qu'il contient.
Étape 2 (`$group`): Regroupe les documents par quartier et calcule la moyenne (`$avg`) des scores de toutes les évaluations de ce quartier.
Étape 3 (`$sort`): Trie les quartiers par leur `moyenne` en ordre décroissant (-1).

Ateliers Sommatifs (Suite) : Agrégations sur `movies` et `dblp`

Passons à des analyses plus complexes en utilisant le puissant framework d'agrégation.

Agrégations sur la collection `movies`

Question 1 : Calculez le nombre de films par acteur, trié par ordre alphabétique du nom de l'acteur.

db.movies.aggregate([
    { $unwind: '$cast' },
    { $group: { '_id': '$cast', totalfilm: { $sum: 1 } } },
    { $sort: { '_id': 1 } }
])

Question 2 : Affichez la liste des acteurs qui ont participé à plus de 10 films.

db.movies.aggregate([
    { $unwind: '$cast' },
    { $group: { '_id': '$cast', totalfilm: { $sum: 1 } } },
    { $match: { totalfilm: { $gt: 10 } } }
])

Question 3 : Calculez la note imdb moyenne des films de « Steven Spielberg ».

db.movies.aggregate([
    { $match: { directors: "Steven Spielberg" } },
    { $group: { _id: "Steven Spielberg", moyenne: { $avg: "$imdb.rating" } } }
])

Question 4 : Affichez le nombre de films par genre, en n'affichant que les 5 genres les plus populaires.

db.movies.aggregate([
    { $unwind: '$genres' },
    { $group: { _id: '$genres', nombre: { $sum: 1 } } },
    { $sort: { nombre: -1 } },
    { $limit: 5 }
])

Question 5 : Donnez les cinq meilleurs réalisateurs de l’année 2000, classés par la note moyenne de leurs films.

db.movies.aggregate([
  { $match: { year: 2000, "imdb.rating": { $type: "number" } } },
  { $unwind: "$directors" },
  { $group: { _id: "$directors", avgRating: { $avg: "$imdb.rating" } } },
  { $sort: { avgRating: -1 } },
  { $limit: 5 }
])

Agrégations sur la collection `dblp`

Question 1 : Comptez le nombre de publications par type depuis 2011.

db.dblp.aggregate([
    { $match: { year: { $gte: 2011 } } },
    { $group: { '_id': '$type', nombre: { $sum: 1 } } }
])

Question 2 : Pour chaque type, donnez le nombre d'ouvrages édités depuis 2011, en n’affichant que les types qui ont plus de 1000 publications.

db.dblp.aggregate([
    { $match: { year: { $gte: 2011 } } },
    { $group: { '_id': '$type', nombre: { $sum: 1 } } },
    { $match: { nombre: { $gte: 1000 } } }
])

Question 3 : Comptez le nombre de publications par auteur et triez le résultat par ordre croissant du nombre de publications.

db.dblp.aggregate([
    { $unwind: '$authors' },
    { $group: { _id: '$authors', nombre: { $sum: 1 } } },
    { $sort: { nombre: 1 } }
])

Partie 4 : Administration et Connexion Applicative

Chapitre 12 : Sauvegarde et Restauration

La sauvegarde régulière des données est une tâche critique. MongoDB fournit les Database Tools, une suite d'utilitaires en ligne de commande pour effectuer des sauvegardes (`mongodump`) et des restaurations (`mongorestore`) de manière efficace.

12.1. Sauvegarder (`mongodump`)

La commande `mongodump` crée une sauvegarde binaire (BSON) de votre base de données. Elle s'exécute depuis le terminal de votre système d'exploitation, et non depuis `mongosh`.

# Sauvegarde simple d'une base de données spécifique
mongodump --db DBSportifs --out=/chemin/vers/backup/

# Sauvegarde avec authentification
mongodump --authenticationDatabase admin --username root --password VOTRE_PASS --db DBSportifs --out=/chemin/vers/backup/

12.2. Restaurer (`mongorestore`)

La commande `mongorestore` permet de restaurer une base de données à partir d'un répertoire de sauvegarde créé par `mongodump`. L'option `--drop` est utile pour supprimer les collections existantes avant de les restaurer.

# Restauration simple (le nom de la DB est déduit du nom du dossier)
mongorestore --drop /chemin/vers/backup/DBSportifs

# Restauration avec authentification
mongorestore --authenticationDatabase admin --username root --password VOTRE_PASS --drop /chemin/vers/backup/DBSportifs

Chapitre 13 : Gestion des Utilisateurs et des Rôles

Sécuriser l'accès à vos données est primordial. MongoDB intègre un système d'authentification et de contrôle d'accès basé sur les rôles (RBAC - Role-Based Access Control) qui permet de définir précisément qui peut faire quoi.

13.1. Activer l'authentification

L'authentification doit être activée dans le fichier de configuration de MongoDB (`mongod.cfg`).

security:
  authorization: "enabled"

Après avoir modifié ce fichier, le service MongoDB doit être redémarré.

13.2. Créer un utilisateur

La commande `db.createUser()` permet de créer un utilisateur en lui assignant un nom, un mot de passe et un ou plusieurs rôles sur une base de données spécifique.

// Se connecter à la base 'admin' pour créer un super-utilisateur
use admin
db.createUser({
    user: "superAdmin",
    pwd: passwordPrompt(), // Pour une saisie sécurisée du mot de passe
    roles: [ { role: "root", db: "admin" } ]
})

// Créer un utilisateur avec des droits de lecture seule sur la base 'DBSportifs'
use DBSportifs
db.createUser({
    user: "guest",
    pwd: "guest123",
    roles: [ { role: "read", db: "DBSportifs" } ]
})

Chapitre 14 : Interroger MongoDB avec Python (PyMongo)

MongoDB peut être interrogé depuis n'importe quel langage de programmation via des "drivers". Pour Python, le driver officiel et de référence est PyMongo. Il fournit une interface simple et pythonique pour interagir avec la base de données.

14.1. Installation et Connexion

L'installation se fait via `pip`. La connexion est établie en instanciant la classe `MongoClient`.

# 1. Installation
pip install pymongo

# 2. Connexion et récupération d'une collection
from pymongo import MongoClient

# Créer une connexion au serveur MongoDB
client = MongoClient('mongodb://localhost:27017/')

# Sélectionner la base de données
db = client['DBSportifs']

# Sélectionner la collection
collection = db['Sportif']

14.2. Exécuter une requête

La méthode `find()` de PyMongo retourne un objet `Cursor`, qui est un itérable. Il suffit de boucler dessus pour accéder aux documents.

# Rechercher tous les sportifs de genre "femme" et n'afficher que leur nom
filtre = { "genre": "femme" }
projection = { "nom": 1, "prenom": 1, "_id": 0 }

cursor = collection.find(filtre, projection)

# Itérer sur les résultats et les afficher
for sportif in cursor:
    print(sportif)

Ateliers Sommatifs (Partie 4)

Mettons en place une configuration de sécurité complète et entraînons-nous à sauvegarder et restaurer nos données.

Exercice : Mettre en place la sécurité

1. Activez l'authentification sur votre serveur MongoDB.
2. Créez un utilisateur `root` sur la base `admin`.
3. Créez un utilisateur `guest` sur la base `DBSportifs` avec uniquement les droits de lecture.
4. Connectez-vous avec `guest` et vérifiez que vous ne pouvez pas insérer de document dans la collection `Sportif`.

// -- Étape 1 : Modifier le fichier mongod.cfg et redémarrer le service --

// -- Étape 2 : Créer l'utilisateur root (connecté sans authentification pour la première fois) --
use admin
db.createUser({ 
    user: "root", 
    pwd: "123456", 
    roles: [ { role: "root", db: "admin" } ] 
});

// -- Étape 3 : Se reconnecter avec l'utilisateur root et créer le guest --
// > mongosh --authenticationDatabase "admin" -u "root" -p "123456"
use DBSportifs
db.createUser({ 
    user: "guest", 
    pwd: "123456", 
    roles: [ { role: "read", db: "DBSportifs" } ] 
});

// -- Étape 4 : Se reconnecter avec guest et tester --
// > mongosh --authenticationDatabase "DBSportifs" -u "guest" -p "123456"
use DBSportifs
db.Sportif.find({}).limit(1) // Devrait fonctionner
db.Sportif.insertOne({ title: "Test" }) // Devrait échouer avec une erreur d'autorisation

Conclusion & Perspectives

Félicitations pour avoir terminé ce parcours complet sur MongoDB !

Vous avez exploré le monde des bases de données NoSQL, en maîtrisant le modèle orienté document qui offre une flexibilité et une scalabilité exceptionnelles. Des requêtes CRUD de base à la puissance du Framework d'Agrégation, en passant par la modélisation de données flexibles et la sécurisation de vos instances, vous possédez désormais les compétences pour construire et gérer des applications modernes, capables de traiter des volumes de données variés et complexes. Le voyage ne s'arrête pas là. Continuez à explorer, à expérimenter et à construire. La pratique est la clé de la maîtrise !