Ir para o conteúdo

Cap 12: Persistência com JDBC

🗄️ O que é o JDBC

Armazenar e recuperar dados de forma eficiente é, por si só, um desafio de engenharia considerável — e a maioria das aplicações não deveria reinventá-lo. Persistência é um serviço oferecido por diversos sistemas de banco de dados (Oracle, PostgreSQL, MySQL, SQLite...), e o Java provê uma API padronizada de acesso a esses sistemas: o JDBC (Java Database Connectivity).

O JDBC é definido inteiramente como um conjunto de interfaces (Connection, Statement, PreparedStatement, ResultSet) — cada fabricante de banco de dados fornece um driver que implementa essas interfaces para o seu produto específico. É um exemplo direto e concreto de por que interfaces importam tanto (Capítulo 8): o mesmo código Java, escrito contra as interfaces do JDBC, funciona com PostgreSQL, MySQL ou SQLite — bastando trocar o driver e a connection string.

🚀 Passo a passo

Para os exemplos abaixo, vamos usar SQLite, por não exigir instalação de um servidor de banco separado — o banco inteiro vive em um único arquivo, o que o torna prático para aprendizado (a versão original deste material usava Derby, com o mesmo objetivo; SQLite é hoje a escolha mais comum para esse mesmo propósito didático).

<!-- pom.xml, se estiver usando Maven -->
<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.46.1.0</version>
</dependency>

Abrindo uma conexão

String url = "jdbc:sqlite:agenda.db";

try (Connection conn = DriverManager.getConnection(url)) {
    System.out.println("conectado");
} catch (SQLException e) {
    throw new RuntimeException(e);
}

try-with-resources também vale para conexões de banco

Connection, PreparedStatement e ResultSet implementam AutoCloseable — o mecanismo visto no capítulo anterior. Usar try-with-resources garante que a conexão é fechada mesmo se uma exceção for lançada no meio do caminho, evitando vazamento de conexões — um bug de produção clássico em aplicações que gerenciam esses recursos manualmente.

Criando uma tabela

CREATE TABLE contato (email VARCHAR(30), telefone VARCHAR(30), nome VARCHAR(30));
String sql = """
    CREATE TABLE contato (
        email VARCHAR(30),
        telefone VARCHAR(30),
        nome VARCHAR(30)
    )
    """; // text block (Java 15+) — muito mais legível que concatenar strings com +

try (Connection conn = DriverManager.getConnection(url);
     PreparedStatement stmt = conn.prepareStatement(sql)) {
    stmt.execute();
}

Inserindo dados — e por que usar PreparedStatement

String sql = "INSERT INTO contato (nome, email, telefone) VALUES (?, ?, ?)";

try (Connection conn = DriverManager.getConnection(url);
     PreparedStatement stmt = conn.prepareStatement(sql)) {

    stmt.setString(1, "Sergio Costa");
    stmt.setString(2, "sergio@ufma.br");
    stmt.setString(3, "); DELETE FROM contato; --");
    stmt.execute();

    System.out.println("inserido");
}

Esse terceiro valor não é um erro de exemplo — é a demonstração de por que PreparedStatement existe

Repare que o valor do telefone contém, propositalmente, um trecho que parece SQL malicioso (); DELETE FROM contato; --). Se esse valor fosse concatenado diretamente em uma string SQL — "INSERT INTO contato VALUES('" + nome + "', ...)" — o banco executaria o DELETE, apagando toda a tabela. Essa é a vulnerabilidade clássica de injeção de SQL. Como PreparedStatement trata cada ? como um parâmetro de dado, não como texto SQL a ser interpretado, esse valor é gravado exatamente como uma string literal, inofensiva, no banco — o DELETE nunca é executado. Nunca construa SQL concatenando strings vindas do usuário; use sempre PreparedStatement com parâmetros.

Consultando dados

String sql = "SELECT * FROM contato";

try (Connection conn = DriverManager.getConnection(url);
     PreparedStatement stmt = conn.prepareStatement(sql);
     ResultSet rs = stmt.executeQuery()) {

    while (rs.next()) {
        System.out.println(rs.getString("nome"));
        System.out.println(rs.getString("email"));
    }
}

ResultSet é, mais uma vez, um exemplo do padrão Iterator (Capítulo 9 e 11): rs.next() avança o cursor para a próxima linha e retorna false quando não há mais dados — a mesma ideia de Iterator.hasNext()/next(), adaptada ao contexto de um cursor de banco de dados.

🧱 Um passo além: mapeando linhas para objetos

Juntando JDBC com o que já vimos sobre classes e coleções, é comum escrever um pequeno método que converte cada linha do ResultSet em um objeto de domínio — o embrião manual do que frameworks como JPA/Hibernate automatizam:

public record Contato(String nome, String email, String telefone) {}

public List<Contato> listarContatos(Connection conn) throws SQLException {
    String sql = "SELECT nome, email, telefone FROM contato";
    List<Contato> resultado = new ArrayList<>();

    try (PreparedStatement stmt = conn.prepareStatement(sql);
         ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            resultado.add(new Contato(
                rs.getString("nome"),
                rs.getString("email"),
                rs.getString("telefone")
            ));
        }
    }
    return resultado;
}

JDBC puro × ORMs

Em aplicações profissionais de maior porte, é raro escrever SQL cru manualmente para tudo — frameworks de mapeamento objeto-relacional (ORM), como JPA/Hibernate ou bibliotecas mais leves como jOOQ, automatizam boa parte desse trabalho de conversão. Mas eles são construídos sobre o JDBC, e entender a camada de baixo nível ajuda bastante a diagnosticar problemas de desempenho ou comportamento inesperado quando a camada de abstração "vaza" (o famoso leaky abstraction).

📝 Atividades

  1. Monte um pequeno CRUD completo (create, read, update, delete) para a tabela contato, usando SQLite e PreparedStatement em todas as operações.
  2. Explique, com suas próprias palavras e sem usar jargão, por que PreparedStatement previne injeção de SQL enquanto concatenação de strings não previne.
  3. Escreva o método listarContatos deste capítulo usando try-with-resources para Connection, PreparedStatement e ResultSet simultaneamente (os três podem ser abertos no mesmo bloco try (...), separados por ;).
  4. Pesquise: o que é connection pooling, e por que abrir uma nova Connection a cada operação (como nos exemplos deste capítulo) é considerado ineficiente em uma aplicação real?

🔗 Para saber mais