Chapitre 421 minIntermédiaire
Requêtes CRUD dans mongosh : insert, find, update, delete
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- insérer des documents avec insertOne et insertMany ;
- lire, projeter, trier et compter avec find ;
- modifier avec updateOne et les opérateurs $set, $inc, $push et $pull ;
- supprimer sans risque avec deleteOne et deleteMany.
3 exercices corrigés
Le CRUD (Create, Read, Update, Delete) regroupe les quatre opérations de base sur les données : créer, lire, modifier, supprimer. Dans MongoDB, chacune est une méthode de la collection : insertOne, find, updateOne, deleteOne et leurs variantes. Une requête n'est pas une phrase comme en SQL, mais un objet JavaScript qui décrit le filtre, puis un second objet qui décrit la modification ou les champs voulus. Ce chapitre les pratique toutes dans mongosh, sur le jeu de données d'AppliFrais.
SQL et MongoDB : la correspondance
| Opération | SQL | MongoDB (mongosh) |
|---|---|---|
| Créer | INSERT INTO fiches (...) VALUES (...) | db.fiches.insertOne({ ... }) |
| Lire | SELECT mois, etat FROM fiches WHERE etat = 'CL' | db.fiches.find({ etat: 'CL' }, { mois: 1, etat: 1 }) |
| Compter | SELECT COUNT(*) FROM fiches WHERE etat = 'CR' | db.fiches.countDocuments({ etat: 'CR' }) |
| Modifier | UPDATE fiches SET etat = 'CL' WHERE ... | db.fiches.updateOne({ ... }, { $set: { etat: 'CL' } }) |
| Supprimer | DELETE FROM fiches WHERE ... | db.fiches.deleteOne({ ... }) |
La logique est la même ; seule l'écriture change. Retiens la forme générale : db.<collection>.<méthode>(filtre, …), où le filtre est un objet qui décrit les documents visés. Un filtre vide {} vise tous les documents.
Insérer : insertOne et insertMany
Camille Roux, embauchée en septembre, commence sa fiche de novembre :
db.fiches.insertOne({
visiteur: ObjectId('6a0000000000000000000004'),
mois: '2026-11',
etat: 'CR',
dateModif: new Date(),
lignesForfait: [],
fraisHorsForfait: [],
montantValide: null
})
{ acknowledged: true, insertedId: ObjectId('6ae8ac9b89b758b28e549ccb') }
acknowledged: true signifie que le serveur a confirmé l'écriture ; insertedId est l'_id que le driver a généré. new Date() donne la date et l'heure courantes. La collection fiches existait déjà ; si elle n'avait pas existé, cette insertion l'aurait créée.
insertMany insère un tableau de documents en un seul aller-retour avec le serveur, bien plus vite qu'une boucle d'insertOne. Par défaut, l'insertion est ordonnée : au premier document refusé (un _id en double, par exemple), MongoDB s'arrête, et les documents précédents restent insérés : il n'y a pas d'annulation globale. Avec l'option { ordered: false }, MongoDB tente tous les documents et signale les échecs à la fin.
// _id 2 existe déjà dans la collection essai
db.essai.insertMany([{ _id: 1 }, { _id: 2 }, { _id: 3 }])
// MongoBulkWriteError: E11000 duplicate key error ... dup key: { _id: 2 }
// Résultat : _id 1 inséré, _id 3 jamais tenté
// Sur la collection de départ (seulement _id 2) :
db.essai.insertMany([{ _id: 1 }, { _id: 2 }, { _id: 3 }], { ordered: false })
// Même erreur, mais _id 1 et _id 3 sont insérés
Lire : find et findOne
find(filtre, projection) renvoie un curseur sur les documents qui correspondent au filtre. Les résultats affichés dans cette section et la suivante sont ceux du jeu de données d'origine : si tu as tapé l'insertOne de Camille, sa fiche de novembre (CR) s'ajoute aux résultats ; relance jeu-de-donnees.js pour retrouver exactement les mêmes. Dans mongosh, le curseur affiche les 20 premiers résultats ; tape it pour les 20 suivants. findOne renvoie directement le premier document trouvé, ou null.
db.fiches.find() // toutes les fiches
db.fiches.find({ etat: 'CR' }) // les fiches en cours de saisie
db.fiches.find({ etat: 'CR', mois: '2026-10' }) // deux conditions : ET implicite
db.visiteurs.findOne({ matricule: 'a131' }) // un seul document, ou null
Plusieurs champs dans le même filtre se combinent en ET. Les autres combinaisons (OU, comparaisons, tableaux) passent par des opérateurs, au chapitre 5.
La projection : choisir les champs
Le deuxième argument de find est la projection, l'équivalent de la liste des colonnes du SELECT : 1 pour inclure un champ, 0 pour l'exclure. L'_id est inclus par défaut ; on l'exclut explicitement avec _id: 0.
db.fiches.find({ etat: 'CR' }, { mois: 1, etat: 1, _id: 0 })
[
{ mois: '2026-10', etat: 'CR' },
{ mois: '2026-10', etat: 'CR' },
{ mois: '2026-10', etat: 'CR' }
]
Une projection est soit une liste de champs à inclure, soit une liste de champs à exclure, jamais un mélange (sauf pour _id). { nom: 1, email: 0 } provoque l'erreur Cannot do exclusion on field email in inclusion projection. Pour masquer quelques champs d'un document, l'exclusion est plus courte :
db.visiteurs.find({ role: 'comptable' }, { email: 0, dateEmbauche: 0 })
[
{
_id: ObjectId('6a0000000000000000000005'),
matricule: 'k002',
nom: 'Lambert',
prenom: 'Sophie',
role: 'comptable'
}
]
La projection sait aussi viser l'intérieur des tableaux : { 'lignesForfait.code': 1 } ne garde que le code de chaque ligne, et { lignesForfait: { $slice: 2 } } ne garde que les deux premières lignes.
Trier, limiter, sauter
On enchaîne des méthodes sur le curseur : sort (1 croissant, −1 décroissant), limit et skip.
// Les trois fiches au plus gros montant validé
db.fiches.find({}, { _id: 0, mois: 1, etat: 1, montantValide: 1 })
.sort({ montantValide: -1 })
.limit(3)
[
{ mois: '2026-09', etat: 'VA', montantValide: 898.9 },
{ mois: '2026-09', etat: 'MP', montantValide: 562.4 },
{ mois: '2026-08', etat: 'RB', montantValide: 512.2 }
]
Quel que soit l'ordre dans lequel tu les écris, MongoDB applique toujours le tri, puis le saut, puis la limite. skip(20).limit(10) donne la troisième page de 10 résultats, comme LIMIT 10 OFFSET 20 en SQL, avec le même défaut sur les grandes collections (pagination).
Compter et lister les valeurs distinctes
db.fiches.countDocuments({ etat: 'CR' }) // 3
db.fiches.countDocuments({}) // 9
db.fiches.estimatedDocumentCount() // 9, lu dans les métadonnées : instantané, sans filtre
db.fiches.distinct('etat') // [ 'CL', 'CR', 'MP', 'RB', 'VA' ]
db.fiches.distinct('mois', { etat: 'CR' }) // [ '2026-10' ]
countDocuments accepte un filtre et compte réellement les documents. estimatedDocumentCount lit le nombre total dans les métadonnées de la collection, sans filtre : instantané, mais approximatif après un arrêt brutal du serveur. L'ancienne méthode count() est dépréciée.
Modifier : updateOne, updateMany et les opérateurs de mise à jour
updateOne(filtre, modification, options) modifie le premier document qui correspond au filtre ; updateMany les modifie tous. La modification s'écrit avec des opérateurs de mise à jour, qui commencent par $ :
| Opérateur | Effet | Exemple |
|---|---|---|
$set | Donne une valeur à un champ (le crée s'il n'existe pas) | { $set: { etat: 'CL' } } |
$unset | Supprime un champ | { $unset: { aRelancer: '' } } |
$inc | Ajoute une valeur (négative pour retirer) | { $inc: { 'lignesForfait.0.quantite': 1 } } |
$mul | Multiplie | { $mul: { montantUnitaire: 1.02 } } |
$currentDate | Met la date courante | { $currentDate: { dateModif: true } } |
$push | Ajoute un élément à un tableau | { $push: { lignesForfait: { code: 'KM', quantite: 80, montantUnitaire: 0.62 } } } |
$addToSet | Ajoute un élément s'il n'y est pas déjà | { $addToSet: { regions: 'Ain' } } |
$pull | Retire les éléments qui correspondent à une condition | { $pull: { lignesForfait: { code: 'ETP' } } } |
$setOnInsert | Donne une valeur seulement si un upsert crée le document | Voir plus bas |
Camille saisit ses frais de novembre, ligne par ligne :
// Ajouter une ligne, et mettre à jour la date de modification
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11' },
{
$push: { lignesForfait: { code: 'KM', quantite: 80, montantUnitaire: 0.62 } },
$currentDate: { dateModif: true }
}
)
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
matchedCount dit combien de documents correspondaient au filtre, modifiedCount combien ont réellement changé. Les deux diffèrent quand la modification ne change rien : un $set: { etat: 'CR' } sur une fiche déjà à CR donne matchedCount: 1, modifiedCount: 0. Un matchedCount: 0 signifie que le filtre ne trouve rien : c'est la première chose à vérifier quand une mise à jour « ne marche pas ».
Pour ajouter plusieurs éléments d'un coup, $push prend un modificateur $each :
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11' },
{ $push: { lignesForfait: { $each: [
{ code: 'REP', quantite: 2, montantUnitaire: 25 },
{ code: 'ETP', quantite: 1, montantUnitaire: 110 }
] } } }
)
Modifier un élément précis d'un tableau
Trois façons de viser un élément d'un tableau :
// 1. Par sa position : la quantité de la première ligne (indice 0), écrite ici sans l'exécuter
// db.fiches.updateOne(filtre, { $inc: { 'lignesForfait.0.quantite': 1 } })
// 2. L'opérateur positionnel $ : le premier élément qui correspond au filtre
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11', 'lignesForfait.code': 'KM' },
{ $inc: { 'lignesForfait.$.quantite': 40 } }
)
// 3. Un filtre de tableau nommé : tous les éléments qui passent arrayFilters
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11' },
{ $set: { 'lignesForfait.$[l].quantite': 3 } },
{ arrayFilters: [{ 'l.code': 'REP' }] }
)
La position (1) est fragile : rien ne garantit que la ligne KM soit la première. Le $ (2) désigne l'élément trouvé par la condition sur le tableau dans le filtre ('lignesForfait.code': 'KM'). $[l] (3) désigne tous les éléments qui satisfont la condition nommée l dans arrayFilters ; le nom est libre. Enfin, $pull retire des éléments :
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11' },
{ $pull: { lignesForfait: { code: 'ETP' } } }
)
db.fiches.findOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11' },
{ _id: 0, etat: 1, lignesForfait: 1 }
)
{
etat: 'CR',
lignesForfait: [
{ code: 'KM', quantite: 120, montantUnitaire: 0.62 },
{ code: 'REP', quantite: 3, montantUnitaire: 25 }
]
}
80 kilomètres + 40 = 120 ; la quantité de repas est passée de 2 à 3 ; la ligne ETP a disparu.
updateMany, upsert et findOneAndUpdate
updateMany applique la même modification à tous les documents qui correspondent :
db.fiches.updateMany({ etat: 'CL' }, { $set: { aRelancer: true } })
// { acknowledged: true, insertedId: null, matchedCount: 2, modifiedCount: 2, upsertedCount: 0 }
db.fiches.updateMany({ aRelancer: { $exists: true } }, { $unset: { aRelancer: '' } })
L'option upsert: true (update or insert) crée le document s'il n'existe pas. Combinée avec $setOnInsert, elle exprime « la fiche du mois, créée si besoin » :
db.fiches.updateOne(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-12' },
{
$setOnInsert: { etat: 'CR', lignesForfait: [], fraisHorsForfait: [], montantValide: null },
$currentDate: { dateModif: true }
},
{ upsert: true }
)
{
acknowledged: true,
insertedId: ObjectId('6ae8ac9be2bbb6f6672bff4e'),
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
}
Le document créé contient les champs du filtre (visiteur, mois), ceux de $setOnInsert et la date. Relancée, la même commande trouve la fiche (matchedCount: 1) et ne met à jour que dateModif : $setOnInsert est ignoré quand le document existe.
findOneAndUpdate modifie un document et le renvoie, en une seule opération atomique. Avec returnDocument: 'after', tu obtiens la version modifiée ; sans, la version d'avant.
db.fiches.findOneAndUpdate(
{ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-11', etat: 'CR' },
{ $set: { etat: 'CL' } },
{ returnDocument: 'after', projection: { _id: 0, mois: 1, etat: 1 } }
)
// { mois: '2026-11', etat: 'CL' }
Le filtre contient etat: 'CR' : relancée, la commande ne trouve plus de fiche en cours et renvoie null. Mettre la condition métier dans le filtre rend l'opération sûre même si deux requêtes arrivent en même temps : une seule passe. C'est la technique de l'API au chapitre 12.
Supprimer : deleteOne et deleteMany
db.fiches.deleteOne({ visiteur: ObjectId('6a0000000000000000000004'), mois: '2026-12' })
// { acknowledged: true, deletedCount: 1 }
db.fiches.deleteMany({ visiteur: ObjectId('6a0000000000000000000004'), mois: { $gte: '2026-11' } })
// { acknowledged: true, deletedCount: 1 } (la fiche de novembre)
deleteOne supprime le premier document trouvé, deleteMany tous. drop() supprime la collection entière, avec ses index.
Exercices
Recharge le jeu de données avant de commencer (mongosh "<chaîne>" jeu-de-donnees.js).
Exercice 1 sur 3 Lire Réussi À revoir
Écris les requêtes qui affichent :
- le nom, le prénom et la région des visiteurs (pas la comptable), triés par région puis par nom, sans
_id; - le nombre de fiches de Lucas Martin ;
- la liste des régions où travaillent des employés, sans doublon ;
- les deux fiches les plus récentes (selon
dateModif), avec seulement leur mois et leur état.
Voir la réponseMasquer la réponse de l'exercice 1
// 1.
db.visiteurs.find({ role: 'visiteur' }, { _id: 0, nom: 1, prenom: 1, region: 1 })
.sort({ region: 1, nom: 1 })
// Benali (Isère), Girard (Rhône), Martin (Rhône), Roux (Savoie)
// 2.
db.fiches.countDocuments({ visiteur: ObjectId('6a0000000000000000000001') }) // 3
// 3.
db.visiteurs.distinct('region') // [ 'Isère', 'Rhône', 'Savoie' ]
// 4.
db.fiches.find({}, { _id: 0, mois: 1, etat: 1 }).sort({ dateModif: -1 }).limit(2)
// [ { mois: '2026-10', etat: 'CL' }, { mois: '2026-10', etat: 'CR' } ]
Pour la 3, Sophie Lambert n'a pas de région : distinct l'ignore. La 4 renvoie la fiche d'octobre de Thomas Girard (modifiée le 1er novembre), puis celle de Lucas Martin (28 octobre).
Tu avais trouvé ?
Exercice 2 sur 3 Saisir une fiche Réussi À revoir
Inès Benali (ObjectId('6a0000000000000000000002')) poursuit sa fiche d'octobre 2026, à l'état CR. Écris les commandes qui :
- ajoutent une ligne de 2 nuitées (NUI, 80 € l'unité) et mettent à jour
dateModif; - passent la quantité de sa ligne KM de 95 à 135, sans dépendre de la position de la ligne ;
- ajoutent un frais hors forfait : « Péage A48 », 8,40 €, le 21 octobre 2026 ;
- clôturent la fiche (état
CL) en la renvoyant modifiée, seulement si elle est encore en cours.
Voir la réponseMasquer la réponse de l'exercice 2
const ines = ObjectId('6a0000000000000000000002');
const filtre = { visiteur: ines, mois: '2026-10' };
// 1.
db.fiches.updateOne(filtre, {
$push: { lignesForfait: { code: 'NUI', quantite: 2, montantUnitaire: 80 } },
$currentDate: { dateModif: true }
})
// 2. Le $ positionnel vise la ligne trouvée par la condition sur le tableau
db.fiches.updateOne(
{ ...filtre, 'lignesForfait.code': 'KM' },
{ $set: { 'lignesForfait.$.quantite': 135 } }
)
// 3.
db.fiches.updateOne(filtre, {
$push: { fraisHorsForfait: { date: ISODate('2026-10-21'), libelle: 'Péage A48', montant: 8.4 } }
})
// 4.
db.fiches.findOneAndUpdate(
{ ...filtre, etat: 'CR' },
{ $set: { etat: 'CL' }, $currentDate: { dateModif: true } },
{ returnDocument: 'after' }
)
{ ...filtre, etat: 'CR' } recopie le filtre et y ajoute une condition : c'est du JavaScript, que mongosh comprend. Pour la 2, $inc: { 'lignesForfait.$.quantite': 40 } convient aussi. Un 'lignesForfait.1.quantite' marcherait ici, mais casserait si l'ordre des lignes changeait.
Tu avais trouvé ?
Exercice 3 sur 3 Prévoir le résultat Réussi À revoir
Sur le jeu de données d'origine, que renvoie chacune de ces commandes ? Que contient la base ensuite ?
db.fiches.updateMany({ etat: 'VA' }, { $set: { etat: 'MP' } })db.fiches.updateOne({ mois: '2027-01' }, { $set: { etat: 'CL' } })db.fiches.deleteMany({ etat: 'cr' })db.visiteurs.updateOne({ matricule: 'a131' }, { region: 'Ain' })
Voir la réponseMasquer la réponse de l'exercice 3
matchedCount: 2, modifiedCount: 2: les deux fiches validées (Martin en septembre, Girard en août) passent en mise en paiement.matchedCount: 0, modifiedCount: 0: aucune fiche de janvier 2027 ; sansupsert, rien n'est créé. Aucune erreur n'est signalée.deletedCount: 0: les valeurs sont sensibles à la casse,'cr'n'est pas'CR'. Rien n'est supprimé.- Une erreur,
Update document requires atomic operators: il manque$set. La base ne change pas. AvecreplaceOne, le visiteur aurait perdu tous ses champs sauf_idetregion.
Tu avais trouvé ?