Aprenda a criar e interpretar Diagramas Entidade-Relacionamento para modelagem eficiente de banco de dados
O que é um DER? O Diagrama Entidade-Relacionamento é uma ferramenta visual fundamental para modelar a estrutura de um banco de dados. Ele representa as entidades (objetos do mundo real), seus atributos (características) e os relacionamentos entre elas.
Objetos ou conceitos do mundo real (Cliente, Produto, Pedido)
Características das entidades (nome, idade, preço)
Conexões entre entidades (Cliente faz Pedido)
Uma entidade é um objeto ou conceito do mundo real que pode ser identificado de forma única e sobre o qual queremos armazenar informações no banco de dados.
Para um sistema de clínica médica, identifique pelo menos 5 entidades principais e justifique por que cada uma é importante para o sistema.
Dica: Pense em quem usa o sistema, o que é gerenciado, e que informações são importantes para o funcionamento da clínica.
Atributos são as características ou propriedades que descrevem uma entidade. Eles representam os dados que queremos armazenar sobre cada instância da entidade.
Não pode ser dividido em partes menores
Pode ser dividido em sub-atributos
Pode ter múltiplos valores
Calculado a partir de outros atributos
Para a entidade PRODUTO de um e-commerce, classifique cada atributo abaixo:
Relacionamentos são as associações ou conexões entre duas ou mais entidades. Eles representam como as entidades interagem entre si no mundo real.
Uma entidade se relaciona consigo mesma
Relacionamento entre duas entidades
Relacionamento entre três entidades
Para um sistema de BIBLIOTECA, identifique os relacionamentos entre as entidades:
Dica: Pense em verbos que descrevem as ações entre as entidades.
Cardinalidade define quantas instâncias de uma entidade podem se relacionar com quantas instâncias de outra entidade. É fundamental para entender as regras de negócio.
Cada instância de A se relaciona com apenas uma instância de B
Uma instância de A se relaciona com várias instâncias de B
Várias instâncias de A se relacionam com várias instâncias de B
Para um sistema de HOSPITAL, determine a cardinalidade dos relacionamentos:
Dica: Pense: "Quantos X podem se relacionar com quantos Y?"
Chaves são atributos ou conjuntos de atributos que identificam unicamente as instâncias de uma entidade ou estabelecem relacionamentos entre entidades.
Identifica unicamente cada registro na tabela
Referencia a chave primária de outra tabela
Atributo que poderia ser chave primária
Combinação de dois ou mais atributos
Para um sistema de BIBLIOTECA, identifique os tipos de chaves:
Tipos: Primária (PK), Estrangeira (FK), Candidata, Composta
Vamos aplicar os conceitos de DER em exemplos práticos reais, criando diagramas completos e implementando-os com código SQL funcional.
Relacionamentos:
• Um cliente pode fazer vários pedidos (1:N)
• Um pedido pode ter vários produtos (N:N)
• Um produto pertence a uma categoria (N:1)
• Um cliente pode ter vários endereços (1:N)
-- Tabela de categorias
CREATE TABLE categorias (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
descricao TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabela de clientes
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
cpf VARCHAR(11) UNIQUE NOT NULL,
telefone VARCHAR(15),
data_nascimento DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabela de produtos
CREATE TABLE produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(200) NOT NULL,
descricao TEXT,
preco DECIMAL(10,2) NOT NULL,
estoque INTEGER DEFAULT 0,
categoria_id INTEGER REFERENCES categorias(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabela de endereços
CREATE TABLE enderecos (
id SERIAL PRIMARY KEY,
rua VARCHAR(200) NOT NULL,
numero VARCHAR(10),
cidade VARCHAR(100) NOT NULL,
cep VARCHAR(8) NOT NULL,
cliente_id INTEGER REFERENCES clientes(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabela de pedidos
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
data_pedido TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'pendente',
valor_total DECIMAL(10,2) DEFAULT 0,
cliente_id INTEGER REFERENCES clientes(id) ON DELETE CASCADE
);
-- Tabela de itens do pedido (relacionamento N:N)
CREATE TABLE itens_pedido (
pedido_id INTEGER REFERENCES pedidos(id) ON DELETE CASCADE,
produto_id INTEGER REFERENCES produtos(id) ON DELETE CASCADE,
quantidade INTEGER NOT NULL,
preco_unitario DECIMAL(10,2) NOT NULL,
subtotal DECIMAL(10,2) NOT NULL,
PRIMARY KEY (pedido_id, produto_id)
);
-- Buscar livros disponíveis para empréstimo
SELECT l.titulo, a.nome as autor, l.isbn
FROM livros l
JOIN autores a ON l.autor_id = a.id
WHERE l.id NOT IN (
SELECT livro_id FROM emprestimos
WHERE status = 'ativo'
);
-- Listar empréstimos em atraso
SELECT u.nome as usuario, l.titulo, e.data_devolucao_prevista,
CURRENT_DATE - e.data_devolucao_prevista as dias_atraso
FROM emprestimos e
JOIN usuarios u ON e.usuario_id = u.id
JOIN livros l ON e.livro_id = l.id
WHERE e.status = 'ativo'
AND e.data_devolucao_prevista < CURRENT_DATE;
-- Relatório de livros mais emprestados
SELECT l.titulo, a.nome as autor, COUNT(e.id) as total_emprestimos
FROM livros l
JOIN autores a ON l.autor_id = a.id
LEFT JOIN emprestimos e ON l.id = e.livro_id
GROUP BY l.id, l.titulo, a.nome
ORDER BY total_emprestimos DESC
LIMIT 10;
Crie um DER para um sistema de restaurante com: Mesas, Garçons, Clientes, Pedidos, Pratos e Ingredientes. Implemente as tabelas SQL.
Dica: Considere relacionamentos N:N entre Pratos e Ingredientes.
Desenvolva um DER para academia com: Alunos, Instrutores, Treinos, Exercícios e Equipamentos. Crie queries para relatórios.
Dica: Um treino pode ter vários exercícios, e um exercício pode usar vários equipamentos.
Projete um DER para clínica veterinária com: Donos, Animais, Veterinários, Consultas, Tratamentos e Medicamentos.
Dica: Um dono pode ter vários animais, e um animal pode ter várias consultas.
Na programação moderna, especialmente com Node.js, Express e Supabase, os conceitos do DER se materializam em estruturas de dados, APIs e bancos de dados. Vamos ver como cada elemento do DER se traduz em código prático.
Cada entidade vira uma tabela no banco
Conexões viram chaves estrangeiras
Operações viram endpoints REST
-- Tabela USUÁRIO
CREATE TABLE usuarios (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
senha VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Tabela POST
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
conteudo TEXT NOT NULL,
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW()
);
-- Tabela COMENTÁRIO
CREATE TABLE comentarios (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
texto TEXT NOT NULL,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW()
);
const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const app = express();
// Configuração Supabase
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
app.use(express.json());
// ROTAS PARA USUÁRIOS
app.post('/api/usuarios', async (req, res) => {
const { nome, email, senha } = req.body;
const { data, error } = await supabase
.from('usuarios')
.insert([{ nome, email, senha }])
.select();
if (error) return res.status(400).json({ error: error.message });
res.json(data[0]);
});
// ROTAS PARA POSTS
app.get('/api/posts', async (req, res) => {
const { data, error } = await supabase
.from('posts')
.select(`
*,
usuarios(nome),
comentarios(count)
`);
if (error) return res.status(400).json({ error: error.message });
res.json(data);
});
app.post('/api/posts', async (req, res) => {
const { titulo, conteudo, usuario_id } = req.body;
const { data, error } = await supabase
.from('posts')
.insert([{ titulo, conteudo, usuario_id }])
.select();
if (error) return res.status(400).json({ error: error.message });
res.json(data[0]);
});
// ROTAS PARA COMENTÁRIOS
app.post('/api/comentarios', async (req, res) => {
const { texto, post_id, usuario_id } = req.body;
const { data, error } = await supabase
.from('comentarios')
.insert([{ texto, post_id, usuario_id }])
.select();
if (error) return res.status(400).json({ error: error.message });
res.json(data[0]);
});
// Classe para gerenciar pedidos
class PedidoService {
constructor(supabaseClient) {
this.supabase = supabaseClient;
}
// Criar pedido com múltiplos produtos
async criarPedido(clienteId, itens) {
try {
// 1. Criar o pedido
const { data: pedido, error: pedidoError } = await this.supabase
.from('pedidos')
.insert([{
cliente_id: clienteId,
data: new Date().toISOString(),
total: 0 // Será calculado depois
}])
.select()
.single();
if (pedidoError) throw pedidoError;
// 2. Adicionar itens ao pedido
let totalPedido = 0;
const itensParaInserir = [];
for (const item of itens) {
// Buscar preço atual do produto
const { data: produto } = await this.supabase
.from('produtos')
.select('preco')
.eq('id', item.produto_id)
.single();
const subtotal = produto.preco * item.quantidade;
totalPedido += subtotal;
itensParaInserir.push({
pedido_id: pedido.id,
produto_id: item.produto_id,
quantidade: item.quantidade,
preco_unitario: produto.preco
});
}
// 3. Inserir itens do pedido
const { error: itensError } = await this.supabase
.from('itens_pedido')
.insert(itensParaInserir);
if (itensError) throw itensError;
// 4. Atualizar total do pedido
const { error: updateError } = await this.supabase
.from('pedidos')
.update({ total: totalPedido })
.eq('id', pedido.id);
if (updateError) throw updateError;
return { ...pedido, total: totalPedido };
} catch (error) {
throw new Error(`Erro ao criar pedido: ${error.message}`);
}
}
// Buscar pedidos com detalhes
async buscarPedidosCliente(clienteId) {
const { data, error } = await this.supabase
.from('pedidos')
.select(`
*,
clientes(nome, email),
itens_pedido(
quantidade,
preco_unitario,
produtos(nome, categoria_id)
)
`)
.eq('cliente_id', clienteId)
.order('data', { ascending: false });
if (error) throw error;
return data;
}
}
// Rota para buscar livros com autores e disponibilidade
app.get('/api/livros', async (req, res) => {
try {
const { data: livros, error } = await supabase
.from('livros')
.select(`
*,
editoras(nome),
livro_autor(
papel,
autores(nome, nacionalidade)
),
emprestimos(
data_emprestimo,
data_devolucao,
usuarios(nome)
)
`);
if (error) throw error;
// Processar dados para incluir disponibilidade
const livrosComDisponibilidade = livros.map(livro => {
const emprestimosAtivos = livro.emprestimos.filter(
emp => !emp.data_devolucao
);
return {
...livro,
disponivel: emprestimosAtivos.length === 0,
emprestado_para: emprestimosAtivos[0]?.usuarios?.nome || null
};
});
res.json(livrosComDisponibilidade);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Rota para realizar empréstimo
app.post('/api/emprestimos', async (req, res) => {
const { livro_id, usuario_id } = req.body;
try {
// Verificar se livro está disponível
const { data: emprestimosAtivos } = await supabase
.from('emprestimos')
.select('*')
.eq('livro_id', livro_id)
.is('data_devolucao', null);
if (emprestimosAtivos.length > 0) {
return res.status(400).json({
error: 'Livro já está emprestado'
});
}
// Criar empréstimo
const { data, error } = await supabase
.from('emprestimos')
.insert([{
livro_id,
usuario_id,
data_emprestimo: new Date().toISOString()
}])
.select(`
*,
livros(titulo),
usuarios(nome)
`);
if (error) throw error;
res.json(data[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
-- Buscar posts com informações do autor e contagem de comentários
SELECT
p.id,
p.titulo,
p.conteudo,
u.nome AS autor,
u.email AS email_autor,
COUNT(c.id) AS total_comentarios,
p.created_at
FROM posts p
INNER JOIN usuarios u ON p.usuario_id = u.id
LEFT JOIN comentarios c ON p.id = c.post_id
GROUP BY p.id, p.titulo, p.conteudo, u.nome, u.email, p.created_at
ORDER BY p.created_at DESC;
-- Buscar usuários mais ativos (com mais posts)
SELECT
u.nome,
u.email,
COUNT(p.id) AS total_posts,
MAX(p.created_at) AS ultimo_post
FROM usuarios u
LEFT JOIN posts p ON u.id = p.usuario_id
GROUP BY u.id, u.nome, u.email
HAVING COUNT(p.id) > 0
ORDER BY total_posts DESC;
-- Relatório de vendas por categoria
SELECT
cat.nome AS categoria,
COUNT(DISTINCT p.id) AS produtos_vendidos,
SUM(ip.quantidade) AS quantidade_total,
SUM(ip.quantidade * ip.preco_unitario) AS receita_total,
AVG(ip.preco_unitario) AS preco_medio
FROM categorias cat
INNER JOIN produtos p ON cat.id = p.categoria_id
INNER JOIN itens_pedido ip ON p.id = ip.produto_id
INNER JOIN pedidos ped ON ip.pedido_id = ped.id
WHERE ped.data >= '2024-01-01'
GROUP BY cat.id, cat.nome
ORDER BY receita_total DESC;
-- Clientes com maior valor de compras
SELECT
c.nome,
c.email,
COUNT(p.id) AS total_pedidos,
SUM(p.total) AS valor_total_compras,
AVG(p.total) AS ticket_medio,
MAX(p.data) AS ultima_compra
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nome, c.email
HAVING SUM(p.total) > 1000
ORDER BY valor_total_compras DESC;
-- Livros mais populares (mais emprestados)
SELECT
l.titulo,
l.isbn,
COUNT(e.id) AS total_emprestimos,
(
SELECT STRING_AGG(a.nome, ', ')
FROM autores a
INNER JOIN livro_autor la ON a.id = la.autor_id
WHERE la.livro_id = l.id
) AS autores
FROM livros l
LEFT JOIN emprestimos e ON l.id = e.livro_id
GROUP BY l.id, l.titulo, l.isbn
ORDER BY total_emprestimos DESC
LIMIT 10;
-- Usuários com empréstimos em atraso
SELECT
u.nome,
u.email,
l.titulo,
e.data_emprestimo,
CURRENT_DATE - e.data_emprestimo::date AS dias_atraso
FROM usuarios u
INNER JOIN emprestimos e ON u.id = e.usuario_id
INNER JOIN livros l ON e.livro_id = l.id
WHERE e.data_devolucao IS NULL
AND e.data_emprestimo < CURRENT_DATE - INTERVAL '14 days'
ORDER BY dias_atraso DESC;
// supabase/config.js
const { createClient } = require('@supabase/supabase-js');
// Configuração do cliente Supabase
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
// Função para verificar conexão
async function testarConexao() {
try {
const { data, error } = await supabase
.from('usuarios')
.select('count')
.limit(1);
if (error) {
console.error('Erro na conexão:', error.message);
return false;
}
console.log('✅ Conexão com Supabase estabelecida!');
return true;
} catch (err) {
console.error('❌ Falha na conexão:', err.message);
return false;
}
}
module.exports = { supabase, testarConexao };
-- Habilitar RLS em todas as tabelas
ALTER TABLE usuarios ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comentarios ENABLE ROW LEVEL SECURITY;
-- Políticas para a tabela usuarios
CREATE POLICY "Usuários podem ver próprio perfil" ON usuarios
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Usuários podem atualizar próprio perfil" ON usuarios
FOR UPDATE USING (auth.uid() = id);
-- Políticas para a tabela posts
CREATE POLICY "Posts são públicos para leitura" ON posts
FOR SELECT USING (true);
CREATE POLICY "Usuários podem criar posts" ON posts
FOR INSERT WITH CHECK (auth.uid() = usuario_id);
CREATE POLICY "Usuários podem editar próprios posts" ON posts
FOR UPDATE USING (auth.uid() = usuario_id);
-- Políticas para comentários
CREATE POLICY "Comentários são públicos" ON comentarios
FOR SELECT USING (true);
CREATE POLICY "Usuários autenticados podem comentar" ON comentarios
FOR INSERT WITH CHECK (auth.uid() = usuario_id);
-- Função para atualizar timestamp automaticamente
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger para posts
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Função para contar comentários automaticamente
CREATE OR REPLACE FUNCTION atualizar_contador_comentarios()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE posts
SET total_comentarios = total_comentarios + 1
WHERE id = NEW.post_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
UPDATE posts
SET total_comentarios = total_comentarios - 1
WHERE id = OLD.post_id;
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Triggers para comentários
CREATE TRIGGER trigger_comentario_insert
AFTER INSERT ON comentarios
FOR EACH ROW
EXECUTE FUNCTION atualizar_contador_comentarios();
CREATE TRIGGER trigger_comentario_delete
AFTER DELETE ON comentarios
FOR EACH ROW
EXECUTE FUNCTION atualizar_contador_comentarios();
// routes/blog.js - API completa para sistema de blog
const express = require('express');
const { supabase } = require('../config/supabase');
const router = express.Router();
// GET /api/posts - Listar posts com autores e comentários
router.get('/posts', async (req, res) => {
try {
const { data: posts, error } = await supabase
.from('posts')
.select(`
id,
titulo,
conteudo,
created_at,
usuarios:usuario_id (
nome,
email
),
comentarios (
id,
conteudo,
created_at,
usuarios:usuario_id (
nome
)
)
`)
.order('created_at', { ascending: false });
if (error) throw error;
res.json({ success: true, data: posts });
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// POST /api/posts - Criar novo post
router.post('/posts', async (req, res) => {
try {
const { titulo, conteudo, usuario_id } = req.body;
const { data: post, error } = await supabase
.from('posts')
.insert({
titulo,
conteudo,
usuario_id,
total_comentarios: 0
})
.select()
.single();
if (error) throw error;
res.status(201).json({ success: true, data: post });
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;
// middleware/auth.js
const { supabase } = require('../config/supabase');
// Middleware para verificar autenticação
const verificarAuth = async (req, res, next) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
error: 'Token de acesso necessário'
});
}
const { data: { user }, error } = await supabase.auth.getUser(token);
if (error || !user) {
return res.status(401).json({
success: false,
error: 'Token inválido'
});
}
req.user = user;
next();
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Middleware para validar dados de entrada
const validarPost = (req, res, next) => {
const { titulo, conteudo } = req.body;
if (!titulo || titulo.trim().length < 5) {
return res.status(400).json({
success: false,
error: 'Título deve ter pelo menos 5 caracteres'
});
}
if (!conteudo || conteudo.trim().length < 10) {
return res.status(400).json({
success: false,
error: 'Conteúdo deve ter pelo menos 10 caracteres'
});
}
next();
};
module.exports = { verificarAuth, validarPost };
🎯 Resultado: Um DER bem planejado se transforma em um sistema completo e funcional!
Crie um DER completo para um sistema de biblioteca com as seguintes entidades: Livro, Autor, Usuário, Empréstimo, Editora
Defina os atributos de cada entidade, identifique as chaves primárias e estabeleça os relacionamentos entre elas.
Desenvolva um DER para um sistema de e-commerce considerando: Cliente, Produto, Pedido, Categoria, Fornecedor
Considere relacionamentos como: um cliente pode fazer vários pedidos, um pedido pode conter vários produtos, produtos pertencem a categorias.