IFT3225

Qu’est-ce que MongoDB ?

MongoDB s’inscrit dans la lignée des bases de données (BD) NoSQL, et plus précisément des bases de données orientées documents.
Là où une BD relationnelle organise les données dans des tables, composées de lignes et de colonnes suivant un schéma défini, MongoDB stocke les données sous forme de documents (structures proches d’objets JSON) regroupés dans des collections.

Par exemple, une collection etudiants pourrait contenir les deux documents suivants :

Étudiant 1

{
  "_id": "1",
  "nom": "Amina",
  "programme": "Informatique",
  "cours": ["IFT1015", "IFT2255"]
}

Étudiant 2

{
  "_id": "2",
  "nom": "Thomas",
  "programme": "Mathématiques",
  "courriel": "thomas@example.com"
}

Ces deux documents appartiennent à la même collection, mais ils ne possèdent pas exactement les mêmes champs : le premier contient un tableau cours, tandis que le second contient un champ courriel.

Par défaut, MongoDB n’impose pas un schéma strict à tous les documents d’une même collection : deux documents d’une même collection peuvent donc avoir des champs différents.

Cette flexibilité est l’une des grandes différences avec le modèle relationnel. Elle facilite l’évolution des données, mais elle déplace aussi une responsabilité vers l’application, l’API ou le service qui exploite ces données. Il faut alors s’assurer que les documents restent cohérents, que les champs attendus sont bien présents lorsque nécessaire, et que les données sont validées au bon endroit.

Le vocabulaire, en correspondance

Si vous venez du monde relationnel (SQL), la traduction est directe :

Relationnel (SQL) MongoDB
base de données base de données
table collection
ligne (row) document
colonne (column) champ (field)
clé primaire _id (un ObjectId par défaut)
jointure (JOIN) imbrication, ou $lookup
schéma rigide schéma souple (défini côté application)

Les types : la correspondance Mongo

Un champ de document a un type BSON. Voici les types courants et leur équivalent conceptuel :

Donnée Type BSON Exemple
identifiant ObjectId ObjectId("...")
texte String "Dune"
nombre Number (Int32 / Double) 42, 19.99
vrai / faux Boolean true
date Date ISODate("2026-05-12")
liste Array ["roman", "essai"]
objet imbriqué Object { lat: 45.5, lng: -73.6 }
absence de valeur Null null

Le type Date et le type ObjectId sont ce qui distingue le BSON du JSON pur : du vrai JSON ne connaît ni les dates ni les identifiants natifs.


La décision centrale : imbriquer ou référencer

C’est ici que MongoDB demande une vraie réflexion. En relationnel, une relation entre deux entités se traduit toujours de la même façon : deux tables et une clé étrangère. En MongoDB, vous avez un choix pour chaque relation :

L’outil ci-dessous rend cette décision manipulable via deux gestes :

La petite bascule 1 / N sur chaque relation ajuste la cardinalité (un sous-document seul ou un tableau ; un identifiant ou un tableau d’identifiants).

Regrouper : glissez une entité dans une autre (imbrication).Lier : cliquez une entité, puis une seconde (référence).
Boîteboites
Attributs
_idObjectId
nomString
quartierString
positionObject
Livrelivres
Attributs
_idObjectId
titreString
auteurString
isbnString
Événementevenements
Attributs
_idObjectId
typeString
dateDate
Utilisateurutilisateurs
Attributs
_idObjectId
nomString
courrielString

Interroger la base de données

Lire avec find

La méthode find permet de rechercher et afficher des documents (données) dans une collection MongoDB selon certains critères. Appelée sans argument, elle les renvoie tous :

db.livres.find()   // tous les documents de la collection "livres"

Forme générale

db[collection].find(<query>, <projection>, <options>)

La méthode find() est appelé sur une collection spécifique, accédée via db[collection]. Ici collection est remplacé par le nom de la collection.

La méthode find() utilise les paramètres (optionnels) suivant:

Cas d’utilisation

Supposons une collection employes avec les données suivante

- id : number
- nom : string
- prenom : string
- poste : string
- salaire: double
- ville : string
- competences : Array(string)
- date_embauche: date
- actif: boolean

1. Le paramètre query

Le paramètre query correspond au filtre utilisé pour sélectionner les documents. Il peut être simple ou très complexe grâce aux opérateurs de requête.

Exemple:

// Récupérer tous les documents (employés):
db["employes"].find({})

//Trouver un employé nommé "Bertrand"
db["employes"].find({ nom: "Bertrand" })

// Trouver tous les employés dont le salaire dépasse 50,000
db["employes"].find({ salaire: { $gt: 50000 } })

// Trouver les employés actif habitant à Montréal
db["employes"].find({ actif: true, ville : "Montréal" })

