Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 213 minIntermédiaire

Document, collection, BSON et ObjectId

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • employer le vocabulaire base, collection, document et champ ;
  • choisir le bon type BSON, en particulier pour les dates et les montants ;
  • expliquer ce que contient un ObjectId et pourquoi ce n'est pas une chaîne ;
  • connaître les limites d'un document.

3 exercices corrigés

Un document MongoDB est un ensemble de paires champ-valeur, écrit comme un objet JSON, qui peut contenir d'autres documents et des tableaux. Les documents sont rangés dans des collections, elles-mêmes rangées dans des bases. MongoDB les stocke au format BSON, une version binaire et typée de JSON, et identifie chacun par un champ _id, le plus souvent un ObjectId. Ce chapitre décrit ce vocabulaire et les types que tu utiliseras dans toutes tes requêtes.

Base, collection, document : le vocabulaire

Une instance MongoDB (le serveur mongod) héberge plusieurs bases ; chaque base contient des collections ; chaque collection contient des documents.

RelationnelMongoDBDans AppliFrais
Base de donnéesBase de donnéesappli_frais
TableCollectionvisiteurs, fiches
LigneDocumentLa fiche de septembre 2026 de Lucas Martin
ColonneChampmois, etat, montantValide
Clé primaireChamp _id, toujours présent et uniqueObjectId('6a00…0001')
Clé étrangèreRéférence : un champ qui contient l'_id d'un autre documentvisiteur dans une fiche
JointureDocument imbriqué, $lookup ou populateVoir les chapitres 6, 8 et 10
Table de lignes d'une factureTableau de sous-documents dans le documentlignesForfait

Voici la fiche de frais de septembre 2026 de Lucas Martin, telle que MongoDB la renvoie :

js
{
  _id: ObjectId('6ae8a9d8fc828abd94f81c6f'),
  visiteur: ObjectId('6a0000000000000000000001'),
  mois: '2026-09',
  etat: 'VA',
  dateModif: ISODate('2026-10-12T09:30:00.000Z'),
  lignesForfait: [
    { code: 'ETP', quantite: 3, montantUnitaire: 110 },
    { code: 'KM', quantite: 420, montantUnitaire: 0.62 },
    { code: 'NUI', quantite: 2, montantUnitaire: 80 },
    { code: 'REP', quantite: 5, montantUnitaire: 25 }
  ],
  fraisHorsForfait: [
    { date: ISODate('2026-09-14T00:00:00.000Z'), libelle: 'Repas client à Lyon', montant: 23.5 }
  ],
  montantValide: 898.9
}

On y lit tout le vocabulaire du modèle :

  • des champs simples : mois est une chaîne, montantValide un nombre, dateModif une date ;
  • une référence : visiteur contient l'_id d'un document de la collection visiteurs, comme une clé étrangère ;
  • des tableaux de sous-documents : lignesForfait contient quatre documents, un par type de frais forfaitisé (ETP pour forfait étape, KM pour frais kilométriques, NUI pour nuitée, REP pour repas) ;
  • l'identifiant _id, généré par MongoDB à l'insertion.

Les codes d'état sont ceux d'AppliFrais : CR (fiche créée, en cours de saisie), CL (clôturée par le visiteur), VA (validée par le comptable), MP (mise en paiement), RB (remboursée).

Un schéma souple, mais un schéma quand même

Rien n'empêche deux documents de la même collection d'avoir des champs différents. Dans le jeu de données du cours, Sophie Lambert, comptable, n'a pas de champ region, alors que les visiteurs en ont un. En relationnel, il faudrait une colonne region vide pour elle ; ici, le champ est simplement absent.

Cette souplesse sert pendant le développement (ajouter un champ ne demande aucune migration) et pour les données réellement variables. Elle devient un piège quand elle n'est pas voulue : une faute de frappe (montantValider au lieu de montantValide) crée silencieusement un nouveau champ, et la requête qui cherche montantValide ne trouve plus le document. D'où deux protections, qu'on verra plus loin :

  • un schéma Mongoose côté application, qui ne garde que les champs déclarés et vérifie les types (chapitre 10) ;
  • un validateur $jsonSchema côté base, qui refuse un document mal formé quelle que soit l'application qui écrit (chapitre 6).

BSON : du JSON binaire et typé

