Douglas Legramante

Douglas Legramante

Informática

Técnico em Informática · Back-End II

06-Relacionamentos entre Tabelas (JOIN)

Relacionamentos entre Tabelas (JOIN)


Sumário

  1. O problema do id sozinho
  2. O que é um JOIN
  3. INNER JOIN — sintaxe
  4. Aplicando: produtos + categorias
  5. Atualizando o projeto
  6. Revisão geral: as tabelas restantes
  7. 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.

TabelaSituação antes de hojeO que falta
pedidoslistarTodos e buscarPorId, com JOINcriar, atualizar (status), deletar
clientesnadaCRUD completo
itens_pedidonadaCRUD 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_2 que já vimos no errorHandler de 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 tabelasTipoO que o JOIN resolveria
pedidos + clientes1:NMostrar o nome do cliente em vez de só cliente_id (feito hoje)
itens_pedido + produtos + pedidosN: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).