Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori6 min de leitura

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:

bash
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"}]})'
{ ok: 1 }

Espere alguns segundos e confirme que o nó virou primário:

bash
docker exec mongo-devclub-rs mongosh --quiet --eval \
  'db.hello().isWritablePrimary'
true

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:

js
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 });
{ 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:

js
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 } },
));
{ 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 }

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:

js
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);
[ { total: 79.8, pedido: 'pedido-1', cliente: 'Ana' } ]

$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:

js
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(),
});
{ transacao: 'confirmada', estoqueLivro2: 4, pedidos: 2 }

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:

js
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,
  });
}
{ erro: 'Estoque insuficiente: nenhuma baixa aplicada', estoqueMantido: 4 }

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.

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:

js
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();
{ atual: 49.9, historico: 39.9 }

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:

bash
docker rm -f mongo-devclub-rs
mongo-devclub-rs
  • mongodb
  • modelagem de dados
  • documentos incorporados
  • referencias
  • transacoes

Perguntas frequentes

Quando devo incorporar dados no MongoDB?
Incorpore quando as partes são normalmente lidas e alteradas juntas, têm crescimento limitado e pertencem ao mesmo agregado. Isso permite recuperar o estado em uma leitura e aproveitar atomicidade por documento.
Quando uma referência é melhor?
Use referência quando o dado tem vida própria, é compartilhado por muitos documentos, muda independentemente ou forma uma relação com crescimento que não cabe bem dentro de um documento.
MongoDB suporta transações?
Sim. MongoDB suporta transações em replica sets e clusters fragmentados. Elas coordenam múltiplos documentos, mas não substituem uma modelagem que mantenha juntos os dados naturalmente atômicos.
Incorporar dados é duplicação errada?
Nem sempre. Um snapshot de título, preço e endereço dentro do pedido é duplicação intencional para preservar histórico. O problema é duplicar um dado mutável sem definir qual cópia é a fonte de verdade.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MongoDB Manual — Data Modeling — mongodb.com
  2. MongoDB Manual — Best Practices for Data Modeling — mongodb.com
  3. MongoDB Manual — Embedded Data Versus References — mongodb.com
  4. MongoDB Manual — Transactions — mongodb.com

Continue por aqui