Pipelines d’agrégation
Fait partie de la section Bases de données avec MongoDB du Journey Node.js de Coddy. Leçon 24 sur 29.
Une liste des mieux notés nécessite des moyennes, un nombre minimum d’évaluations, un ordre et une limite. Chacun de ces éléments constitue une étape.
$match conserve les documents qui satisfont un filtre, comme find. $avg calcule la moyenne d’un champ à l’intérieur de $group. Les étapes s’exécutent dans l’ordre où vous les écrivez, et chacune ne voit que ce que l’étape précédente lui a transmis. Un $match avant $group filtre les documents stockés. Un $match après $group filtre les groupes selon les champs créés par le groupe, comme dans { $match: { orders: { $gte: 3 } } }. Un seul $group peut créer plusieurs champs à la fois, tels que spent: { $sum: '$total' } et orders: { $sum: 1 }.
await orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$customer', spent: { $sum: '$total' } } },
{ $sort: { spent: -1 } },
{ $limit: 5 },
]).toArray();$sort et $limit prennent les mêmes arguments que sur un curseur, mais un pipeline les exécute dans l’ordre où vous les écrivez. Un $limit avant $sort conserve cinq éléments quelconques et ne trie que ceux-ci.
Défi
FacileCette étape ouvre le projet d’API de bibliothèque préparé pour le cours, plutôt que les fichiers de votre projet de Regrouper avec $group (y compris server.js). Votre travail précédent reste dans sa propre leçon. Lisez les fichiers fournis avant de modifier server.js. Cette copie contient les routes de la bibliothèque et le récapitulatif du nombre de livres par genre.
La bibliothèque veut une liste des « mieux notés » : les trois livres ayant la moyenne d’étoiles la plus élevée. Un livre doit avoir au moins deux évaluations pour figurer dans la liste, donc une seule évaluation chanceuse de 5 étoiles ne le propulse pas en tête.
- Dans
server.js, ajoutez une routeGET /stats/top. Elle effectue une agrégation sur la collectionratings, et non surbooks. - Regroupez les évaluations par
bookId. Chaque groupe comporte trois champs :_id, qui est l’identifiant du livre ;avg, la moyenne de sesstars; etcount, le nombre d’évaluations qu’il a reçues. Une seule étape$grouppeut calculer à la foisavgetcount. - Ne gardez que les groupes dont le
countest égal ou supérieur à 2. Cette règle s’applique au groupe d’un livre, et non à une évaluation individuelle ; cette étape vient donc après le regroupement. - Triez les groupes par
avg, du plus élevé au plus bas, et gardez les 3 premiers. - Répondez avec le statut 200 et le tableau. Chaque entrée comporte exactement ces trois champs :
[ { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 }, { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 } ]_idest le_iddu livre ; un client peut donc récupérer le livre avecGET /books/suivi de cet identifiant. S’il y a moins de trois livres ayant deux évaluations, le tableau est plus court. Essayez : chaque exécution videratings, alors envoyez ces requêtes après votre dernière exécution. EnvoyezGET /bookset copiez le_idde Dune, Kindred et Emma. EnvoyezPUT /books/<Dune's _id>/ratingsavec{ "user": "ada", "stars": 5 }, puis une nouvelle fois avec{ "user": "bo", "stars": 4 }. Évaluez Kindred de la même manière, avec 3 étoiles de la part deadaet 3 de la part debo. Évaluez Emma une seule fois, avec{ "user": "ada", "stars": 5 }. EnvoyezGET /stats/top: vous obtenez Dune (avg4.5,count2), puis Kindred (avg3,count2). Emma est absente, car elle n’a qu’une seule évaluation. Si Emma apparaît en premier, votre filtre sur le nombre d’évaluations s’exécute avant le regroupement, ou ne s’exécute pas du tout.
Essayez vous-même
import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');
const ratings = db.collection('ratings');
await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
{ title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
{ title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
{ title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);
const app = express();
app.use(express.json());
// Chaque erreur renvoie { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Le :id sous forme d’ObjectId, ou null après avoir renvoyé 400 pour un id invalide.
function parseId(req, res) {
if (ObjectId.isValid(req.params.id)) {
return new ObjectId(req.params.id);
}
sendError(res, 400, 'Not a valid id');
return null;
}
app.get('/', (req, res) => {
res.send('Bookshelf API');
});
app.get('/books', async (req, res) => {
const filter = {};
if (req.query.author) {
filter.author = req.query.author;
}
if (req.query.from || req.query.to) {
filter.year = {};
if (req.query.from) {
filter.year.$gte = Number(req.query.from);
}
if (req.query.to) {
filter.year.$lte = Number(req.query.to);
}
}
const list = await books.find(filter).toArray();
res.json(list);
});
app.post('/books', async (req, res) => {
const book = req.body;
try {
await books.insertOne(book);
} catch (err) {
if (err.code === 11000) {
return sendError(res, 409, 'That ISBN is already on the shelf');
}
throw err;
}
res.status(201).json(book);
});
app.get('/books/newest', async (req, res) => {
const n = Number(req.query.n) || 3;
const list = await books
.find({}, { projection: { title: 1, year: 1, _id: 0 } })
.sort({ year: -1 })
.limit(n)
.toArray();
res.json(list);
});
app.get('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const book = await books.findOne({ _id: id });
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
app.patch('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $set: req.body });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/copies', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/tags', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.delete('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.deleteOne({ _id: id });
if (result.deletedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.status(204).end();
});
app.put('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const { user, stars } = req.body;
await ratings.updateOne({ bookId: id, user }, { $set: { stars } }, { upsert: true });
res.json({ user, stars });
});
app.get('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
res.json(await ratings.find({ bookId: id }).toArray());
});
app.get('/stats', async (req, res) => {
const filter = req.query.genre ? { genre: req.query.genre } : {};
res.json({
books: await books.countDocuments(filter),
ratings: await ratings.countDocuments(),
});
});
app.get('/stats/genres', async (req, res) => {
const genres = await books
.aggregate([{ $group: { _id: '$genre', count: { $sum: 1 } } }])
.toArray();
res.json(genres);
});
app.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Bases de données avec MongoDB
1Documents
Un livre comme documentCollections et bases de donnéesSe connecter à MongoDBLire des documents stockésInsérer des documentsTrouver un documentRechercher par ObjectIdComparer des nombresFiltres de requêteChoisir les champs renvoyésTrier les résultats de requêteConserver les premiers résultatsProjection, tri et limiteEntraînez-vous par vous-même : Playground Express