Regrouper avec $group
Fait partie de la section Bases de données avec MongoDB du Journey Node.js de Coddy. Leçon 21 sur 29.
Compter un genre correspond à une requête. Compter tous les genres à la fois nécessite que la base de données trie les livres en groupes.
aggregate exécute un pipeline : un tableau d’étapes, chacune transmettant sa sortie à la suivante. L’étape $group crée un document de sortie pour chaque valeur distincte de son _id. Ici, _id n’est pas l’identifiant d’un document. Il indique selon quelle valeur regrouper, et chaque document de sortie contient cette valeur dans _id. Un $ devant un nom de champ, '$team', signifie « la valeur de ce champ dans ce document ». Sans lui, 'team' est le même texte pour chaque document, donc ils se retrouvent tous dans un seul groupe. $sum: 1 ajoute 1 pour chaque document.
await players
.aggregate([{ $group: { _id: '$team', players: { $sum: 1 } } }])
.toArray();
// [{ _id: 'red', players: 4 }, { _id: 'blue', players: 3 }]Un résultat de regroupement contient uniquement cet _id et les champs calculés par ses accumulateurs ; les autres champs des documents d’entrée ne sont pas conservés.
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 Comptage de documents (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 CRUD de la bibliothèque, l’index ISBN unique, les filtres GET /books et les décomptes GET /stats.
Une page des genres affiche le nombre de livres de chaque genre, le tout à partir d’une seule requête, y compris les genres auxquels personne n’avait pensé.
- Dans
server.js, ajoutez une routeGET /stats/genres. - Effectuez une agrégation sur
booksavec une seule étape$group. Elle produit un résultat pargenredistinct, avec le nombre de livres qui en font partie. - Renvoyez le statut 200 et le tableau renvoyé par l’agrégation. Chaque entrée contient le genre dans
_idet le nombre de livres danscount. Dans$group,_idn’est pas un ObjectId : c’est la valeur selon laquelle les livres ont été regroupés. Les entrées peuvent apparaître dans n’importe quel ordre. Avec les livres de départ, la réponse est :Essayez : Envoyez[ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]GET /stats/genres: vous obtenez les deux entrées ci-dessus. EnvoyezPOST /booksavec{ "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }: vous obtenez 201. Envoyez de nouveauGET /stats/genres: une troisième entrée apparaît,{ "_id": "poetry", "count": 1 }.
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 si l’id est 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.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
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