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.
Wyzwanie
ŁatwyTen 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.
- W pliku
server.jsdodaj trasęGET /stats/top. Wykonuje ona agregację na kolekcjiratings, a nie nabooks. - Pogrupuj oceny według
bookId. Każda grupa otrzymuje trzy pola:_id, czyli identyfikator książki;avg, czyli średnią jej ocenstars; orazcount, czyli liczbę jej ocen. Jeden etap$groupmoże obliczyć zarównoavg, jak icount. - Pozostaw tylko te grupy, których
countwynosi 2 lub więcej. Ta reguła dotyczy grupy książki, a nie pojedynczej oceny, więc ten etap następuje po grupowaniu. - Posortuj grupy według
avg, od najwyższej wartości, i zachowaj pierwsze 3. - 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 } ]_idto_idksiążki, więc klient może pobrać książkę, wysyłającGET /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ślijGET /booksi skopiuj_idksiążek Dune, Kindred i Emma. WyślijPUT /books/<Dune's _id>/ratingsz{ "user": "ada", "stars": 5 }, a następnie ponownie z{ "user": "bo", "stars": 4 }. Oceń Kindred w ten sam sposób: 3 gwiazdki odadai 3 odbo. Oceń Emmę raz, używając{ "user": "ada", "stars": 5 }. WyślijGET /stats/top: otrzymasz najpierw Dune (avg4.5,count2), a następnie Kindred (avg3,count2). Emma nie pojawi się na liście, ponieważ ma tylko jedną ocenę. Jeśli Emma znajdzie się na pierwszym miejscu, filtrcountdział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');
});
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
1Dokumenty
Książka jako dokumentKolekcje i bazy danychŁączenie z MongoDBOdczytywanie zapisanych dokumentówWstawianie dokumentówWyszukiwanie jednego dokumentuWyszukiwanie według ObjectIdPorównywanie liczbFiltry zapytańWybieranie zwracanych pólSortowanie wyników zapytaniaOgraniczanie do pierwszych wynikówProjekcja, sortowanie i limit2Modyfikowanie dokumentów
Aktualizowanie dokumentówOperatory aktualizacjiPoćwicz samodzielnie: Edytor online Express