JSON, que tu connais par les API (le format JSON), n'a que six types : chaîne, nombre, booléen, null, objet et tableau. C'est trop peu pour une base de données : JSON ne distingue pas un entier d'un décimal, et n'a pas de type date. MongoDB stocke donc ses documents en BSON (Binary JSON), un format binaire qui ajoute des types et la longueur de chaque élément, pour que le serveur parcoure un document sans le relire entièrement.

Type BSONExemple dans mongoshUsage
string'2026-09'Texte, en UTF-8
int (32 bits)3Quantités, compteurs (mongosh range un entier littéral dans ce type)
long (64 bits)Long('5000000000')Grands entiers
double0.62Nombres à virgule (approximatifs)
decimal (Decimal128)Decimal128('110.00')Nombres décimaux exacts : montants
booltrueBooléens
dateISODate('2026-10-12T09:30:00.000Z')Dates et heures, en UTC, à la milliseconde
nullnullValeur absente mais champ présent
object{ code: 'KM', quantite: 420 }Sous-document
array[ 'Rhône', 'Isère' ]Liste, de valeurs ou de sous-documents
objectIdObjectId('6a00…0001')Identifiants
binDataUUID('…'), BinData(…)Données binaires, UUID

Tu vérifies le type réel d'un champ avec l'opérateur $type (chapitre 5), ou dans Compass, qui l'affiche à côté de chaque valeur.

Les nombres et l'argent

Un double est un nombre à virgule flottante, comme tous les nombres de JavaScript : il ne représente pas exactement la plupart des décimaux. Dans mongosh, 0.1 + 0.2 affiche 0.30000000000000004. Pour des montants qu'on additionne, deux solutions propres existent :

  • le type Decimal128, exact : Decimal128('0.1') plus Decimal128('0.2') donne exactement Decimal128('0.3') ;
  • des entiers en centimes (2350 pour 23,50 €), comme le font beaucoup d'API de paiement.

Le jeu de données du cours garde des double pour que les exemples restent lisibles, et arrondit au centime avec $round dans les calculs (chapitre 8). Pour un vrai logiciel comptable, choisis Decimal128 ou les centimes dès le départ : changer de type plus tard demande de réécrire tous les documents.

L'identifiant _id et l'ObjectId

Chaque document a un champ _id, unique dans sa collection, qui joue le rôle de clé primaire. MongoDB crée automatiquement un index unique dessus. Si tu n'en fournis pas, le driver en génère un de type ObjectId au moment de l'insertion.

Un ObjectId tient sur 12 octets, affichés en 24 caractères hexadécimaux :

OctetsContenu
4 premiersL'horodatage de création, en secondes depuis le 1er janvier 1970
5 suivantsUne valeur aléatoire, propre à la machine et au processus qui l'a généré
3 derniersUn compteur, qui démarre à une valeur aléatoire

Cette construction a trois conséquences utiles :

  • deux programmes peuvent générer des identifiants sans se concerter et sans risque de doublon : c'est ce qui permet de répartir la base sur plusieurs serveurs, là où un AUTO_INCREMENT demande un compteur central ;
  • les ObjectId sont à peu près croissants dans le temps : trier par _id trie à peu près par date de création ;
  • la date de création se lit dans l'identifiant : ObjectId('6ae8a9d8fc828abd94f81c6f').getTimestamp() renvoie ISODate('2026-11-02T15:18:48.000Z').

Tu peux aussi fournir ton propre _id, de n'importe quel type sauf un tableau ou une expression régulière : un matricule, un code ISO de pays, un numéro de commande métier. C'est pertinent quand l'identifiant naturel est stable et unique. Les visiteurs du jeu de données ont des ObjectId fixés à la main (6a0000000000000000000001, …0002…) pour que tes résultats ressemblent à ceux du cours ; dans une vraie application, laisse MongoDB les générer.

Les limites d'un document

  • 16 Mio par document, au maximum. C'est énorme pour des données texte (la fiche ci-dessus pèse moins d'un kilo-octet), mais c'est une limite dure : un tableau qui grossit sans fin (l'historique de toutes les connexions d'un utilisateur, les commentaires d'un article très populaire) finira par l'atteindre. C'est un critère de modélisation majeur (chapitre 6).
  • 100 niveaux d'imbrication, au maximum. Tu n'en approcheras jamais avec un modèle raisonnable.
  • Les noms de champs : évite ceux qui commencent par $ (réservé aux opérateurs) ou qui contiennent un point (réservé à la notation pointée, lignesForfait.code).
  • Les fichiers : une image ou un PDF n'a rien à faire dans un document. Range-le sur un disque ou dans un stockage objet (comme S3), et garde son chemin dans le document. Pour les rares cas où il faut le garder en base, MongoDB propose GridFS, qui découpe un fichier en morceaux de 255 Kio.

