Menu
CoddyTech

Potoki agregacji

Część sekcji Bazy danych z MongoDB ścieżki Node.js w Coddy. Lekcja 24 z 29.

Lista „najlepiej ocenianych” wymaga średnich, minimalnej liczby ocen, sortowania i ograniczenia wyników. Każdy z tych elementów to etap.

$match zachowuje dokumenty, które spełniają filtr, podobnie jak find. $avg oblicza średnią wartości pola wewnątrz $group. Etapy są wykonywane w kolejności, w jakiej je zapiszesz, a każdy z nich widzi tylko to, co przekazał mu poprzedni etap. $match przed $group filtruje przechowywane dokumenty. $match po $group filtruje grupy według pól utworzonych przez grupę, tak jak w { $match: { orders: { $gte: 3 } } }. Jeden $group może jednocześnie utworzyć kilka pól, na przykład spent: { $sum: '$total' } i orders: { $sum: 1 }.

await orders.aggregate([
    { $match: { status: 'paid' } },
    { $group: { _id: '$customer', spent: { $sum: '$total' } } },
    { $sort: { spent: -1 } },
    { $limit: 5 },
]).toArray();

$sort i $limit przyjmują te same argumenty co w przypadku kursora, ale potok wykonuje je w kolejności, w jakiej je zapiszesz. $limit przed $sort zachowuje dowolne pięć elementów i sortuje tylko je.

challenge icon

Wyzwanie

Łatwy

Ten krok otwiera przygotowany w kursie projekt API półki z książkami, a nie pliki projektu z lekcji Grupowanie za pomocą $group (w tym server.js). Twoja wcześniejsza praca pozostaje w osobnej lekcji. Przed edycją pliku server.js przeczytaj dostarczone pliki. Ta kopia zawiera trasy półki z książkami i podsumowanie liczby gatunków.

Półka potrzebuje listy „najwyżej ocenianych” książek: trzech książek z najwyższą średnią ocen. Aby książka znalazła się na liście, musi mieć co najmniej dwie oceny, więc jedna szczęśliwa ocena 5 gwiazdek nie wystarczy, by trafiła na szczyt.

  1. W pliku server.js dodaj trasę GET /stats/top. Wykonuje ona agregację na kolekcji ratings, a nie na books.
  2. Pogrupuj oceny według bookId. Każda grupa otrzymuje trzy pola: _id, czyli identyfikator książki; avg, czyli średnią jej ocen stars; oraz count, czyli liczbę jej ocen. Jeden etap $group może obliczyć zarówno avg, jak i count.
  3. Pozostaw tylko te grupy, których count wynosi 2 lub więcej. Ta reguła dotyczy grupy książki, a nie pojedynczej oceny, więc ten etap następuje po grupowaniu.
  4. Posortuj grupy według avg, od najwyższej wartości, i zachowaj pierwsze 3.
  5. Zwróć status 200 i tablicę. Każdy wpis zawiera dokładnie te trzy pola:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id to _id książki, więc klient może pobrać książkę, wysyłając GET /books/ i dodając ten identyfikator. Jeśli mniej niż trzy książki mają po dwie oceny, tablica będzie krótsza. Wypróbuj: każde uruchomienie czyści kolekcję ratings, więc wyślij te żądania po ostatnim uruchomieniu. Wyślij GET /books i skopiuj _id książek Dune, Kindred i Emma. Wyślij PUT /books/<Dune's _id>/ratings z { "user": "ada", "stars": 5 }, a następnie ponownie z { "user": "bo", "stars": 4 }. Oceń Kindred w ten sam sposób: 3 gwiazdki od ada i 3 od bo. Oceń Emmę raz, używając { "user": "ada", "stars": 5 }. Wyślij GET /stats/top: otrzymasz najpierw Dune (avg 4.5, count 2), a następnie Kindred (avg 3, count 2). Emma nie pojawi się na liście, ponieważ ma tylko jedną ocenę. Jeśli Emma znajdzie się na pierwszym miejscu, filtr count działa przed grupowaniem albo wcale.

Spróbuj swoich sił

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());

// Każdy błąd zwraca odpowiedź { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Wartość :id jako ObjectId albo null po zwróceniu odpowiedzi 400 dla nieprawidłowego id.
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 iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Bazy danych z MongoDB

Poćwicz samodzielnie: Edytor online Express