// Trouver ceux ayant une compétences précise
db["employes"].find({ competences: "Python" })

2. Le paramètre projection

Le paramètre projection permet de choisir les champs à afficher. Par défaut, tous les champs sont retournés, mais tu peux limiter les résultats pour plus de clarté ou d’efficacité.

// Afficher uniquement le nom et le salaire
db["employes"].find({}, { nom: 1, salaire: 1, _id: 0 })

// Afficher uniquement le nom et la ville des employés actifs
db["employes"].find({ actif: true }, { nom: 1, ville: 1, _id: 0 })

// Afficher seulement les compétences
db["employes"].find(
    { nom: "Claudia" },
    { competences: 1, _id: 0 }
)

Égalité et comparaison

Sans opérateur, un champ teste l’égalité stricte :

db.livres.find({ categorie: "roman" })   // categorie === "roman"

Les opérateurs de comparaison élargissent le test :

Opérateur Sens Exemple
$eq égal (l’égalité implicite) { annee: { $eq: 2020 } }
$ne différent { categorie: { $ne: "essai" } }
$gt / $gte supérieur / ou égal { annee: { $gte: 2000 } }
$lt / $lte inférieur / ou égal { annee: { $lt: 1950 } }
$in dans une liste { quartier: { $in: ["Plateau", "Verdun"] } }
$nin hors d’une liste { categorie: { $nin: ["essai", "BD"] } }
// Livres parus depuis 2000, mais pas les essais
db.livres.find({ annee: { $gte: 2000 }, categorie: { $ne: "essai" } })

Plusieurs champs dans le même filtre sont reliés par un ET implicite : tous doivent être satisfaits. Le filtre ci-dessus exige à la fois l’année et la catégorie.

Opérateurs logiques

Pour un OU — ou des combinaisons plus libres — on passe par les opérateurs logiques, qui prennent un tableau de conditions :

Opérateur Sens
$or au moins une condition vraie
$and toutes vraies (souvent implicite)
$nor aucune vraie
$not nie une condition sur un champ
// Les romans, ou n'importe quel livre paru avant 1950
db.livres.find({
  $or: [
    { categorie: "roman" },
    { annee: { $lt: 1950 } },
  ],
})

$and explicite ne sert qu’à imposer deux conditions sur le même champ : { $and: [ { annee: { $gte: 2000 } }, { annee: { $lt: 2010 } } ] } — impossible à écrire comme deux clés annee dans un seul objet.

VérificationÉcrire le filtre : les événements de type emprunt survenus depuis le 1er mai 2026.

db.evenements.find({ type: "emprunt", date: { $gte: ISODate("2026-05-01") } }). Les deux champs forment un ET implicite : on veut les documents dont le type vaut "emprunt" et dont la date est postérieure ou égale au 1er mai. $gte accepte une Date (ISODate) aussi naturellement qu’un nombre.


Opérations CRUD

On peut lire et écrire dans la base de données sans aucune bibliothèque, directement dans mongosh (le pilote Node mongodb expose les mêmes méthodes). Les quatre opérations CRUD :

Create — insérer

db.boites.insertOne({ nom: "Boîte du parc", quartier: "Rosemont" });
db.boites.insertMany([
  { nom: "Coin lecture", quartier: "Plateau" },
  { nom: "Halte-livres",  quartier: "Verdun"  },
]);

insertOne renvoie l’_id généré (insertedId) ; la BD le crée pour vous si vous ne le fournissez pas.

Read — lire

db.boites.find({ quartier: "Plateau" });    // un curseur sur les documents correspondants
db.boites.findOne({ _id: ObjectId("…") });  // un seul document, ou null
db.boites.countDocuments({ quartier: "Plateau" });

Le filtre accepte exactement les opérateurs de la section précédente.

Update — modifier

Une écriture combine un filtre (quels documents) et un document de mise à jour bâti avec des opérateurs : $set (affecter un champ), $unset (le retirer), $inc (incrémenter un nombre) :

// Renommer une boîte précise
db.boites.updateOne(
  { _id: ObjectId("…") },
  { $set: { nom: "Boîte rénovée" } }
);
// Reclasser tout un quartier d'un coup (plusieurs documents)
db.boites.updateMany(
  { quartier: "Plateau" },
  { $set: { quartier: "Le Plateau-Mont-Royal" } }
);

Sans $set, MongoDB remplace le document entier par l’objet fourni. updateOne(filtre, { nom: "X" }) effacerait quartier et tous les autres champs. On veut donc presque toujours $set.

Delete — supprimer

db.boites.deleteOne({ _id: ObjectId("…") });   // le premier qui correspond
db.boites.deleteMany({ quartier: "Verdun" });   // tous ceux qui correspondent


