Pipelines de agregação
Parte da seção Bancos de dados com MongoDB do Journey de Node.js da Coddy. Lição 24 de 29.
Uma lista dos mais bem avaliados precisa de médias, de um número mínimo de avaliações, de uma ordenação e de um corte. Cada um desses elementos é uma etapa.
$match mantém os documentos que passam por um filtro, como find. $avg calcula a média de um campo dentro de $group. As etapas são executadas na ordem em que você as escreve, e cada uma só vê o que a etapa anterior passou adiante. Um $match antes de $group filtra os documentos armazenados. Um $match depois de $group filtra os grupos pelos campos criados pelo grupo, como em { $match: { orders: { $gte: 3 } } }. Um $group pode criar vários campos de uma só vez, como spent: { $sum: '$total' } e orders: { $sum: 1 }.
await orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$customer', spent: { $sum: '$total' } } },
{ $sort: { spent: -1 } },
{ $limit: 5 },
]).toArray();$sort e $limit recebem os mesmos argumentos que em um cursor, mas um pipeline os executa na ordem em que você os escreve. Um $limit antes de $sort mantém quaisquer cinco documentos e ordena apenas esses.
Desafio
FácilEsta etapa abre o projeto da API de estante preparado para o curso, em vez dos arquivos do seu projeto de Agrupamento com $group (incluindo server.js). Seu trabalho anterior continua na própria lição. Leia os arquivos fornecidos antes de editar server.js. Esta cópia contém as rotas da estante e o resumo da contagem por gênero.
A estante quer uma lista dos “mais bem avaliados”: os três livros com as maiores médias de estrelas. Um livro precisa ter pelo menos duas avaliações para entrar na lista, então uma única avaliação sortuda de 5 estrelas não o coloca no topo.
- Em
server.js, adicione uma rotaGET /stats/top. Ela executa uma agregação na coleçãoratings, não embooks. - Agrupe as avaliações por
bookId. Cada grupo recebe três campos:_id, que é o id do livro;avg, a média dasstars; ecount, o número de avaliações que ele tem. Uma etapa$grouppode calcular tantoavgquantocount. - Mantenha apenas os grupos cujo
countseja 2 ou mais. Essa regra se aplica ao grupo de um livro, não a uma avaliação, então essa etapa vem depois do agrupamento. - Ordene os grupos por
avg, da maior para a menor, e mantenha os 3 primeiros. - Responda com o status 200 e o array. Cada item tem exatamente estes três campos:
[ { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 }, { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 } ]_idé o_iddo livro, para que um cliente possa buscar o livro comGET /books/mais esse id. Quando menos de três livros tiverem duas avaliações, o array será menor. Experimente: cada execução de Run esvaziaratings, então envie estas requisições depois da sua última execução. EnvieGET /bookse copie o_idde Dune, Kindred e Emma. EnviePUT /books/<Dune's _id>/ratingscom{ "user": "ada", "stars": 5 }, depois envie novamente com{ "user": "bo", "stars": 4 }. Avalie Kindred da mesma forma, com 3 estrelas deadae 3 debo. Avalie Emma uma vez, com{ "user": "ada", "stars": 5 }. EnvieGET /stats/top: você receberá Dune (avg4.5,count2) e, em seguida, Kindred (avg3,count2). Emma não aparece, porque tem apenas uma avaliação. Se Emma aparecer primeiro, seu filtro de contagem está sendo executado antes do agrupamento, ou não está sendo executado.
Experimente você mesmo
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());
// Todo erro retorna { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// O :id como ObjectId, ou null após retornar 400 para um id inválido.
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');
});
Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.
Todas as lições de Bancos de dados com MongoDB
1Documentos
Um livro como documentoColeções e bancos de dadosConectando-se ao MongoDBLendo documentos armazenadosInserindo documentosEncontrando um documentoEncontrando por ObjectIdComparando númerosFiltros de consultaEscolhendo os campos retornadosOrdenando os resultados da consultaMantendo os primeiros resultadosProjeção, ordenação e limitePratique por conta própria: Playground de Express