Module 5 : Schema Design
Philosophie MongoDB
La modélisation MongoDB est différente du relationnel :
+----------------------------------------------------------+
| RELATIONNEL vs MONGODB |
+----------------------------------------------------------+
| |
| RELATIONNEL: |
| - Normalisation (éviter redondance) |
| - Jointures à l'exécution |
| - Schema rigide |
| |
| MONGODB: |
| - Modéliser selon les accès |
| - Embedding (denormalisation) |
| - Schema flexible |
| - "Data that is accessed together should be stored |
| together" |
| |
+----------------------------------------------------------+
Embedding vs Referencing
Embedding (Imbrication)
// Document avec données imbriquées
{
_id: ObjectId("..."),
nom: "Dupont",
email: "dupont@test.com",
adresse: {
rue: "123 Rue de la Paix",
ville: "Paris",
codePostal: "75001"
},
commandes: [
{ date: ISODate("..."), total: 150.00, produits: [...] },
{ date: ISODate("..."), total: 89.50, produits: [...] }
]
}
// Avantages:
// - Une seule requête pour tout
// - Atomicité (mise à jour transactionnelle)
// - Performances de lecture
// Inconvenients:
// - Limite de 16MB par document
// - Duplication de données
// - Difficile si données partagées
Referencing (Références)
// Collection users
{
_id: ObjectId("user1"),
nom: "Dupont",
email: "dupont@test.com"
}
// Collection orders (avec reference)
{
_id: ObjectId("order1"),
userId: ObjectId("user1"), // Reference
date: ISODate("..."),
total: 150.00
}
// Lecture avec $lookup
db.orders.aggregate([
{ $match: { _id: ObjectId("order1") } },
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}}
])
// Avantages:
// - Pas de duplication
// - Pas de limite de taille
// - Données partagées facilement
// Inconvenients:
// - Plusieurs requêtes ou $lookup
// - Pas d'atomicité native
Quand utiliser quoi ?
| Critère | Embedding | Referencing |
|---|---|---|
| Relation | 1:1, 1:Few | 1:Many, Many:Many |
| Accès données | Toujours ensemble | Indépendamment |
| Taille | Petite, bornée | Grande, non bornée |
| Mise à jour | Atomique nécessaire | Indépendante OK |
| Duplication | Acceptable | À éviter |
Patterns de modélisation
Pattern: Subset
// Problème: Document trop gros avec historique complet
// Solution: Garder seulement les N derniers éléments
// Document principal (lectures fréquentes)
{
_id: ObjectId("..."),
nom: "Dupont",
dernieresCommandes: [
// 10 dernières seulement
{ date: ISODate("..."), total: 150 },
{ date: ISODate("..."), total: 89 }
]
}
// Collection archive (lectures rares)
{
userId: ObjectId("..."),
commandes: [
// Historique complet
]
}
Pattern: Computed
// Problème: Calculs coûteux à chaque lecture
// Solution: Pré-calculer et stocker
{
_id: ObjectId("..."),
produit: "Widget",
ventes: [...], // Tableau de ventes individuelles
// Champs pré-calculés (mis à jour lors des écritures)
stats: {
totalVentes: 15000,
nombreVentes: 250,
venteMoyenne: 60,
derniereMiseAJour: ISODate("...")
}
}
// Mise a jour atomique
db.products.updateOne(
{ _id: ObjectId("...") },
{
$push: { ventes: nouvelleVente },
$inc: {
"stats.totalVentes": nouvelleVente.montant,
"stats.nombreVentes": 1
},
$set: { "stats.derniereMiseAJour": new Date() }
}
)
Pattern: Bucket
// Problème: Time-series avec beaucoup de petits documents
// Solution: Grouper par période
// Au lieu de:
{ timestamp: ISODate("..."), temperature: 22.5 }
{ timestamp: ISODate("..."), temperature: 22.6 }
// ... millions de documents
// Utiliser:
{
sensorId: "sensor-001",
date: ISODate("2024-01-15"), // Jour
mesures: [
{ heure: 0, min: 22.1, max: 22.5, avg: 22.3, count: 60 },
{ heure: 1, min: 22.0, max: 22.4, avg: 22.2, count: 60 },
// ... 24 heures
],
stats: {
min: 21.5,
max: 24.2,
avg: 22.8
}
}
Pattern: Extended Reference
// Problème: $lookup fréquent et coûteux
// Solution: Copier les champs fréquemment accédés
// Collection orders
{
_id: ObjectId("..."),
userId: ObjectId("user1"),
// Référence étendue (copie des champs fréquents)
user: {
nom: "Dupont",
email: "dupont@test.com"
// Pas tous les champs, juste ceux nécessaires
},
produits: [...]
}
// Attention: nécessite de maintenir la cohérence
// lors des mises a jour du user
Pattern: Polymorphic
// Documents de types différents dans la même collection
// Produits physiques
{
type: "physique",
nom: "Laptop",
prix: 999,
poids: 1.5,
dimensions: { l: 30, h: 2, p: 20 }
}
// Produits numériques
{
type: "digital",
nom: "eBook",
prix: 15,
format: "PDF",
tailleMo: 5
}
// Requete polymorphe
db.products.find({ prix: { $lt: 100 } }) // Tous types
Schema Validation
// Définir un schéma de validation
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["nom", "email"],
properties: {
nom: {
bsonType: "string",
description: "Nom requis"
},
email: {
bsonType: "string",
pattern: "^.+@.+$",
description: "Email valide requis"
},
age: {
bsonType: "int",
minimum: 0,
maximum: 150
},
status: {
enum: ["actif", "inactif", "suspendu"]
}
}
}
},
validationLevel: "moderate", // strict | moderate
validationAction: "error" // error | warn
})
// Modifier la validation
db.runCommand({
collMod: "users",
validator: { ... }
})
Anti-patterns
À éviter :
- Massive Arrays - Tableaux qui grandissent indéfiniment
- Unnecessary Indexes - Index sur chaque champ
- Bloated Documents - Documents > 16MB
- Over-normalization - Trop de references comme en SQL
- Case-sensitive Fields - Mélanger casses (Name, name, NAME)
Checklist modélisation
- Quelles sont les requêtes principales ?
- Ratio lecture/écriture ?
- Quelle taille maximale des documents ?
- Les données sont-elles toujours accédées ensemble ?
- Fréquence de mise à jour des données ?
- Besoin de cohérence forte ?