Pular para conteúdo

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 devolve DriverManager.getConnection(...) — evita repetir a configuração.
  • Configuração externa: guarde dados de conexão em um arquivo .properties ou em variáveis de ambiente (DB_URL, DB_USUARIO, DB_SENHA), carregados com java.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-resources fecha 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_INCREMENT gera o id; prefira BIGINT para muitos registros.
  • Dinheiro: use DECIMAL ↔ BigDecimal, nunca double/float.
  • Regras: NOT NULL, UNIQUE, DEFAULT e CHECK (ex.: CHECK (idade >= 0)).
  • Chaves estrangeiras: FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE RESTRICT ON UPDATE CASCADE evita registros órfãos.
  • Índices: CREATE INDEX idx_nome ON produtos (nome) melhora WHERE/JOIN, com custo em escritas; evite excesso.
  • Auditoria: colunas criado_em/atualizado_em com TIMESTAMP preenchem-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 um ResultSet.
  • Leia colunas por nome ou índice (rs.getString("nome")); trate valores nulos com rs.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), BETWEEN e IN, 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 para INSERT, UPDATE e DELETE e devolve o número de linhas afetadas.
  • Nunca execute DELETE/UPDATE sem WHERE.
  • 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 DataSource ao 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 com con.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) e SERIALIZABLE (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 do autocommit.

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=1 facilita anexar condições opcionais com AND.
  • ORDER BY seguro: nomes de coluna não podem ser parâmetros (?); aceite só colunas de uma lista permitida (whitelist), com padrão definido e ASC/DESC validados — 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 ? (com ORDER BY para resultados estáveis); a página começa em 1: offset = (pagina - 1) * tamanho. Para o total de registros use SELECT COUNT(*) com os mesmos filtros (sem LIMIT/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 BY agrupa; HAVING filtra grupos (o WHERE filtra linhas antes do agrupamento).
  • COALESCE(SUM(total), 0) troca NULL por um valor padrão.
  • Datas: agrupe com YEAR(), MONTH() e DATE() (ex.: vendas por ano e mês).
  • Use um DTO de relatório (CategoriaResumoDTO), tipos adequados (BigDecimal para valores monetários), aliases claros e índices nas colunas de WHERE/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 um int[] 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=true na 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 o EntityManager/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 propriedade spring.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

  1. Dependências Maven: jakarta.persistence-api, hibernate-core e mysql-connector-j, em versões compatíveis.
  2. 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;
  1. 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).
  2. Dialeto: hibernate.dialect=org.hibernate.dialect.MySQLDialect (SQL compatível com o MySQL); hibernate.show_sql e format_sql exibem o SQL no console.
  3. 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
  1. 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, modo ENABLE_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; EAGER carrega 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<>();
  • @JoinTable define a tabela intermediária (joinColumns = FK para o dono; inverseJoinColumns = FK para o outro lado). Use Set para evitar duplicidades e implemente equals/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 @ManyToMany tem fetch LAZY por padrão; evite CascadeType.REMOVE (um produto pode estar em outros pedidos) e use JOIN FETCH nas 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: persist dentro de uma transação; após o commit a entidade fica gerenciada.
  • Read: find(Classe.class, id) devolve a entidade ou null.
  • Update: busque a entidade, altere os campos e, no commit, a JPA detecta a mudança (dirty checking) e gera o UPDATE; para entidades desanexadas use merge.
  • Delete: busque (a entidade precisa estar gerenciada), remove e commit.
  • Fechamento: feche o EntityManager em finally (e a EntityManagerFactory no 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() com begin(), commit() e rollback() em try/catch (verifique tx.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 catch vazio); @Transactional declarativo 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 @EnableJpaAuditing na aplicação e @EntityListeners(AuditingEntityListener. class); use uma classe base com @MappedSuperclass para 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 = true e 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 ativo e í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, papeis e a associativa usuario_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: validate em 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

  • Dockerfile em múltiplos estágios: build com Maven e imagem final só com o JRE (menor e mais segura), copiando o JAR e executando java -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 o root na aplicação.
  • Docker Compose: serviços app e db na mesma rede, depends_on e 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 ping no banco e /actuator/health na 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.