Exercices

Exercice 1 sur 3 Lire un document Réussi À revoir

À partir de la fiche de septembre 2026 de Lucas Martin montrée plus haut :

  1. Quel est le type BSON de visiteur, de mois, de dateModif, de lignesForfait ?
  2. Quel chemin, en notation pointée, désigne la quantité de la deuxième ligne de frais forfaitisés ?
  3. Que vaut le total des frais forfaitisés ? Et le total de la fiche ? Vérifie-le avec montantValide.
  4. À quelle date cette fiche a-t-elle été insérée dans la base ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. visiteur : objectId ; mois : string ; dateModif : date ; lignesForfait : array (de sous-documents, de type object).
  2. lignesForfait.1.quantite, qui vaut 420 : les indices de tableau commencent à 0.
  3. ETP 3 × 110 = 330 ; KM 420 × 0,62 = 260,40 ; NUI 2 × 80 = 160 ; REP 5 × 25 = 125 ; soit 875,40 € de frais forfaitisés. Avec le frais hors forfait de 23,50 €, la fiche totalise 898,90 € : c'est bien montantValide, le comptable a tout accepté.
  4. On lit l'horodatage dans les 4 premiers octets de l'_id : ObjectId('6ae8a9d8fc828abd94f81c6f').getTimestamp() donne le 2 novembre 2026 à 15 h 18 min 48 s (UTC), le moment où le jeu de données a été chargé pour écrire ce cours ; chez toi, l'_id et donc cette date seront différents. Ce n'est pas dateModif, qui est une donnée métier.

Tu avais trouvé ?

Exercice 2 sur 3 Choisir les types Réussi À revoir

Tu ajoutes à AppliFrais une collection vehicules pour les voitures de fonction. Propose un document d'exemple et le type BSON de chaque champ : immatriculation, modèle, date de mise en circulation, puissance fiscale, kilométrage, liste des visiteurs qui l'ont utilisée, prix d'achat.

Voir la réponseMasquer la réponse de l'exercice 2
js
{
  _id: 'GH-482-KL',                              // string : l'immatriculation, identifiant naturel
  modele: 'Peugeot e-208',                       // string
  miseEnCirculation: ISODate('2024-03-18'),      // date
  puissanceFiscale: 5,                           // int
  kilometrage: 48210,                            // int (un long au-delà de 2 147 483 647)
  utilisateurs: [                                // array d'objectId : des références
    ObjectId('6a0000000000000000000001'),
    ObjectId('6a0000000000000000000003')
  ],
  prixAchat: Decimal128('31450.00')              // decimal : un montant exact
}

L'immatriculation peut servir d'_id : elle est unique et ne change pas pendant la vie du véhicule en France. Un ObjectId généré, avec un champ immatriculation sous index unique, est aussi un bon choix. Les utilisateurs sont référencés par leur _id : on ne recopie pas les visiteurs dans le véhicule.

Tu avais trouvé ?

Exercice 3 sur 3 Trouver l'erreur Réussi À revoir

Chacune de ces requêtes ne renvoie rien alors que des documents correspondent. Explique pourquoi.

  1. db.fiches.find({ visiteur: '6a0000000000000000000002' })
  2. db.visiteurs.find({ dateEmbauche: '2023-03-13' })
  3. db.fiches.find({ Etat: 'CL' })
Voir la réponseMasquer la réponse de l'exercice 3
  1. visiteur contient un ObjectId, pas une chaîne : il faut { visiteur: ObjectId('6a0000000000000000000002') }.
  2. dateEmbauche est une date : la chaîne '2023-03-13' ne lui est jamais égale. Il faut { dateEmbauche: ISODate('2023-03-13') }, ou un intervalle de dates si l'heure peut varier.
  3. Les noms de champs sont sensibles à la casse : le champ s'appelle etat, pas Etat. MongoDB ne signale pas de champ inconnu dans un filtre : il cherche simplement des documents qui ont un champ Etat, et n'en trouve aucun.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Installer MongoDB : Docker, Atlas, mongosh et CompassRevoir le sommaire du cours