Menu
CoddyTech

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.

challenge icon

Aufgabe

Einfach

Dieser 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.

  1. Füge in server.js eine GET /stats/top-Route hinzu. Sie führt eine Aggregation auf der Sammlung ratings aus, nicht auf books.
  2. Gruppiere die Bewertungen nach bookId. Jede Gruppe erhält drei Felder: _id, die ID des Buches; avg, den Durchschnitt seiner stars; und count, die Anzahl seiner Bewertungen. Eine $group-Stufe kann sowohl avg als auch count berechnen.
  3. Behalte nur die Gruppen, deren count mindestens 2 beträgt. Diese Regel bezieht sich auf die Gruppe eines Buches, nicht auf eine einzelne Bewertung. Daher kommt diese Stufe nach der Gruppierung.
  4. Sortiere die Gruppen nach avg, beginnend mit dem höchsten Wert, und behalte die ersten 3.
  5. 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 }
    ]
    _id ist die _id des Buches, sodass ein Client das Buch mit GET /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“ wird ratings geleert. Sende diese Anfragen daher nach deinem letzten Klick auf „Ausführen“. Sende GET /books und kopiere die _id von Dune, Kindred und Emma. Sende PUT /books/<Dune's _id>/ratings mit { "user": "ada", "stars": 5 } und anschließend erneut mit { "user": "bo", "stars": 4 }. Bewerte Kindred auf dieselbe Weise mit 3 Sternen von ada und 3 von bo. Bewerte Emma einmal mit { "user": "ada", "stars": 5 }. Sende GET /stats/top: Du erhältst zuerst Dune (avg 4.5, count 2), dann Kindred (avg 3, count 2). 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');
});
quiz iconTeste dich selbst

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

Übe selbstständig: Express-Playground