Embutir ou Referenciar
Parte da seção Bancos de dados com MongoDB do Journey de Node.js da Coddy. Lição 29 de 29.
As avaliações e as notas pertencem a um livro, mas o melhor lugar para armazenar cada uma é diferente.
Incorporar coloca os dados relacionados dentro do documento, de modo que uma única leitura obtém o livro e suas avaliações. Isso é adequado para dados lidos junto com o documento pai e que permanecem pequenos. Referenciar mantém os dados em sua própria coleção, junto com o id do documento pai, como faz ratings. Isso é adequado para dados que crescem sem limite ou são consultados separadamente, como uma média de todas as notas.
Um documento pode ter no máximo 16 MB, então um array incorporado precisa ter um limite. Dentro de $push, $each lista os valores a adicionar e $slice: -3 mantém os três últimos.
await players.updateOne(
{ name: 'Ada' },
{ $push: { recentScores: { $each: [120], $slice: -3 } } }
);Desafio
FácilAs avaliações são curtas, sempre aparecem com o livro, e um livro mostra apenas as 5 mais recentes. Portanto, mantenha-as dentro do documento do livro, em um array reviews, e nunca deixe esse array crescer além de 5.
- Em
server.js, adicionePOST /books/:id/reviews. O corpo tem este formato:{ "user": "ada", "text": "Loved it" }. UseparseIdpara um ID inválido, que recebe 400. - Com uma única atualização, adicione a avaliação, por exemplo
{ "user": "ada", "text": "Loved it" }, ao final do arrayreviewsdo livro. Na mesma atualização, reduza o array aos últimos 5 itens, para que a avaliação mais antiga seja removida. Um livro sem avaliações recebe um novo array. - Quando nenhum livro tiver esse
_id, responda com 404. - Caso contrário, responda com o status 201 e somente o array
reviewsdo livro, não o livro inteiro, com a avaliação mais antiga primeiro. Após a primeira avaliação, a resposta é:Experimente: Envie[ { "user": "ada", "text": "Loved it" } ]GET /bookse copie o_idde Dune. EnviePOST /books/<Dune's _id>/reviewscom{ "user": "ada", "text": "Loved it" }: você recebe 201 e o array acima. Envie mais cinco avaliações, com os textos"Review 2"a"Review 6". A última resposta contém cinco avaliações, deReview 2aReview 6, eLoved itdesaparece. EnvieGET /books/<Dune's _id>: Dune contém as mesmas cinco avaliações emreviews, e a guia Data as mostra dentro do livro.
Experimente você mesmo
import express from 'express';
import { z } from 'zod';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Todo erro retorna { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Retorna :id como um ObjectId, ou null após responder com 400 se o id for 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;
}
const BookInput = z.object({
title: z.string().min(1),
author: z.string().min(1),
year: z.number().int(),
isbn: z.string().min(1),
genre: z.string().min(1),
});
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 page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const items = await books
.find(filter)
.skip((page - 1) * limit)
.limit(limit)
.toArray();
const total = await books.countDocuments(filter);
res.json({ items, total });
});
app.post('/books', async (req, res) => {
const parsed = BookInput.safeParse(req.body);
if (!parsed.success) {
const issue = parsed.error.issues[0];
return sendError(res, 400, `${issue.path.join('.')}: ${issue.message}`);
}
const book = parsed.data;
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.get('/stats/top', async (req, res) => {
const top = await ratings
.aggregate([
{ $group: { _id: '$bookId', avg: { $avg: '$stars' }, count: { $sum: 1 } } },
{ $match: { count: { $gte: 2 } } },
{ $sort: { avg: -1 } },
{ $limit: 3 },
])
.toArray();
res.json(top);
});
app.get('/books/:id/full', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const [book] = await books
.aggregate([
{ $match: { _id: id } },
{
$lookup: {
from: 'authors',
localField: 'author',
foreignField: 'name',
as: 'authorInfo',
},
},
])
.toArray();
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
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 limite4Estrutura
Um Módulo de Banco de DadosPaginação no Banco de DadosPratique por conta própria: Playground de Express