Aggregation-Pipelines
Teil des Abschnitts Datenbanken mit MongoDB der Node.js-Journey von Coddy. Lektion 24 von 29.
Eine Liste der „bestbewerteten“ Einträge benötigt Durchschnittswerte, eine Mindestanzahl an Bewertungen, eine Sortierung und eine Begrenzung. Jeder dieser Schritte ist eine Phase.
$match behält die Dokumente, die einen Filter erfüllen, ähnlich wie find. $avg berechnet innerhalb von $group den Durchschnitt eines Feldes. Die Phasen werden in der Reihenfolge ausgeführt, in der du sie schreibst, und jede sieht nur das, was die vorherige Phase weitergegeben hat. Ein $match vor $group filtert die gespeicherten Dokumente. Ein $match nach $group filtert die Gruppen anhand von Feldern, die die Gruppe erstellt hat, wie in { $match: { orders: { $gte: 3 } } }. Ein $group kann mehrere Felder gleichzeitig erstellen, zum Beispiel spent: { $sum: '$total' } und orders: { $sum: 1 }.
await orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$customer', spent: { $sum: '$total' } } },
{ $sort: { spent: -1 } },
{ $limit: 5 },
]).toArray();$sort und $limit erwarten dieselben Argumente wie bei einem Cursor, aber eine Pipeline führt sie in der Reihenfolge aus, in der du sie schreibst. Ein $limit vor $sort behält beliebige fünf Einträge bei und sortiert nur diese.
Aufgabe
EinfachDieser Schritt öffnet das vorbereitete Bookshelf-API-Projekt des Kurses statt deiner Projektdateien aus Grouping with $group (einschließlich server.js). Deine frühere Arbeit bleibt in ihrer eigenen Lektion. Lies die bereitgestellten Dateien, bevor du server.js bearbeitest. Diese Kopie enthält die Bookshelf-Routen und die Zusammenfassung der Genre-Anzahlen.
Das Bücherregal soll eine Liste der „am besten bewerteten“ Bücher anzeigen: die drei Bücher mit dem höchsten Sterndurchschnitt. Ein Buch braucht mindestens zwei Bewertungen, um in der Liste zu erscheinen. Eine einzige glückliche 5-Sterne-Bewertung reicht also nicht aus, um an die Spitze zu kommen.
- Füge in
server.jseineGET /stats/top-Route hinzu. Sie führt eine Aggregation auf der Sammlungratingsaus, nicht aufbooks. - Gruppiere die Bewertungen nach
bookId. Jede Gruppe erhält drei Felder:_id, die ID des Buches;avg, den Durchschnitt seinerstars; undcount, die Anzahl seiner Bewertungen. Eine$group-Stufe kann sowohlavgals auchcountberechnen. - Behalte nur die Gruppen, deren
countmindestens 2 beträgt. Diese Regel bezieht sich auf die Gruppe eines Buches, nicht auf eine einzelne Bewertung. Daher kommt diese Stufe nach der Gruppierung. - Sortiere die Gruppen nach
avg, beginnend mit dem höchsten Wert, und behalte die ersten 3. - Antworte mit dem Status 200 und dem Array. Jeder Eintrag hat genau diese drei Felder:
[ { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 }, { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 } ]_idist die_iddes Buches, sodass ein Client das Buch mitGET /books/und dieser ID abrufen kann. Wenn weniger als drei Bücher zwei Bewertungen haben, ist das Array kürzer. Probiere es aus: Bei jedem Klick auf „Ausführen“ wirdratingsgeleert. Sende diese Anfragen daher nach deinem letzten Klick auf „Ausführen“. SendeGET /booksund kopiere die_idvon Dune, Kindred und Emma. SendePUT /books/<Dune's _id>/ratingsmit{ "user": "ada", "stars": 5 }und anschließend erneut mit{ "user": "bo", "stars": 4 }. Bewerte Kindred auf dieselbe Weise mit 3 Sternen vonadaund 3 vonbo. Bewerte Emma einmal mit{ "user": "ada", "stars": 5 }. SendeGET /stats/top: Du erhältst zuerst Dune (avg4.5,count2), dann Kindred (avg3,count2). Emma fehlt, weil sie nur eine Bewertung hat. Wenn Emma an erster Stelle erscheint, wird dein Count-Filter vor der Gruppierung oder gar nicht ausgeführt.
Probier es selbst
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());
// Jeder Fehler wird mit { error: message } beantwortet.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Die :id als ObjectId oder null, nachdem bei einer ungültigen id mit 400 geantwortet wurde.
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');
});
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Datenbanken mit MongoDB
1Dokumente
Ein Buch als DokumentSammlungen und DatenbankenVerbindung zu MongoDB herstellenGespeicherte Dokumente lesenDokumente einfügenEin Dokument findenÜber ObjectId suchenZahlen vergleichenAbfragefilterZurückgegebene Felder auswählenAbfrageergebnisse sortierenDie ersten Ergebnisse behaltenProjektion, Sortierung und LimitÜbe selbstständig: Express-Playground