06-Relacionamentos entre Tabelas (JOIN)
Materiais desta aula
Relacionamentos entre Tabelas (JOIN)
Sumário
- O problema do id sozinho
- O que é um JOIN
- INNER JOIN — sintaxe
- Aplicando: produtos + categorias
- Atualizando o projeto
- Revisão geral: as tabelas restantes
- Outros relacionamentos da StockAPI
1. O problema do id sozinho
Hoje a API devolve um produto assim:
{
"id": 1,
"nome": "Caderno",
"preco": 15.9,
"categoria_id": 3
}
categoria_id: 3 é ótimo para o banco de dados, mas péssimo para quem está usando a API — ninguém sabe de cabeça qual categoria é a de número 3. Sem um JOIN, resolver isso exigiria uma segunda consulta (buscar a categoria pelo id) e juntar as duas respostas manualmente.
2. O que é um JOIN
Um JOIN é uma instrução SQL que combina linhas de duas (ou mais) tabelas em uma única consulta, usando a chave estrangeira como ponte entre elas.
É o banco de dados fazendo, em uma consulta só, o que antes exigiria duas consultas separadas.
3. INNER JOIN — sintaxe
SELECT colunas
FROM tabela_a
INNER JOIN tabela_b
ON tabela_a.coluna_fk = tabela_b.coluna_pk;
O ON diz como as tabelas se conectam — geralmente a chave estrangeira de um lado, apontando para a chave primária do outro.
4. Aplicando: produtos + categorias
SELECT produtos.id, produtos.nome, produtos.preco,
categorias.nome AS categoria
FROM produtos
INNER JOIN categorias
ON produtos.categoria_id = categorias.id;
AS categoria renomeia a coluna nome (que existiria duas vezes no resultado — uma de cada tabela) para ficar claro qual é qual.
O resultado
{
"id": 1,
"nome": "Caderno",
"preco": 15.9,
"categoria": "Papelaria"
}
5. Atualizando o projeto
A boa notícia: só o service muda. Controller e rota continuam exatamente iguais — eles não sabem (nem precisam saber) que a query mudou por dentro.
services/produtosService.js — listarTodos
export async function listarTodos() {
const [rows] = await pool.query(
`SELECT produtos.id, produtos.nome, produtos.preco,
categorias.nome AS categoria
FROM produtos
INNER JOIN categorias
ON produtos.categoria_id = categorias.id`
);
return rows;
}
services/produtosService.js — buscarPorId
export async function buscarPorId(id) {
const [rows] = await pool.query(
`SELECT produtos.id, produtos.nome, produtos.preco,
categorias.nome AS categoria
FROM produtos
INNER JOIN categorias
ON produtos.categoria_id = categorias.id
WHERE produtos.id = ?`,
[id]
);
return rows[0];
}
Essa é uma boa demonstração de por que vale a pena separar o projeto em camadas: uma mudança na forma de consultar o banco fica isolada dentro do service, sem precisar tocar em controller, rota ou middleware de validação.
6. Revisão geral: as tabelas restantes
Esta é a última aula do bimestre antes da Avaliação 2 — por isso a atividade de hoje inclui uma revisão geral, aplicando o mesmo padrão de CRUD (já visto em produtos e categorias) nas tabelas que ainda faltavam: completar pedidos, e construir clientes e itens_pedido do zero.
| Tabela | Situação antes de hoje | O que falta |
|---|---|---|
| pedidos | listarTodos e buscarPorId, com JOIN | criar, atualizar (status), deletar |
| clientes | nada | CRUD completo |
| itens_pedido | nada | CRUD completo (tabela com 2 FKs) |
Completando services/pedidosService.js
export async function criar(pedido) {
const { cliente_id, status } = pedido;
const [r] = await pool.query(
'INSERT INTO pedidos (cliente_id, status) VALUES (?, ?)',
[cliente_id, status || 'pendente']
);
return r.insertId;
}
export async function atualizarStatus(id, status) {
const [r] = await pool.query(
'UPDATE pedidos SET status = ? WHERE id = ?',
[status, id]
);
return r.affectedRows;
}
export async function deletar(id) {
const [r] = await pool.query('DELETE FROM pedidos WHERE id = ?', [id]);
return r.affectedRows;
}
O restante (controller com next(erro), checagem de existência antes de atualizar, rotas com POST/PUT/DELETE) segue exatamente o padrão já usado em produtosController.js.
clientes — CRUD completo, sem chave estrangeira
clientes segue o padrão mais simples, igual a categorias: nenhuma FK, então o middlewares/validarCliente.js só precisa confirmar que nome foi enviado.
itens_pedido — CRUD completo, com duas chaves estrangeiras
CREATE TABLE itens_pedido (
id INT AUTO_INCREMENT PRIMARY KEY,
pedido_id INT,
produto_id INT,
quantidade INT,
preco_unitario DECIMAL(10,2),
FOREIGN KEY (pedido_id) REFERENCES pedidos(id),
FOREIGN KEY (produto_id) REFERENCES produtos(id)
);
Cada linha representa "esse produto está nesse pedido, nessa quantidade, a esse preço". O middlewares/validarItemPedido.js confere pedido_id, produto_id (obrigatórios) e que quantidade/preco_unitario sejam números maiores que zero.
O que acontece ao deletar um pedido com itens vinculados? O MySQL recusa a operação — o erro é o "espelho" do
ER_NO_REFERENCED_ROW_2que já vimos noerrorHandlerde produtos (categoria_id inválido). Lá, o problema era criar uma linha que referencia algo que não existe; aqui, o problema é apagar uma linha que ainda é referenciada por outra (ER_ROW_IS_REFERENCED_2). Mesma lógica de integridade referencial, no sentido contrário.
7. Outros relacionamentos possíveis
| Par de tabelas | Tipo | O que o JOIN resolveria |
|---|---|---|
| pedidos + clientes | 1:N | Mostrar o nome do cliente em vez de só cliente_id (feito hoje) |
| itens_pedido + produtos + pedidos | N:N (via itens_pedido) | Um JOIN encadeado (2 JOINs) para montar a "nota" completa de um pedido — fora do escopo de hoje |
Para a próxima aula
Avaliação 2 — prática e cumulativa, cobrindo modelagem, MySQL, CRUD e JOIN (Encontros 2 a 6).