Agrupando com $group
Parte da seção Bancos de dados com MongoDB do Journey de Node.js da Coddy. Lição 21 de 29.
Contar um gênero é uma consulta. Contar todos os gêneros de uma vez exige que o banco de dados organize os livros em grupos.
aggregate executa um pipeline: um array de estágios, cada um passando sua saída para o próximo. O estágio $group cria um documento de saída para cada valor distinto de seu _id. Aqui, _id não é o id de um documento. Ele indica pelo que agrupar, e cada documento de saída contém esse valor em _id. Um $ antes do nome de um campo, '$team', significa “o valor desse campo neste documento”. Sem ele, 'team' é o mesmo texto para todos os documentos, então todos ficam em um único grupo. $sum: 1 soma 1 para cada documento.
await players
.aggregate([{ $group: { _id: '$team', players: { $sum: 1 } } }])
.toArray();
// [{ _id: 'red', players: 4 }, { _id: 'blue', players: 3 }]Um resultado de grupo contém apenas esse _id e os campos calculados por seus acumuladores; os outros campos dos documentos de entrada não são mantidos.
Desafio
FácilEsta etapa abre o projeto de API de estante de livros preparado para o curso, em vez dos arquivos do seu projeto de Contagem de documentos (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 CRUD da estante, o índice exclusivo de ISBN, os filtros de GET /books e as contagens de GET /stats.
Uma página de gêneros mostra quantos livros há em cada gênero, tudo a partir de uma única consulta, incluindo gêneros que ninguém previu.
- Em
server.js, adicione uma rotaGET /stats/genres. - Execute uma agregação em
bookscom um único estágio$group. Ela gera um resultado para cadagenredistinto, com o número de livros que o têm. - Responda com o status 200 e o array retornado pela agregação. Cada entrada contém o gênero em
_ide o número de livros emcount. Dentro de$group,_idnão é um ObjectId: é o valor pelo qual os livros foram agrupados. As entradas podem aparecer em qualquer ordem. Com os livros iniciais, a resposta é:Experimente: Envie[ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]GET /stats/genres: você recebe as duas entradas acima. EnviePOST /bookscom{ "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }: você recebe 201. EnvieGET /stats/genresnovamente: uma terceira entrada aparece,{ "_id": "poetry", "count": 1 }.
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 um 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.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
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