Modelagem MongoDB: quando incorporar ou referenciar dados
Modele clientes, livros e pedidos no MongoDB, decida entre documentos incorporados e referências e execute uma transação com estoque.
Modelar no MongoDB é decidir quais dados ficam no mesmo documento e quais ficam ligados por referência. Nesta lição, você vai desenhar um pedido de livraria, recuperar dados relacionados e confirmar estoque e venda numa transação.
Os nomes técnicos são embedding, ou incorporação, e referencing, ou referência. Não são estilos rivais: um mesmo documento pode incorporar o endereço da compra e referenciar o cadastro atual do cliente.
A caixa do pedido precisa contar uma história completa
Quando a Livraria Horizonte fecha uma encomenda, coloca dentro da caixa uma nota com os títulos, preços, quantidades e endereço daquela venda. O cadastro do cliente continua no sistema e pode receber outro telefone amanhã; a nota da compra não deveria mudar retroativamente.
No documento pedido, a nota é um bom paralelo para dados incorporados. O
clienteId funciona como o número do cadastro consultado separadamente. Título
e preço entram como snapshot: são cópias deliberadas do momento da compra.
O limite aparece rápido. Uma caixa não recebe toda compra futura do cliente, e um documento não deve conter um array que cresce para sempre. MongoDB também impõe limite de 16 MiB por documento BSON. Mesmo antes desse limite, documentos grandes e muito disputados podem causar custo e contenção. A analogia ajuda a achar a fronteira do pedido; ela não autoriza colocar o sistema inteiro dentro dele.
Se objetos, arrays e validação ainda forem novidade, faça primeiro a lição de documentos e coleções.
Prepare um replica set de um nó
Uma escrita em um único documento já é atômica. Como a missão também altera um
livro e cria um pedido, vamos testar uma transação. Transações exigem replica
set; um mongod standalone devolve erro.
Para este laboratório descartável, suba um nó e inicie rs0. A porta publicada
e directConnection=true permitem que o driver local converse com esse único
membro:
docker run -d \
--name mongo-devclub-rs \
-p 127.0.0.1:27017:27017 \
mongo:8.0.28 \
--replSet rs0 --bind_ip_all
docker exec mongo-devclub-rs mongosh --quiet --eval \
'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27017"}]})'Espere alguns segundos e confirme que o nó virou primário:
docker exec mongo-devclub-rs mongosh --quiet --eval \
'db.hello().isWritablePrimary'Esse arranjo prova comportamento transacional, não alta disponibilidade. Um replica set de um nó não tem outro membro para assumir em caso de falha. Em produção, topologia, autenticação, backup e monitoramento exigem planejamento.
Comece pelas perguntas do produto
Antes de escrever JSON, liste as operações principais:
- a confirmação precisa mostrar pedido, itens, preço e entrega numa leitura;
- o perfil precisa mostrar o cadastro atual do cliente;
- o catálogo atualiza preço e estoque independentemente dos pedidos antigos;
- relatórios precisam agrupar vendas por cliente e livro;
- criar uma venda precisa baixar estoque e criar o pedido como unidade.
Essas perguntas produzem uma divisão concreta:
| Dado | Decisão | Motivo | Limite a observar |
|---|---|---|---|
| endereço usado na entrega | incorporar no pedido | histórico e leitura conjunta | repetir é intencional |
| itens, títulos e preços vendidos | incorporar como snapshot | pedido não muda com catálogo | array deve ter tamanho limitado |
| cadastro completo do cliente | referenciar por clienteId |
tem vida e mudanças próprias | leitura conjunta pode pedir $lookup |
| livro atual e estoque | coleção livros |
compartilhado por muitos pedidos | venda cruza dois documentos |
| avaliações sem limite | coleção separada | crescimento independente | consultar página com índice e limite |
Essa tabela não vem do desejo de “normalizar” ou “desnormalizar”. Ela vem das operações que precisam ser rápidas, consistentes e compreensíveis.
Grave as fontes de verdade separadas
Instale mongodb@7.5.0 e crie modelagem.mjs. Conecte com a URI do replica set,
limpe somente o database do laboratório e cadastre cliente e livros:
import { MongoClient } from "mongodb";
const uri = process.env.MONGODB_URI ??
"mongodb://127.0.0.1:27017/?replicaSet=rs0&directConnection=true";
const client = new MongoClient(uri);
await client.connect();
const db = client.db("devclub_modelagem");
await db.dropDatabase();
await db.collection("clientes").insertOne({
_id: "cliente-1",
nome: "Ana",
email: "ana@exemplo.com",
});
await db.collection("livros").insertMany([
{ _id: "livro-1", titulo: "JavaScript do zero", preco: 39.9, estoque: 8 },
{ _id: "livro-2", titulo: "Node.js na prática", preco: 54.5, estoque: 5 },
]);
console.log({ clientes: 1, livros: 2 });O cliente e o livro têm identidades próprias. Atualizar o e-mail de Ana não exige reescrever todos os pedidos. Atualizar o preço atual do livro também não deve alterar o que já foi cobrado.
Incorpore o retrato da venda
Agora crie o pedido. clienteId aponta para a fonte atual do cadastro;
entrega e itens registram o que valeu naquela operação:
const pedidos = db.collection("pedidos");
await pedidos.insertOne({
_id: "pedido-1",
clienteId: "cliente-1",
entrega: {
rua: "Rua das Flores",
numero: 120,
cidade: "Campinas",
},
itens: [
{
livroId: "livro-1",
titulo: "JavaScript do zero",
precoUnitario: 39.9,
quantidade: 2,
},
],
total: 79.8,
status: "pago",
});
console.log(await pedidos.findOne(
{ _id: "pedido-1" },
{ projection: { _id: 0, clienteId: 1, entrega: 1, itens: 1, total: 1 } },
));Uma alteração sobre status, entrega e itens desse mesmo documento tem
atomicidade no nível do documento. Isso é uma vantagem real da incorporação:
partes do agregado não ficam visíveis pela metade.
Mas duplicação exige propriedade. precoUnitario no pedido pertence ao
histórico; preco em livros pertence ao catálogo atual. Se ninguém consegue
dizer qual cópia responde qual pergunta, a duplicação virou ambiguidade.
Resolva uma referência com $lookup
Quando o relatório precisa do nome atual do cliente, o pipeline pode combinar
as coleções. $lookup procura clienteId em _id e devolve um array; $first
pega o primeiro nome esperado:
const pedidoComCliente = await pedidos.aggregate([
{ $match: { _id: "pedido-1" } },
{
$lookup: {
from: "clientes",
localField: "clienteId",
foreignField: "_id",
as: "cliente",
},
},
{
$project: {
_id: 0,
pedido: "$_id",
cliente: { $first: "$cliente.nome" },
total: 1,
},
},
]).toArray();
console.log(pedidoComCliente);$lookup não é proibido, mas também não deve remendar toda tela básica. Se
quase toda leitura exige combinar muitas coleções, reveja se partes lidas juntas
deveriam estar incorporadas. A aula de
aggregation pipeline
explica cada estágio com um relatório maior.
Confirme estoque e pedido na mesma transação
A segunda venda altera dois documentos: baixa livro-2 e cria pedido-2. Uma
falha no meio não pode deixar apenas metade. withTransaction() confirma tudo
quando a função termina ou aborta quando ela lança erro:
const livros = db.collection("livros");
const session = client.startSession();
try {
await session.withTransaction(async () => {
const baixa = await livros.updateOne(
{ _id: "livro-2", estoque: { $gte: 1 } },
{ $inc: { estoque: -1 } },
{ session },
);
if (baixa.modifiedCount !== 1) {
throw new Error("Estoque insuficiente");
}
await pedidos.insertOne({
_id: "pedido-2",
clienteId: "cliente-1",
itens: [{
livroId: "livro-2",
titulo: "Node.js na prática",
precoUnitario: 54.5,
quantidade: 1,
}],
total: 54.5,
status: "pago",
}, { session });
});
} finally {
await session.endSession();
}
console.log({
transacao: "confirmada",
estoqueLivro2: (await livros.findOne({ _id: "livro-2" })).estoque,
pedidos: await pedidos.countDocuments(),
});Todos os métodos dentro da transação recebem { session }. Esquecer a sessão
em uma operação faz essa operação ocorrer fora da transação, mesmo que apareça
dentro da função no código.
Transação também tem custo e condições operacionais. Não use uma transação multidocumento para compensar uma fronteira ruim que poderia caber naturalmente num documento; use-a quando a regra de domínio realmente cruza fontes de verdade.
Faça a operação falhar sem mexer no estoque
Repita a lógica pedindo 99 unidades, mas não crie outro pedido quando o filtro não encontrar estoque. O erro é deliberado e o estado final é a prova:
try {
const baixa = await livros.updateOne(
{ _id: "livro-2", estoque: { $gte: 99 } },
{ $inc: { estoque: -99 } },
);
if (baixa.modifiedCount !== 1) {
throw new Error("Estoque insuficiente: nenhuma baixa aplicada");
}
} catch (erro) {
console.log({
erro: erro.message,
estoqueMantido: (await livros.findOne({ _id: "livro-2" })).estoque,
});
}Esse erro nasce da regra da aplicação, não do servidor. O MongoDB informa
modifiedCount: 0; seu domínio traduz isso para “estoque insuficiente”. Essa
separação deixa claro quem detectou o fato e quem deu significado a ele.
Missão: prove que um snapshot não acompanha o catálogo
Altere o preço atual de livro-1 para 49.9 e consulte o pedido antigo. O
catálogo deve mudar; precoUnitario dentro de pedido-1 deve continuar 39.9:
await livros.updateOne(
{ _id: "livro-1" },
{ $set: { preco: 49.9 } },
);
const atual = await livros.findOne(
{ _id: "livro-1" },
{ projection: { _id: 0, preco: 1 } },
);
const historico = await pedidos.findOne(
{ _id: "pedido-1" },
{ projection: { _id: 0, "itens.precoUnitario": 1 } },
);
console.log({ atual: atual.preco, historico: historico.itens[0].precoUnitario });
await client.close();A missão termina quando os dois valores são diferentes pelo motivo certo. Se
você esperava que o pedido acompanhasse o catálogo, o problema não está no
updateOne(): está na definição do que o pedido representa. Compare esse
desenho com a modelagem relacional no PostgreSQL
para enxergar como a mesma livraria expressa relações nos dois modelos.
Depois do laboratório, remova somente o container criado nesta aula:
docker rm -f mongo-devclub-rsPerguntas frequentes
Quando devo incorporar dados no MongoDB?
Quando uma referência é melhor?
MongoDB suporta transações?
Incorporar dados é duplicação errada?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em MongoDB 8.0.28 em replica set de um nó, MongoDB Node.js Driver 7.5.0, Node 26.3.0 e Docker 29.5.3, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MongoDB Manual — Data Modeling — mongodb.com
- MongoDB Manual — Best Practices for Data Modeling — mongodb.com
- MongoDB Manual — Embedded Data Versus References — mongodb.com
- MongoDB Manual — Transactions — mongodb.com


