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

  1. Quelles sont les requêtes principales ?
  2. Ratio lecture/écriture ?
  3. Quelle taille maximale des documents ?
  4. Les données sont-elles toujours accédées ensemble ?
  5. Fréquence de mise à jour des données ?
  6. Besoin de cohérence forte ?