W3docs

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étodoUsoRetorna
executeQuery(sql)SELECTum ResultSet
executeUpdate(sql)INSERT / UPDATE / DELETE / DDLint linhas afetadas
execute(sql)desconhecido / múltiplos resultadosboolean (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.

Aviso

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.

java— editable, runs on the server

O que extrair da execução:

  • As constantes de cursor são simples ints que você passa para createStatement. 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 um INSERT retorne o novo id de auto-incremento por meio de getGeneratedKeys() — sem ele, não é possível recuperar a chave atribuída pelo banco de dados.
  • A primeira consulta concatenada é inofensiva porque Acme nã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 SELECT pretendido se torna um SELECT seguido de um DROP 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 PreparedStatement envie o template e os dados separadamente — assunto do próximo capítulo.

Prática

Prática
Seu código constrói uma consulta concatenando um valor de formulário web diretamente na string SQL após WHERE owner =. Qual é a correção correta?
Seu código constrói uma consulta concatenando um valor de formulário web diretamente na string SQL após WHERE owner =. Qual é a correção correta?
Was this page helpful?