Acesso a Dados com Java (JDBC, DAO, JPA)¶
Como uma aplicação Java conversa com um banco relacional (MySQL nos exemplos). Esta página segue a progressão: JDBC puro → DAO → pool de conexões → transações → consultas avançadas. A SQL em si está em SQL e a modelagem em Modelagem de Dados; a camada de frameworks (JPA, Spring Data) vem depois e está resumida em Spring.
Visão geral: Java + MySQL¶
flowchart LR
A["Aplicação Java"] --> B["JDBC API<br/>(java.sql)"]
B --> C["Driver<br/>MySQL Connector/J"]
C --> D[("Servidor MySQL")]
Fluxo de dados: a aplicação abre uma conexão, envia comandos SQL, recebe resultados e os
converte em objetos. Ferramentas do ambiente: JDK 17+, MySQL Server, uma IDE, um
cliente (MySQL Workbench), o driver (Connector/J) e um gerenciador de dependências
(Maven). Teste a instalação com java -version e uma conexão simples antes de começar.
JDBC: a ponte entre Java e o banco¶
Definição: JDBC (Java Database Connectivity)
API padrão do Java (pacote java.sql) para acessar bancos relacionais de forma
independente do fornecedor. Fornece interfaces para conexões, comandos e resultados;
cada banco fornece um driver que implementa essas interfaces e traduz as chamadas
para o seu protocolo.
| Componente | Papel |
|---|---|
Driver (MySQL Connector/J, com.mysql.cj.jdbc.Driver) |
Implementa as interfaces JDBC; precisa estar no classpath (dependência Maven) |
Connection |
Sessão ativa com o banco; cria comandos e controla transações (setAutoCommit, commit, rollback); deve ser fechada |
Statement |
Executa SQL fixo (sem parâmetros); simples, mas vulnerável a SQL injection com valores externos |
PreparedStatement |
SQL com parâmetros ?; mais seguro e permite reutilizar o comando |
ResultSet |
Resultado de um SELECT: percorre linhas com next() e lê colunas com getInt, getString… |
SQLException |
Erro de acesso ao banco: getMessage(), getErrorCode() e getSQLState() |
Ciclo JDBC (sempre a mesma ordem): obter a conexão → preparar o comando → definir os
parâmetros → executar (executeQuery ou executeUpdate) → ler o resultado → fechar os
recursos (use try-with-resources).
Conectando¶
Definição: URL JDBC
Endereço do banco: jdbc:mysql://host:porta/banco, por exemplo
jdbc:mysql://localhost:3306/loja (porta padrão do MySQL: 3306).
String url = "jdbc:mysql://localhost:3306/loja";
try (Connection con = DriverManager.getConnection(url, usuario, senha)) {
System.out.println(con.isValid(2) ? "Conectado" : "Conexão inválida");
} catch (SQLException e) {
System.out.println("Erro ao conectar: " + e.getMessage());
}
ConnectionFactory: classe própria que centraliza URL, usuário e senha e devolveDriverManager.getConnection(...)— evita repetir a configuração.- Configuração externa: guarde dados de conexão em um arquivo
.propertiesou em variáveis de ambiente (DB_URL,DB_USUARIO,DB_SENHA), carregados comjava.util.Properties; facilita trocar de ambiente (dev, homologação, produção). - Segurança: não publique senhas no código nem as versione no Git; use um usuário com permissões mínimas e, em produção, restrinja o acesso por IP.
- Erros comuns: servidor MySQL parado, porta incorreta, banco inexistente na URL, senha errada e driver ausente do classpath.
- Boa prática:
try-with-resourcesfecha a conexão automaticamente.
Criando tabelas pensadas para o Java¶
CREATE TABLE produtos (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
preco DECIMAL(10,2) NOT NULL,
estoque INT NOT NULL DEFAULT 0,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
atualizado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
| Tipo Java | Tipo MySQL |
|---|---|
int |
INT |
long |
BIGINT |
String |
VARCHAR(n) |
BigDecimal |
DECIMAL(p, s) |
LocalDate |
DATE |
LocalDateTime |
DATETIME |
AUTO_INCREMENTgera oid; prefiraBIGINTpara muitos registros.- Dinheiro: use
DECIMAL↔BigDecimal, nuncadouble/float. - Regras:
NOT NULL,UNIQUE,DEFAULTeCHECK(ex.:CHECK (idade >= 0)). - Chaves estrangeiras:
FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE RESTRICT ON UPDATE CASCADEevita registros órfãos. - Índices:
CREATE INDEX idx_nome ON produtos (nome)melhoraWHERE/JOIN, com custo em escritas; evite excesso. - Auditoria: colunas
criado_em/atualizado_emcomTIMESTAMPpreenchem-se sozinhas.
CRUD com PreparedStatement¶
Create: INSERT¶
public long salvar(Cliente cliente) throws SQLException {
String sql = "INSERT INTO clientes (nome, email) VALUES (?, ?)";
try (Connection con = DriverManager.getConnection(URL, USUARIO, SENHA);
PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, cliente.getNome()); // índices começam em 1
ps.setString(2, cliente.getEmail());
int linhas = ps.executeUpdate(); // linhas afetadas
if (linhas > 0) {
try (ResultSet rs = ps.getGeneratedKeys()) { // id gerado (AUTO_INCREMENT)
if (rs.next()) return rs.getLong(1);
}
}
}
throw new SQLException("Falha ao inserir cliente");
}
Os ? são placeholders cujo valor é definido depois, com o setXxx adequado ao tipo
(setString, setInt, setDate, setBoolean, setBigDecimal). Nunca concatene dados
do usuário no SQL (ver SQL injection).
Read: SELECT e ResultSet¶
public List<Cliente> listarTodos() throws SQLException {
List<Cliente> clientes = new ArrayList<>();
String sql = "SELECT id, nome, email FROM clientes ORDER BY nome";
try (Connection con = ConnectionFactory.getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) { // avança o cursor; false no fim
Cliente c = new Cliente();
c.setId(rs.getLong("id"));
c.setNome(rs.getString("nome"));
c.setEmail(rs.getString("email"));
clientes.add(c);
}
}
return clientes;
}
executeQuery()serve só para consultas e devolve umResultSet.- Leia colunas por nome ou índice (
rs.getString("nome")); trate valores nulos comrs.wasNull(). - Mapeamento objeto-relacional manual: a classe (
Cliente) representa uma linha da tabela; cada coluna vira um atributo. - Filtros:
WHERE,LIKE("%" + termo + "%"passado como parâmetro),BETWEENeIN, sempre com parâmetros.
Update e Delete¶
String sql = "UPDATE clientes SET nome = ?, email = ? WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, nome); ps.setString(2, email); ps.setInt(3, id);
int linhas = ps.executeUpdate(); // 0 -> nenhum registro foi alterado
executeUpdate()serve paraINSERT,UPDATEeDELETEe devolve o número de linhas afetadas.- Nunca execute
DELETE/UPDATEsemWHERE. - Valide antes: ID válido, campos obrigatórios, se o registro existe e regras de negócio (ex.: não excluir cliente ativo).
CRUD
Create → INSERT; Read → SELECT; Update → UPDATE; Delete →
DELETE. É a base da maioria das aplicações.
O padrão DAO¶
Definição: DAO (Data Access Object)
Padrão que encapsula o código SQL e a comunicação com o banco em uma classe específica, isolando o acesso a dados da lógica de negócio.
public interface ClienteDAO {
void salvar(Cliente cliente) throws SQLException;
Cliente buscarPorId(int id) throws SQLException;
List<Cliente> listarTodos() throws SQLException;
void atualizar(Cliente cliente) throws SQLException;
void excluir(int id) throws SQLException;
}
public class ClienteDAOJdbc implements ClienteDAO { /* JDBC + PreparedStatement */ }
Responsabilidades do DAO: abrir/gerenciar a conexão, executar SQL, mapear resultados para objetos, tratar exceções de persistência e fechar recursos.
Interface (entrada) -> Serviço (regras de negócio) -> DAO (acesso a dados) -> JDBC -> MySQL
com.exemplo.loja
├── model (entidades: Cliente)
├── dao (interfaces e implementações)
├── service (regras de negócio)
├── config (configuração da conexão)
└── app (classe principal)
Benefícios: manutenção mais fácil; testes unitários com mocks (a interface permite substituir a implementação); reuso de código; troca de tecnologia (JDBC → JPA) sem afetar as outras camadas. É a mesma ideia de Repository do Spring e se alinha ao princípio de inversão de dependência (Boas Práticas).
DataSource e pool de conexões¶
Problema: abrir uma conexão física a cada operação é caro (handshake, latência, recursos do servidor) e degrada aplicações com muitas requisições.
Definição: DataSource e pool de conexões
javax.sql.DataSource é a fábrica padronizada de conexões, alternativa ao uso
direto do DriverManager e que permite usar pools. O pool mantém um conjunto de
conexões prontas: getConnection() empresta uma e close() a devolve ao pool
(não a fecha de verdade).
O HikariCP é a biblioteca leve e de alto desempenho mais usada:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/meubanco");
config.setUsername("usuario");
config.setPassword("senha");
config.setMaximumPoolSize(10);
config.setConnectionTimeout(30000); // 30 s
HikariDataSource ds = new HikariDataSource(config);
try (Connection con = ds.getConnection()) { /* usa a conexão */ } // devolve ao pool
ds.close(); // ao encerrar a aplicação
- Dimensionamento: comece com um número moderado (ex.: 5 a 20); mais conexões não significam mais desempenho; considere o número de threads e a capacidade do banco; monitore e ajuste.
- Boas práticas: sempre devolver conexões (try-with-resources), detectar vazamentos,
definir timeouts e encerrar o
DataSourceao finalizar. Conceito geral em Administração e Operação de Banco.
Transações em JDBC¶
Conjunto de operações tratado como uma única unidade de trabalho (tudo ou nada),
controlado pela mesma Connection. Propriedades ACID em
SQL.
- Autocommit: por padrão é
true(cada comando é confirmado sozinho). Para controlar a transação, desative-o comcon.setAutoCommit(false)e restaure o valor ao final. commit()confirma tudo;rollback()desfaz tudo (deve ser chamado no tratamento de exceção).
Connection con = DriverManager.getConnection(url, usuario, senha);
try {
con.setAutoCommit(false);
// débito
try (PreparedStatement debita = con.prepareStatement(
"UPDATE contas SET saldo = saldo - ? WHERE id = ?")) {
debita.setBigDecimal(1, new BigDecimal("100.00"));
debita.setInt(2, 1);
debita.executeUpdate();
}
// crédito
try (PreparedStatement credita = con.prepareStatement(
"UPDATE contas SET saldo = saldo + ? WHERE id = ?")) {
credita.setBigDecimal(1, new BigDecimal("100.00"));
credita.setInt(2, 2);
credita.executeUpdate();
}
con.commit(); // as duas operações juntas
} catch (SQLException e) {
con.rollback(); // desfaz tudo
throw e;
} finally {
con.setAutoCommit(true);
con.close();
}
Savepoint: permite desfazer parte da transação (Savepoint sp = con.setSavepoint(); ... con.rollback(sp);).- Níveis de isolamento:
READ_COMMITTED(evita ler dados não confirmados),REPEATABLE_READ(leitura consistente durante a transação; padrão do MySQL/InnoDB) eSERIALIZABLE(maior nível). Níveis mais altos aumentam a consistência, mas podem reduzir a concorrência — escolha conforme a necessidade. - Boas práticas: transações curtas, sempre a mesma
Connection, tratamento de exceções adequado e restauração doautocommit.
JOINs, filtros, paginação e agregações com JDBC¶
JOINs¶
String sql = "SELECT c.id, c.nome, p.id AS pedido_id, p.data " +
"FROM clientes c INNER JOIN pedidos p ON c.id = p.cliente_id " +
"ORDER BY c.nome";
| Tipo | Retorna | Uso |
|---|---|---|
INNER JOIN |
Só registros com correspondência nas duas tabelas | Relações obrigatórias (clientes que têm pedidos) |
LEFT JOIN |
Todos da esquerda; colunas da direita ficam NULL |
Relatórios (clientes, inclusive sem pedidos) |
RIGHT JOIN |
Todos da direita | Identificar registros órfãos; menos usado |
| N:N | Tabela associativa (itens_pedido) entre pedidos e produtos |
Pedidos com vários produtos |
Use aliases (AS cliente_nome) para nomes claros e para evitar colisões; mapeie o
resultado para um DTO com campos de várias tabelas (ex.: PedidoResumoDTO). Boas
práticas: evite SELECT *, crie índices nas chaves de JOIN, analise o plano (EXPLAIN)
e use PreparedStatement.
Filtros dinâmicos, ORDER BY seguro e paginação¶
StringBuilder sql = new StringBuilder("SELECT * FROM clientes WHERE 1=1");
List<Object> params = new ArrayList<>();
if (filtro.getNome() != null) {
sql.append(" AND nome LIKE ?");
params.add("%" + filtro.getNome() + "%");
}
if (filtro.getIds() != null && !filtro.getIds().isEmpty()) {
sql.append(" AND id IN (")
.append(String.join(",", Collections.nCopies(filtro.getIds().size(), "?")))
.append(")");
params.addAll(filtro.getIds());
}
WHERE 1=1facilita anexar condições opcionais comAND.ORDER BYseguro: nomes de coluna não podem ser parâmetros (?); aceite só colunas de uma lista permitida (whitelist), com padrão definido eASC/DESCvalidados — nunca concatene entrada livre do usuário.
Set<String> permitidas = Set.of("id", "nome", "email", "data_cadastro");
String coluna = permitidas.contains(filtro.getOrdenacao()) ? filtro.getOrdenacao() : "nome";
String sentido = filtro.isAsc() ? "ASC" : "DESC";
String sql = "SELECT * FROM clientes ORDER BY " + coluna + " " + sentido;
- Paginação:
LIMIT ? OFFSET ?(comORDER BYpara resultados estáveis); a página começa em 1:offset = (pagina - 1) * tamanho. Para o total de registros useSELECT COUNT(*)com os mesmos filtros (semLIMIT/OFFSET). - Objeto página: encapsula itens, página atual, tamanho, total de elementos e total de
páginas (
ceil(total / tamanho)).
Agregações e relatórios¶
SELECT categoria, COUNT(*) AS quantidade, SUM(total) AS total_vendas
FROM vendas
GROUP BY categoria
HAVING SUM(total) > 1000
ORDER BY total_vendas DESC;
- Funções:
COUNT,SUM,AVG,MIN,MAX;GROUP BYagrupa;HAVINGfiltra grupos (oWHEREfiltra linhas antes do agrupamento). COALESCE(SUM(total), 0)trocaNULLpor um valor padrão.- Datas: agrupe com
YEAR(),MONTH()eDATE()(ex.: vendas por ano e mês). - Use um DTO de relatório (
CategoriaResumoDTO), tipos adequados (BigDecimalpara valores monetários), aliases claros e índices nas colunas deWHERE/GROUP BY.
Operações em lote (batch)¶
Use quando precisar processar muitos registros (importações, atualizações em massa, migrações): reduz as idas e voltas ao banco.
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO produtos (nome, preco) VALUES (?, ?)")) {
for (Produto p : lista) {
ps.setString(1, p.getNome());
ps.setBigDecimal(2, p.getPreco());
ps.addBatch(); // adiciona ao lote
}
int[] resultados = ps.executeBatch(); // envia tudo de uma vez
conn.commit();
} catch (BatchUpdateException e) {
int[] contagens = e.getUpdateCounts(); // quais comandos funcionaram
conn.rollback();
throw e;
}
executeBatch()devolve umint[]com as linhas afetadas por comando (Statement.SUCCESS_NO_INFO= -2;EXECUTE_FAILED= -3).- Processe em blocos (ex.: 500 registros) para controlar a memória: ao atingir o limite,
chame
executeBatch()e limpe o lote. - Combine com transação (
setAutoCommit(false)+commit/rollback). - No MySQL, o parâmetro
rewriteBatchedStatements=truena URL otimiza inserts em lote.
Stored procedures com CallableStatement¶
Definição: Stored procedure
Rotina SQL armazenada no servidor de banco, que pode receber parâmetros e retornar dados; é executada várias vezes por aplicações como a Java. Centraliza regras no banco e reutiliza código.
DELIMITER //
CREATE PROCEDURE buscar_cliente(IN p_id BIGINT)
BEGIN
SELECT id, nome, email FROM clientes WHERE id = p_id;
END //
DELIMITER ;
CallableStatement cs = con.prepareCall("{call buscar_cliente(?)}");
cs.setLong(1, 1L); // parâmetro IN
ResultSet rs = cs.executeQuery(); // procedimento que devolve um SELECT
CallableStatement total = con.prepareCall("{call total_clientes(?)}");
total.registerOutParameter(1, java.sql.Types.INT); // parâmetro OUT
total.execute();
int n = total.getInt(1);
| Vantagens | Limites |
|---|---|
| Centraliza regras no banco; reuso e padronização; menos tráfego; segurança de acesso | Maior acoplamento ao banco; manutenção e versionamento mais complexos; teste unitário difícil; reduz portabilidade (depende da linguagem SQL do SGBD) |
Para Oracle, ver PL/SQL (procedures e packages).
ORM, JPA e Hibernate¶
Definição: ORM, JPA e Hibernate
ORM (Object-Relational Mapping) mapeia classes para tabelas, objetos para
linhas e atributos para colunas, reduzindo o SQL repetitivo. JPA (Jakarta
Persistence API) é a especificação Java de persistência (anotações e contratos; pacote
jakarta.persistence) — não é uma implementação. Hibernate é a implementação mais
usada da JPA: gera o SQL, controla o ciclo de vida das entidades e usa o JDBC por baixo.
flowchart LR
A["Aplicação Java<br/>(entidades)"] --> B["JPA<br/>(especificação)"]
B --> C["Hibernate<br/>(implementação)"]
C --> D["JDBC<br/>(driver)"]
D --> E[("MySQL")]
Vantagens: menos código JDBC e SQL manual, modelo mais orientado a objetos, maior
produtividade, portabilidade entre bancos e manutenção mais fácil. Cuidados: entender o
SQL que é gerado, evitar consultas excessivas (problema N+1), controlar o carregamento
dos relacionamentos (LAZY/EAGER), usar cache quando necessário e testar/monitorar.
(Versão simplificada via Spring Data em Spring.)
Por que existe o ORM: impedância e armadilhas¶
Definição: impedância objeto-relacional
Impedância objeto-relacional (object-relational impedance mismatch) é o conjunto de diferenças entre o modelo orientado a objetos (herança, referências, coleções, identidade por referência) e o relacional (tabelas, chaves, junções, identidade por chave). Um ORM tenta fazer a ponte, escondendo, por exemplo, a tabela associativa de um relacionamento muitos-para-muitos atrás de uma coleção.
Escrever tudo com JDBC puro exige cuidar sozinho de: compilar consultas (PreparedStatement), try/catch/finally
para não deixar transações e conexões abertas, pool de conexões (DataSource) e decidir onde colocar o SQL
(DAOs, arquivos, anotações). Muito código repetido; por isso surgiram os mapeadores de dados (como o MyBatis, que
mantém o SQL visível) e os ORMs completos (Hibernate/JPA), que geram o SQL a partir do modelo.
Armadilhas de quem usa ORM sem entender o que ele faz:
- Carregamento lazy x eager (
FetchType): o padrão serve na maioria dos casos, mas associações pesadas ou coleções devem ser lazy; associações sempre usadas, eager. Mal escolhido, gera consultas demais (N+1, Performance e o problema N+1) ou carrega o banco inteiro. LazyInitializationException: acessar uma associação lazy depois que oEntityManager/sessão fechou. Soluções: buscar o que é preciso na própria consulta (JOIN FETCH, entity graph), devolver DTOs/projeções da camada de serviço, ou manter a sessão aberta durante a requisição (padrão Open EntityManager in View, derivado do Open Session in View). Esse último é cômodo, mas mantém conexão do pool presa durante a renderização e esconde consultas dentro da view; muitos times preferem desligá-lo (no Spring Boot, a propriedadespring.jpa.open-in-view) e usar DTOs.- Conheça o SQL gerado: ligue o log de SQL em desenvolvimento, e use consultas nativas ou projeções quando o ORM atrapalhar (relatórios).
- Transações e cache de primeiro/segundo nível têm efeitos sutis: leia a documentação da implementação (Hibernate, EclipseLink).
Configuração JPA + MySQL¶
- Dependências Maven:
jakarta.persistence-api,hibernate-coreemysql-connector-j, em versões compatíveis. - Banco e usuário (com UTF-8 e privilégios restritos ao banco da aplicação):
CREATE DATABASE loja DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
CREATE USER 'loja_user'@'localhost' IDENTIFIED BY 'senha_forte';
GRANT ALL PRIVILEGES ON loja.* TO 'loja_user'@'localhost';
FLUSH PRIVILEGES;
META-INF/persistence.xml: define a persistence unit (nome), o provider (Hibernate), as classes de entidade e as propriedades de conexão (jakarta.persistence.jdbc.driver,.url,.user,.password— a senha por variável de ambiente).- Dialeto:
hibernate.dialect=org.hibernate.dialect.MySQLDialect(SQL compatível com o MySQL);hibernate.show_sqleformat_sqlexibem o SQL no console. - DDL automático (
hibernate.hbm2ddl.auto):
| Valor | Efeito |
|---|---|
none |
Não altera o esquema |
validate |
Apenas valida o esquema (seguro para produção) |
update |
Atualiza o esquema (só em desenvolvimento) |
create |
Cria e recria o esquema — apaga os dados |
EntityManagerFactory: criada a partir da persistence unit —Persistence.createEntityManagerFactory("lojaPU"). É cara de criar: crie uma única vez (singleton) e reutilize durante a vida da aplicação.
Mapeamento de entidades¶
@Entity
@Table(name = "clientes", schema = "loja")
public class Cliente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nome", nullable = false, length = 100)
private String nome;
@Column(name = "email", unique = true, nullable = false, length = 100)
private String email;
public Cliente() { } // construtor sem parâmetros acessível (exigido pela JPA)
}
| Anotação | Função |
|---|---|
@Entity |
Marca a classe como entidade persistente; precisa de construtor sem parâmetros acessível; não pode ser final |
@Table |
Define nome da tabela e schema (opcional se coincidir com o nome da classe) |
@Id |
Chave primária (um por entidade; qualquer tipo: Long, Integer, UUID…) |
@GeneratedValue |
Estratégia de geração: IDENTITY aproveita o AUTO_INCREMENT do MySQL; também SEQUENCE, TABLE, AUTO |
@Column |
Personaliza a coluna: name, nullable, unique, length, precision, scale |
Tipos: String→VARCHAR, Integer→INT, Long→BIGINT, BigDecimal→DECIMAL,
LocalDate→DATE, LocalDateTime→DATETIME. Boas práticas: entidades simples (sem regra
de negócio complexa — essa fica na camada de serviço), nomes significativos, equals/
hashCode baseados no identificador, BigDecimal para valores monetários e java.time
para datas.
Mapeamento: detalhes úteis¶
| Recurso | Para quê |
|---|---|
@Transient (ou a palavra transient) |
Atributo que não é persistido (por exemplo, idade calculada a partir da data de nascimento) |
@Basic, @Column |
Mapeamento padrão e ajustes (nome, tamanho, nullable, unique); por padrão, todo atributo é persistente |
@Enumerated(EnumType.STRING) |
Grava o nome do enum (mais seguro que ORDINAL, que quebra se a ordem mudar) |
@Lob |
Textos longos (CLOB) e binários como imagens (BLOB, byte[]); evite carregá-los em listagens (use lazy ou projeções) |
| Chave composta | Duas formas: @EmbeddedId + classe @Embeddable, ou @IdClass; a classe da chave deve ser Serializable e implementar equals/hashCode |
| Dono do relacionamento | Em relacionamentos bidirecionais, o lado sem mappedBy é o dono (grava a chave estrangeira); mantenha os dois lados sincronizados nos métodos de conveniência |
Caches da JPA¶
- Primeiro nível: automático, vive enquanto viver o
EntityManager(em geral, uma requisição); garante que a mesma entidade seja a mesma instância. - Segundo nível: compartilhado entre
EntityManagers (anotação@Cacheable, modoENABLE_SELECTIVE; Ehcache, Infinispan). Útil para dados lidos com frequência e alterados raramente (tabelas de domínio); exige configuração de tamanho e expiração. - Cache de consultas: guarda o resultado de uma consulta por parâmetros (dica
@QueryHint); é invalidado quando entidades da região mudam. - Cuidados: consistência (dado desatualizado), memória e clusters. Meça antes de ligar (N+1 costuma ser o problema real).
Relacionamentos com JPA¶
1:N (um cliente, vários pedidos)¶
@Entity
public class Cliente {
@OneToMany(mappedBy = "cliente", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Pedido> pedidos = new ArrayList<>();
public void addPedido(Pedido pedido) { // mantém os dois lados consistentes
pedidos.add(pedido);
pedido.setCliente(this);
}
}
@Entity
public class Pedido {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cliente_id") // FK na tabela pedidos
private Cliente cliente;
}
@ManyToOneé o lado dono (tem a chave estrangeira, atualiza a FK);@OneToMany (mappedBy = ...)é o lado pai (inverso, sem FK).cascade: propaga operações —PERSIST(salva junto),MERGE,REMOVE(use com cautela),ALL.fetch:LAZY(recomendado) carrega só quando necessário;EAGERcarrega imediatamente — evite buscar dados desnecessários.- Mantenha os dois lados sincronizados com métodos auxiliares (
addPedido,removePedido).
N:N (pedidos e produtos)¶
@ManyToMany
@JoinTable(name = "pedido_produto",
joinColumns = @JoinColumn(name = "pedido_id"),
inverseJoinColumns = @JoinColumn(name = "produto_id"))
private Set<Produto> produtos = new HashSet<>();
@JoinTabledefine a tabela intermediária (joinColumns= FK para o dono;inverseJoinColumns= FK para o outro lado). UseSetpara evitar duplicidades e implementeequals/hashCode.- Quando não usar
@ManyToMany: se o relacionamento tem atributos próprios (quantidade, preço, desconto), crie uma entidade de associação (ItemPedido) com dois@ManyToOne, que modela corretamente o item e permite evoluir as regras. - O
@ManyToManytemfetchLAZYpor padrão; eviteCascadeType.REMOVE(um produto pode estar em outros pedidos) e useJOIN FETCHnas consultas que precisam dos dados.
CRUD com EntityManager¶
Definição: EntityManager
Interface central da JPA: gerencia o ciclo de vida das entidades, executa
operações de CRUD e consultas (JPQL) e é criada pela EntityManagerFactory.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("meuPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Cliente c = new Cliente(); c.setNome("João"); c.setEmail("joao@email.com");
em.persist(c); // CREATE
Cliente lido = em.find(Cliente.class, 1L); // READ (null se não existir)
lido.setNome("João Santos"); // UPDATE: dirty checking
em.remove(lido); // DELETE (entidade gerenciada)
tx.commit();
} catch (Exception e) {
if (tx.isActive()) tx.rollback();
throw e;
} finally {
em.close(); // sempre fechar
}
- Create:
persistdentro de uma transação; após ocommita entidade fica gerenciada. - Read:
find(Classe.class, id)devolve a entidade ounull. - Update: busque a entidade, altere os campos e, no
commit, a JPA detecta a mudança (dirty checking) e gera oUPDATE; para entidades desanexadas usemerge. - Delete: busque (a entidade precisa estar gerenciada),
removeecommit. - Fechamento: feche o
EntityManageremfinally(e aEntityManagerFactoryno encerramento da aplicação); faça rollback em caso de erro.
Estados de uma entidade¶
stateDiagram-v2
[*] --> Transient: new
Transient --> Managed: persist
Managed --> Detached: detach / close
Detached --> Managed: merge
Managed --> Removed: remove
| Estado | Significado |
|---|---|
| Transient (nova) | Criada com new, ainda não gerenciada |
| Managed (gerenciada) | Acompanhada pelo EntityManager: mudanças são sincronizadas com o banco |
| Detached (desanexada) | Fora do contexto de persistência |
| Removed (removida) | Marcada para remoção (excluída no commit) |
JPQL e consultas¶
Definição: JPQL
Java Persistence Query Language: linguagem de consulta da JPA que consulta entidades (nomes de classes e atributos Java), não tabelas — independente do banco.
TypedQuery<Cliente> q = em.createQuery(
"SELECT c FROM Cliente c WHERE c.email = :email ORDER BY c.nome ASC", Cliente.class);
q.setParameter("email", email); // parâmetro nomeado: evita SQL injection
List<Cliente> clientes = q.getResultList();
| Recurso | Uso |
|---|---|
Parâmetros nomeados (:nome) |
Seguros e legíveis |
JOIN / JOIN FETCH |
Navega relacionamentos mapeados; JOIN FETCH carrega a entidade relacionada na mesma consulta e evita N+1 |
| Agregações | COUNT, SUM, AVG, MIN, MAX com GROUP BY (resultado como Object[] ou DTO) |
| Paginação | setFirstResult(offset) e setMaxResults(tamanho); use uma consulta separada de COUNT para o total |
@NamedQuery |
Consulta fixa definida na entidade, reutilizável (createNamedQuery("Cliente.findByEmail", ...)) |
Boas práticas: use TypedQuery (tipagem segura), selecione só os campos necessários
(projeção/DTO), monitore o SQL gerado e evite trazer dados desnecessários.
Transações com JPA¶
- Controle manual:
em.getTransaction()combegin(),commit()erollback()emtry/catch(verifiquetx.isActive()antes do rollback). Todas as operações de um caso de uso (salvar pedido, itens e atualizar estoque) ficam na mesma transação. - Boas práticas: transações curtas; limites definidos na camada de serviço; sem
interação com o usuário dentro da transação; nunca ignorar exceções (nada de
catchvazio);@Transactionaldeclarativo quando possível (Spring); testar cenários de falha e concorrência.
Controle de concorrência: lock otimista x pessimista¶
| Lock otimista | Lock pessimista | |
|---|---|---|
| Como | Campo @Version na entidade; detecta que outra transação alterou o registro e lança OptimisticLockException |
LockModeType.PESSIMISTIC_WRITE (em em.find(..., lockMode) ou na consulta) bloqueia o registro no banco |
| Quando | Colisões pouco frequentes | Alta disputa pelo mesmo registro |
| Custo | Baixo | Pode causar bloqueios e reduzir a concorrência |
@Entity
public class Produto {
@Id private Long id;
@Version private Long versao; // incrementado a cada atualização
private Integer estoque;
}
Performance e o problema N+1¶
Definição: Problema N+1
Ao buscar uma lista de entidades (1 consulta) e depois acessar um relacionamento
LAZY de cada item, o Hibernate dispara uma consulta adicional por item (N
consultas). Resultado: muitas idas ao banco, mais tempo de resposta e sobrecarga —
comum com relacionamentos LAZY percorridos em loop.
List<Pedido> pedidos = pedidoRepository.findAll(); // 1 consulta
for (Pedido p : pedidos) {
System.out.println(p.getCliente().getNome()); // +1 consulta POR pedido
}
Soluções:
| Técnica | Como |
|---|---|
JOIN FETCH |
Carrega a relação na mesma consulta: @Query("SELECT p FROM Pedido p JOIN FETCH p.cliente") |
@EntityGraph |
Plano de carregamento declarativo: @EntityGraph(attributePaths = {"cliente"}) no método do repositório; permite várias associações |
| Projeção/DTO | Buscar só os campos necessários (SELECT new ...PedidoResumoDTO(p.id, p.data, c.nome) FROM Pedido p JOIN p.cliente c): menos dados e memória |
| Paginação | Pageable/Page — nunca carregar tabelas grandes de uma vez; ordenação estável e índices nas colunas de filtro e ordenação |
| Batch fetching | @BatchSize ou hibernate.default_batch_fetch_size reduz o número de consultas quando JOIN FETCH não é viável |
Medir antes de otimizar: ative os logs de SQL do Hibernate (spring.jpa.show-sql,
format_sql, estatísticas com hibernate.generate_statistics) e use EXPLAIN no MySQL
para contar as consultas executadas e comparar o antes/depois. Checklist: evitar
FetchType.EAGER em todos os relacionamentos, inspecionar as consultas geradas em
desenvolvimento, preferir DTOs e paginação, criar índices nas colunas de filtro/ordenação/
chaves estrangeiras, usar cache de 2º nível só com evidência de ganho e monitorar em
produção.
JPA avançado: consultas dinâmicas, projeções e cache¶
Complementa JPQL e consultas e Performance e o problema N+1.
Criteria API¶
Monta consultas por código, com tipagem verificada pelo compilador — útil quando os filtros são dinâmicos (só entram na consulta os que o usuário preencheu).
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Pedido> cq = cb.createQuery(Pedido.class);
Root<Pedido> pedido = cq.from(Pedido.class);
List<Predicate> filtros = new ArrayList<>();
if (status != null) filtros.add(cb.equal(pedido.get("status"), status));
if (minimo != null) filtros.add(cb.ge(pedido.get("total"), minimo));
cq.select(pedido).where(filtros.toArray(new Predicate[0]))
.orderBy(cb.desc(pedido.get("criadoEm")));
List<Pedido> resultado = em.createQuery(cq).getResultList();
Mais verbosa que a JPQL; o metamodelo (Pedido_.status) elimina as strings mágicas.
Spring Data: Specification e query by example¶
public interface PedidoRepository
extends JpaRepository<Pedido, Long>, JpaSpecificationExecutor<Pedido> {}
static Specification<Pedido> comStatus(Status s) {
return (root, query, cb) -> s == null ? null : cb.equal(root.get("status"), s);
}
repo.findAll(comStatus(status).and(totalMinimo(minimo)), PageRequest.of(0, 20));
Cada Specification é um filtro reutilizável e combinável (and, or, not).
Projeções: buscar só o que precisa¶
| Técnica | Quando |
|---|---|
Projeção de interface (interface PedidoResumo { Long getId(); BigDecimal getTotal(); }) |
Leituras simples; o Spring gera o SQL só com essas colunas |
DTO projection (select new ...PedidoDTO(p.id, p.total) from Pedido p ou record) |
Resultado pronto para a API, sem carregar a entidade |
| Entidade completa | Só quando for alterar o objeto |
Evita carregar colunas e relacionamentos desnecessários e reduz o risco de N+1.
JOIN FETCH e @EntityGraph¶
select p from Pedido p join fetch p.itens carrega a coleção na mesma consulta. Cuidados:
join fetch de coleções com paginação faz o Hibernate paginar em memória (aviso
HHH000104) — pagine os identificadores primeiro, ou use @BatchSize; buscar duas coleções
no mesmo fetch gera produto cartesiano (MultipleBagFetchException).
Cache de segundo nível e de consultas¶
- 1º nível: o do
EntityManager(por transação/sessão) — sempre ativo. - 2º nível: compartilhado entre sessões (Ehcache, Infinispan, Redis via provider);
anote a entidade com
@Cache. Bom para dados lidos com frequência e raramente alterados (países, categorias). Perigoso para dados mutáveis em cluster — comprove o ganho antes. - Cache de consultas (query cache): guarda os resultados de uma JPQL; é invalidado a cada escrita na tabela, por isso compensa pouco em tabelas voláteis.
Índices e @Table¶
@Entity
@Table(name = "pedido",
indexes = { @Index(name = "idx_pedido_cliente_status", columnList = "cliente_id, status") },
uniqueConstraints = @UniqueConstraint(columnNames = {"numero"}))
public class Pedido { /* ... */ }
Crie índices para as colunas usadas em WHERE, JOIN e ORDER BY (e compostos respeitando a
ordem das colunas); em produção, o esquema deve ser versionado com Flyway/Liquibase, não
gerado por ddl-auto. Teoria em SQL e Administração de banco.
Auditoria com JPA¶
Rastreia quando (data), quem (usuário) e o que mudou, para diagnóstico, investigação e conformidade (ex.: LGPD). Detalhe do mecanismo em Spring — Auditoria.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class EntidadeAuditavel {
@CreatedDate
@Column(name = "data_criacao", nullable = false, updatable = false)
private LocalDateTime dataCriacao;
@LastModifiedDate
@Column(name = "data_atualizacao", nullable = false)
private LocalDateTime dataAtualizacao;
@CreatedBy @Column(name = "criado_por", updatable = false) private String criadoPor;
@LastModifiedBy @Column(name = "atualizado_por") private String atualizadoPor;
}
- Habilite com
@EnableJpaAuditingna aplicação e@EntityListeners(AuditingEntityListener. class); use uma classe base com@MappedSuperclasspara reaproveitar os campos em várias entidades. - O
AuditorAware<String>devolve o usuário autenticado (integra com o Spring Security) para@CreatedBy/@LastModifiedBy. - Boas práticas: armazene datas em UTC; não confie em timestamps enviados pelo
cliente;
DATETIME(6)no MySQL para precisão de microssegundos; auditoria também de exclusões e mudanças de status; proteja o acesso aos registros de auditoria.
Exclusão lógica (soft delete)¶
Definição: Exclusão lógica
Em vez de remover fisicamente o registro, marca-o como inativo (coluna ativo),
preservando histórico e as relações com outras entidades.
@Transactional
public void excluirLogicamente(Long id, String usuario) {
Cliente c = clienteRepository.findById(id)
.orElseThrow(() -> new EntidadeNaoEncontradaException("Cliente não encontrado"));
c.setAtivo(false);
c.setDataExclusao(LocalDateTime.now());
c.setExcluidoPor(usuario);
clienteRepository.save(c);
}
List<Cliente> findByAtivoTrue(); // consultas consideram apenas ativos
- Colunas típicas:
ativo BOOLEAN,data_exclusao,excluido_por. - Consultas: padrão só com
ativo = true; ofereça métodos que incluam inativos para uso administrativo, evitando expô-los na aplicação. - Restauração: marque
ativo = truee limpe os campos de exclusão, validando conflitos (ex.: e-mail agora em uso por outro registro ativo). - Relacionamentos: defina se os filhos também são inativados (cascata lógica) e documente a regra.
- Índices e unicidade: índice em
ativoe índice único composto (ex.:email+ativo) para permitir reutilizar o valor após a exclusão lógica. - Quando usar: dados importantes, necessidade de histórico/auditoria, relacionamentos com outras entidades, requisitos legais (LGPD). Exclusão física é adequada para dados temporários, logs e cache, ambientes de teste e dados sem relevância histórica; defina a política de retenção (Administração e Operação de Banco).
Testes com JPA e MySQL¶
Teoria geral em Qualidade e testes do Spring em Spring.
| Camada de teste | Ferramenta |
|---|---|
| Pirâmide | Muitos testes unitários, alguns de integração (JPA + MySQL) e poucos end-to-end |
| Unitário | JUnit 5, padrão Arrange-Act-Assert; nomes descritivos; testes independentes |
| Dependências simuladas | Mockito (when().thenReturn(), verify()) para testar o serviço isolado do banco |
| Repositório | @DataJpaTest: carrega só a camada de persistência com banco H2 ou MySQL; cada teste é transacional com rollback automático |
| MySQL real | Testcontainers sobe um MySQL em contêiner (@Container MySQLContainer): mesmo dialeto da produção e testes reprodutíveis, sem o "funciona na minha máquina" |
| Dados de teste | Builders/fábricas (ClienteBuilder) com valores únicos (UUID, timestamp); cada teste limpa o que criou |
Cenários importantes: salvar, buscar (findById, findAll, consultas personalizadas),
atualizar, excluir, paginação e ordenação, restrições (unique, NOT NULL), rollback
(dados não devem persistir em erro) e concorrência. Teste comportamento, não detalhes de
implementação; evite testes frágeis (dependentes de tempo ou de IDs fixos).
Segurança, Docker e do projeto ao deploy¶
Segurança de API com Spring Security + JWT¶
Fluxo: o cliente faz POST /auth/login (e-mail e senha) → a API autentica e emite um
token JWT → o cliente o envia no cabeçalho Authorization: Bearer <token> → a API
valida o token a cada requisição. Detalhes em
Spring — JWT e
Segurança.
- Usuários e papéis: tabelas
usuarios,papeise a associativausuario_papel(N:N), com papéis padronizados (ROLE_USER,ROLE_ADMIN). - Senhas: nunca em texto plano; use
BCryptPasswordEncoder(com salt e custo ajustável). - Autorização:
@PreAuthorize("hasRole('ADMIN')"); 401 = não autenticado, 403 = autenticado sem permissão. SecurityFilterChain: API stateless (SessionCreationPolicy.STATELESS), rotas públicas (/auth/**) e protegidas, filtro JWT na cadeia.- Banco seguro: usuário com permissões mínimas, credenciais em variáveis de ambiente,
TLS em produção, acesso via
PreparedStatement/JPA e banco não exposto à rede pública.ddl-auto: validateem produção. - Boas práticas JWT: expiração curta (ex.: 15 min), segredo de assinatura em variável de ambiente ou gerenciador de segredos (com rotação), HTTPS, rate limiting.
Docker e deploy¶
Dockerfileem múltiplos estágios: build com Maven e imagem final só com o JRE (menor e mais segura), copiando o JAR e executandojava -jar app.jar.- MySQL em contêiner: imagem oficial
mysql:8, com banco, usuário e senhas por variáveis de ambiente; não use orootna aplicação. - Docker Compose: serviços
appedbna mesma rede,depends_one porta 3306 exposta só quando necessário. - Volume: persista os dados do MySQL (
mysql_data:/var/lib/mysql); mantenha uma estratégia de backup (mysqldump). healthcheck:mysqladmin pingno banco e/actuator/healthna aplicação, evitando que a aplicação suba antes do banco.- Migrações e perfis: Flyway na inicialização, perfis
dev/prod, segredos fora da imagem. - Checklist de deploy: testes passando; imagem versionada (tag); variáveis de ambiente configuradas; logs e monitoramento; HTTPS; limites de CPU/memória; backup; plano de rollback; ambientes separados; processo documentado. Teoria em Containers (Docker).
Projeto final: API de vendas¶
Reúne tudo: cadastros de clientes e produtos (CRUD), pedidos e itens, controle de estoque,
autenticação e usuários (JWT), relatórios e paginação/filtros. Arquitetura em camadas
(Controller → Service → Repository → JPA/Hibernate → MySQL), DTOs e @ControllerAdvice.
Fluxo de venda: autenticar (JWT) → selecionar o cliente → adicionar itens → validar
estoque → calcular o total → confirmar em transação
(@Transactional). Qualidade: Bean Validation, tratamento de exceções, testes
(JUnit/Mockito, Testcontainers), Swagger/OpenAPI e revisão de código. Banco: migrações
Flyway, índices, auditoria, soft delete.
A jornada em uma frase
Fundamentos de Java e banco → conexão JDBC e operações básicas → padrão DAO → CRUD → modelagem e SQL → JPA e Hibernate → Spring Boot e Spring Data → API REST → testes e qualidade → segurança (Spring Security + JWT) → Docker e deploy → projeto completo.