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.
| Relationnel | MongoDB | Dans AppliFrais |
|---|---|---|
| Base de données | Base de données | appli_frais |
| Table | Collection | visiteurs, fiches |
| Ligne | Document | La fiche de septembre 2026 de Lucas Martin |
| Colonne | Champ | mois, etat, montantValide |
| Clé primaire | Champ _id, toujours présent et unique | ObjectId('6a00…0001') |
| Clé étrangère | Référence : un champ qui contient l'_id d'un autre document | visiteur dans une fiche |
| Jointure | Document imbriqué, $lookup ou populate | Voir les chapitres 6, 8 et 10 |
| Table de lignes d'une facture | Tableau de sous-documents dans le document | lignesForfait |
Voici la fiche de frais de septembre 2026 de Lucas Martin, telle que MongoDB la renvoie :
{
_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 :
moisest une chaîne,montantValideun nombre,dateModifune date ; - une référence :
visiteurcontient l'_idd'un document de la collectionvisiteurs, comme une clé étrangère ; - des tableaux de sous-documents :
lignesForfaitcontient 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
$jsonSchemacô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 BSON | Exemple dans mongosh | Usage |
|---|---|---|
string | '2026-09' | Texte, en UTF-8 |
int (32 bits) | 3 | Quantités, compteurs (mongosh range un entier littéral dans ce type) |
long (64 bits) | Long('5000000000') | Grands entiers |
double | 0.62 | Nombres à virgule (approximatifs) |
decimal (Decimal128) | Decimal128('110.00') | Nombres décimaux exacts : montants |
bool | true | Booléens |
date | ISODate('2026-10-12T09:30:00.000Z') | Dates et heures, en UTC, à la milliseconde |
null | null | Valeur absente mais champ présent |
object | { code: 'KM', quantite: 420 } | Sous-document |
array | [ 'Rhône', 'Isère' ] | Liste, de valeurs ou de sous-documents |
objectId | ObjectId('6a00…0001') | Identifiants |
binData | UUID('…'), 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')plusDecimal128('0.2')donne exactementDecimal128('0.3'); - des entiers en centimes (
2350pour 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 :
| Octets | Contenu |
|---|---|
| 4 premiers | L'horodatage de création, en secondes depuis le 1er janvier 1970 |
| 5 suivants | Une valeur aléatoire, propre à la machine et au processus qui l'a généré |
| 3 derniers | Un 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_INCREMENTdemande un compteur central ; - les ObjectId sont à peu près croissants dans le temps : trier par
_idtrie à peu près par date de création ; - la date de création se lit dans l'identifiant :
ObjectId('6ae8a9d8fc828abd94f81c6f').getTimestamp()renvoieISODate('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 :
- Quel est le type BSON de
visiteur, demois, dedateModif, delignesForfait? - Quel chemin, en notation pointée, désigne la quantité de la deuxième ligne de frais forfaitisés ?
- Que vaut le total des frais forfaitisés ? Et le total de la fiche ? Vérifie-le avec
montantValide. - À 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
visiteur:objectId;mois:string;dateModif:date;lignesForfait:array(de sous-documents, de typeobject).lignesForfait.1.quantite, qui vaut 420 : les indices de tableau commencent à 0.- 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é. - 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'_idet donc cette date seront différents. Ce n'est pasdateModif, 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
{
_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.
db.fiches.find({ visiteur: '6a0000000000000000000002' })db.visiteurs.find({ dateEmbauche: '2023-03-13' })db.fiches.find({ Etat: 'CL' })
Voir la réponseMasquer la réponse de l'exercice 3
visiteurcontient un ObjectId, pas une chaîne : il faut{ visiteur: ObjectId('6a0000000000000000000002') }.dateEmbaucheest 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.- Les noms de champs sont sensibles à la casse : le champ s'appelle
etat, pasEtat. MongoDB ne signale pas de champ inconnu dans un filtre : il cherche simplement des documents qui ont un champEtat, et n'en trouve aucun.
Tu avais trouvé ?