Intégration pas à pas avec Express

On va maintenant brancher MongoDB à un serveur Express — en reprenant l’API de boîtes à livres construite précédemment. Jusqu’ici, les données vivaient dans un tableau en mémoire ; on va les persister dans MongoDB.

Mongoose est une bibliothèque ODM (Object Data Modeling) qui ajoute une couche au-dessus de MongoDB : des schémas définis côté application, de la validation, du type casting et une API de requêtes plus confortable.

Étape 1 — Installer Mongoose

npm install mongoose

On suppose un cluster MongoDB Atlas ou une instance locale. La connexion se fait via une URI.

Étape 2 — Se connecter à la base de données

On isole la connexion dans son module, appelé une fois au démarrage :

// Fichier src/db.js
import mongoose from "mongoose";

export async function connecterDB() {
  const uri = process.env.MONGO_URI;
  await mongoose.connect(uri);
  console.log("Connecté à MongoDB");
}

mongoose.connect() retourne une Promise — d’où le await. On l’appelle dans le point d’entrée, avant de démarrer le serveur :

// Fichier index.js
import app from "./src/app.js";
import { connecterDB } from "./src/db.js";

await connecterDB();                 // d'abord la BD...

app.listen(3000, () => {             // ...puis le serveur
  console.log("Serveur sur http://localhost:3000");
});

Connecter la BD avant d’écouter garantit qu’aucune requête n’arrive avant que la base de données soit prête.

Étape 3 — Définir un schéma et un modèle

Le schéma décrit la forme attendue d’un document ; le modèle est l’objet par lequel on interroge la collection. C’est ici qu’on réintroduit la structure que MongoDB ne force pas :

// Fichier src/models/Boite.js
import mongoose from "mongoose";

const boiteSchema = new mongoose.Schema({
  nom: { type: String, required: true },
  quartier: { type: String, default: "inconnu" },
});

// "Boite" → collection "boites" (Mongoose met au pluriel et en minuscules)
export default mongoose.model("Boite", boiteSchema);

Remarquez la correspondance avec les types vus plus haut : String est le type BSON, required et default sont des règles que Mongoose applique côté application.

Étape 4 — Utiliser le modèle dans les routes

On remplace le tableau en mémoire par des appels au modèle. Toutes les méthodes sont asynchrones, donc on utilise async/await partout :

// Fichier src/routes/boites.js
import { Router } from "express";
import Boite from "../models/Boite.js";

const router = Router();

// Lister (avec filtre optionnel par quartier)
router.get("/", async (req, res) => {
  const filtre = req.query.quartier ? { quartier: req.query.quartier } : {};
  const boites = await Boite.find(filtre);
  res.json(boites);
});

// Détail d'une boîte
router.get("/:id", async (req, res) => {
  const boite = await Boite.findById(req.params.id);
  if (!boite) {
    return res.status(404).json({ error: "Boîte introuvable" });
  }
  res.json(boite);
});

// Créer
router.post("/", async (req, res) => {
  try {
    const boite = await Boite.create(req.body);   // valide selon le schéma
    res.status(201).json(boite);
  } catch (err) {
    res.status(400).json({ error: err.message }); // échec de validation
  }
});

export default router;

Étape 5 — Compléter avec modifier et supprimer

// Modifier
router.put("/:id", async (req, res) => {
  const boite = await Boite.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }   // renvoie la version à jour, valide les champs
  );
  if (!boite) {
    return res.status(404).json({ error: "Boîte introuvable" });
  }
  res.json(boite);
});

// Supprimer
router.delete("/:id", async (req, res) => {
  const boite = await Boite.findByIdAndDelete(req.params.id);
  if (!boite) {
    return res.status(404).json({ error: "Boîte introuvable" });
  }
  res.status(204).end();
});

Ce qui a changé

Le squelette de l’API n’a pas bougé : mêmes routes, mêmes verbes, mêmes codes de statut. Seul le contenu des gestionnaires a changé — d’un tableau en mémoire à des appels Mongoose. C’est exactement pourquoi on avait isolé les données dans leur module : remplacer la source de données ne touche ni au routeur, ni à l’app, ni au point d’entrée.

VérificationPourquoi les gestionnaires de routes sont-ils tous devenus async après l'ajout de MongoDB ?

Parce que toute opération sur la BD passe par le réseau — c’est de l’I/O, donc asynchrone. Boite.find(), create(), findById() retournent des Promises ; il faut await leur résultat. Avec le tableau en mémoire, l’accès était synchrone et immédiat ; avec une vraie base de données, on attend une réponse. C’est le même passage au non-bloquant que pour fetch.