5 exercices · Solutions masquées
Modélisez un blog MongoDB avec des collections users, posts et comments. Insérez 2 users, 3 posts (avec tags embedded), 4 commentaires. Choisissez embedding vs référence en justifiant.
const { MongoClient, ObjectId } = require('mongodb');
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const db = client.db('blog');
// Users
const u1 = await db.collection('users').insertOne({
name: 'Alice Dupont', email: 'alice@example.com',
role: 'author', createdAt: new Date()
});
const u2 = await db.collection('users').insertOne({
name: 'Bob Martin', email: 'bob@example.com',
role: 'reader', createdAt: new Date()
});
// Posts (auteur en référence car modifiable, tags embeddés car peu nombreux)
const p1 = await db.collection('posts').insertOne({
title: 'Introduction à MongoDB',
body: 'MongoDB est une base NoSQL orientée documents...',
authorId: u1.insertedId, // référence
tags: ['mongodb', 'nosql', 'database'], // embedding (1-to-few)
published: true, views: 0, createdAt: new Date()
});
const p2 = await db.collection('posts').insertOne({
title: 'PostgreSQL vs MongoDB',
body: 'Comparaison entre SGBD relationnel et NoSQL...',
authorId: u1.insertedId,
tags: ['postgresql', 'mongodb', 'comparison'],
published: true, views: 0, createdAt: new Date()
});
// Commentaires (référence au post et à l'auteur)
await db.collection('comments').insertMany([
{ postId: p1.insertedId, authorId: u2.insertedId,
content: 'Super article !', createdAt: new Date() },
{ postId: p1.insertedId, authorId: u2.insertedId,
content: 'Merci pour les explications.', createdAt: new Date() },
{ postId: p2.insertedId, authorId: u2.insertedId,
content: 'Très instructif !', createdAt: new Date() },
{ postId: p2.insertedId, authorId: u1.insertedId,
content: 'Bonne comparaison.', createdAt: new Date() }
]);
Sur la collection products (name, price, category, stock, tags[]), effectuez : (a) trouver tous les produits "electronics" de prix entre 50 et 200€, (b) ajouter le tag "sale" aux produits dont le stock > 10, (c) trouver les produits avec le tag "sale" ET le tag "electronics", (d) supprimer les produits hors stock depuis 30 jours.
const products = db.collection('products');
// Seed
await products.insertMany([
{ name: 'Laptop', price: 899, category: 'electronics', stock: 15,
tags: ['electronics'], lastStocked: new Date() },
{ name: 'Mouse', price: 29, category: 'electronics', stock: 50,
tags: ['electronics', 'accessories'], lastStocked: new Date() },
{ name: 'Book JS', price: 35, category: 'books', stock: 5,
tags: ['books', 'programming'], lastStocked: new Date() },
{ name: 'Headphones', price: 149, category: 'electronics', stock: 0,
tags: ['electronics', 'audio'],
lastStocked: new Date(Date.now() - 35 * 86400000) }
]);
// (a) Electronics 50-200€
const electronics = await products.find({
category: 'electronics',
price: { $gte: 50, $lte: 200 }
}).toArray();
// (b) Ajouter tag "sale" aux produits stock > 10
await products.updateMany(
{ stock: { $gt: 10 } },
{ $addToSet: { tags: 'sale' } }
);
// (c) Produits avec tags "sale" ET "electronics"
const saleElec = await products.find({
tags: { $all: ['sale', 'electronics'] }
}).toArray();
// (d) Supprimer hors stock depuis 30 jours
const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000);
const deleted = await products.deleteMany({
stock: 0,
lastStocked: { $lt: thirtyDaysAgo }
});
console.log(`${deleted.deletedCount} produits supprimés`);
Créez les index optimaux pour la collection orders (userId, status, createdAt, items[].productId, total). Créez un index TTL pour les commandes "abandoned" (expiration 7 jours). Utilisez explain("executionStats") pour vérifier.
const orders = db.collection('orders');
// Index composé pour les requêtes typiques : orders d'un user triées par date
await orders.createIndex({ userId: 1, createdAt: -1 });
// Index sur status pour les filtres de statut
await orders.createIndex({ status: 1 });
// Index sur items.productId pour findByProduct
await orders.createIndex({ 'items.productId': 1 });
// Index composé pour analytics : status + date
await orders.createIndex({ status: 1, createdAt: -1 });
// Index TTL : supprimer les commandes "abandoned" après 7 jours
// Nécessite un champ abandonedAt mis à jour par l'application
await orders.createIndex(
{ abandonedAt: 1 },
{ expireAfterSeconds: 7 * 24 * 3600, sparse: true }
);
// Vérification avec explain
const plan = await orders.find({
userId: new ObjectId('...'),
createdAt: { $gte: new Date('2024-01-01') }
}).explain('executionStats');
console.log('Stage:', plan.queryPlanner.winningPlan.stage);
console.log('Docs examined:', plan.executionStats.totalDocsExamined);
console.log('Keys examined:', plan.executionStats.totalKeysExamined);
Implémentez un compteur de vues atomique et un système de réservation de places. (a) Incrémenter le compteur de vues d'un article et retourner le nouveau total. (b) Réserver une place dans un événement (decrement seulement si places > 0).
// (a) Compteur de vues atomique
async function incrementViews(articleId) {
const result = await db.collection('articles').findOneAndUpdate(
{ _id: new ObjectId(articleId) },
{
$inc: { views: 1 },
$currentDate: { lastViewed: true }
},
{ returnDocument: 'after', projection: { title: 1, views: 1 } }
);
return result; // { title: '...', views: 42 }
}
// (b) Réservation de place (atomic decrement with condition)
async function reserveSpot(eventId, userId) {
const result = await db.collection('events').findOneAndUpdate(
{
_id: new ObjectId(eventId),
availableSpots: { $gt: 0 },
registeredUsers: { $ne: new ObjectId(userId) } // pas déjà inscrit
},
{
$inc: { availableSpots: -1 },
$addToSet: { registeredUsers: new ObjectId(userId) },
$currentDate: { updatedAt: true }
},
{ returnDocument: 'after' }
);
if (!result) {
throw new Error('Aucune place disponible ou déjà inscrit');
}
return { success: true, remainingSpots: result.availableSpots };
}
Modélisez une collection carts (panier d'achat) avec : userId (ref), items embeddings (productId, name, price snapshot, qty), total calculé, expiresAt (TTL 24h). Implémentez addItem, removeItem, et clearCart.
const carts = db.collection('carts');
// Index TTL — panier expire après 24h d'inactivité
await carts.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
);
await carts.createIndex({ userId: 1 }, { unique: true });
async function addItem(userId, product, qty = 1) {
// Snapshot du prix à l'instant T (important en e-commerce)
const item = {
productId: new ObjectId(product._id),
name: product.name,
price: product.price, // snapshot
qty,
addedAt: new Date()
};
const result = await carts.findOneAndUpdate(
{ userId: new ObjectId(userId), 'items.productId': { $ne: item.productId } },
{
$push: { items: item },
$inc: { total: product.price * qty },
$set: { expiresAt: new Date(Date.now() + 24 * 3600 * 1000) },
$setOnInsert: { createdAt: new Date() }
},
{ upsert: true, returnDocument: 'after' }
);
return result;
}
async function removeItem(userId, productId) {
const cart = await carts.findOne({ userId: new ObjectId(userId) });
if (!cart) return null;
const item = cart.items.find(i => i.productId.equals(productId));
if (!item) return null;
return carts.findOneAndUpdate(
{ userId: new ObjectId(userId) },
{
$pull: { items: { productId: new ObjectId(productId) } },
$inc: { total: -(item.price * item.qty) }
},
{ returnDocument: 'after' }
);
}
async function clearCart(userId) {
return carts.findOneAndUpdate(
{ userId: new ObjectId(userId) },
{ $set: { items: [], total: 0 } },
{ returnDocument: 'after' }
);
}