Java JDBC Statement
Execute SQL em Java com a interface Statement — quando usá-la versus PreparedStatement.
Um Statement envia uma string SQL completa e fixa ao banco de dados. Você cria um a partir de uma Connection, passa o SQL e recebe de volta um ResultSet (para consultas) ou uma contagem de atualização (para alterações). É o mais simples dos três tipos de statement JDBC — e o que você deve usar menos, pois qualquer dado variável no SQL precisa ser concatenado manualmente, que é exatamente como surgem bugs de injeção de SQL.
Este capítulo aborda como criar e executar um Statement, os três métodos de execução e quando cada um se aplica, como ajustar o cursor e ler chaves geradas, e — mais importante — quando parar e usar um PreparedStatement. Se você é novo no JDBC, comece com a introdução ao JDBC.
Criando e executando
try (Connection conn = DriverManager.getConnection(url, user, pw);
Statement st = conn.createStatement()) {
// a query → ResultSet
try (ResultSet rs = st.executeQuery("SELECT count(*) FROM product")) {
rs.next();
System.out.println(rs.getInt(1));
}
// a change → update count
int rows = st.executeUpdate("UPDATE product SET active = true WHERE price > 0");
System.out.println(rows + " rows updated");
}Três métodos de execução
| Método | Uso | Retorna |
|---|---|---|
executeQuery(sql) | SELECT | um ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int linhas afetadas |
execute(sql) | desconhecido / múltiplos resultados | boolean (true se um ResultSet) |
Use executeQuery e executeUpdate sempre que souber de antemão qual tipo de statement está executando — eles retornam o tipo correto diretamente. Recorra a execute apenas em ferramentas genéricas (um console SQL, um executor de migrations) onde o SQL não é conhecido até o tempo de execução; após isso, chame getResultSet() ou getUpdateCount() para recuperar o resultado.
executeUpdate retorna 0 para DDL como CREATE TABLE, e para INSERT/UPDATE/DELETE retorna o número de linhas afetadas — útil para confirmar que uma atualização realmente correspondeu a uma linha.
Ajustando o cursor e chaves geradas
Ao criar um statement, você pode escolher como o cursor resultante se comporta com createStatement(resultSetType, resultSetConcurrency) — por exemplo TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (o padrão e mais rápido). Solicite TYPE_SCROLL_INSENSITIVE somente quando precisar percorrer o resultado ao contrário, e CONCUR_UPDATABLE somente quando pretender editar linhas pelo cursor; ambos têm um custo maior.
Para inserções, passe Statement.RETURN_GENERATED_KEYS e leia a chave primária atribuída pelo banco de dados com getGeneratedKeys():
try (Statement st = conn.createStatement()) {
st.executeUpdate(
"INSERT INTO product(name, price) VALUES ('Widget', 9.99)",
Statement.RETURN_GENERATED_KEYS);
try (ResultSet keys = st.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("inserted id = " + newId);
}
}
}Sem esse sinalizador, a chamada tem sucesso, mas getGeneratedKeys() retorna um ResultSet vazio, portanto não é possível recuperar o novo id.
Quando NÃO usar Statement
No momento em que qualquer parte do SQL vier de uma variável — um nome de usuário, um id, um termo de busca — pare e use PreparedStatement. Concatenar valores em uma string de Statement é inseguro: um valor contendo uma aspas pode alterar o significado do comando. PreparedStatement também armazena em cache seu plano de análise, portanto uma consulta executada em loop é mais rápida como prepared statement. O próximo capítulo é dedicado a essa alternativa segura; para procedimentos armazenados, consulte CallableStatement.
Reserve o Statement para SQL fixo, sem valores: configuração de esquema (CREATE TABLE …), DDL pontual, ou um SELECT hard-coded sem parte variável.
Nunca feche um Statement enquanto ainda precisar do seu ResultSet — fechar o statement fecha qualquer resultado que ele produziu. Use um bloco try-with-resources, como nos exemplos acima, para que cada um seja fechado na ordem correta.
Exemplo prático: constantes de cursor e a armadilha de injeção
Este programa imprime as constantes de ajuste de ResultSet/Statement que você passa ao criar um statement e demonstra concretamente por que o SQL construído por string é perigoso — mostrando o que um valor malicioso faz ao texto do comando.
O que extrair da execução:
- As constantes de cursor são simples
ints que você passa paracreateStatement.TYPE_FORWARD_ONLY+CONCUR_READ_ONLYé o padrão e o mais barato; você só solicita um cursor com scroll ou atualizável quando realmente precisa. Statement.RETURN_GENERATED_KEYSé o sinalizador que permite que umINSERTretorne o novo id de auto-incremento por meio degetGeneratedKeys()— sem ele, não é possível recuperar a chave atribuída pelo banco de dados.- A primeira consulta concatenada é inofensiva porque
Acmenão possui metacaracteres SQL. É exatamente por isso que a concatenação de strings parece funcionar nos testes — e então quebra em produção com dados do mundo real. - O segundo valor contém uma aspas e um ponto e vírgula, então o único
SELECTpretendido se torna umSELECTseguido de umDROP TABLE. Os dados escaparam das aspas e se tornaram SQL executável — a definição clássica de injeção. - A correção nunca é "escapar as aspas você mesmo". É parar de construir SQL a partir de valores e deixar que
PreparedStatementenvie o template e os dados separadamente — assunto do próximo capítulo.