Java JDBC Statement
Esegui SQL in Java con l'interfaccia Statement — quando usarla rispetto a PreparedStatement.
Un Statement invia una stringa SQL completa e fissa al database. Lo si crea da una Connection, gli si passa l'SQL e si ottiene in cambio un ResultSet (per le query) o un conteggio degli aggiornamenti (per le modifiche). È il più semplice dei tre tipi di statement JDBC — e quello che dovresti usare meno, perché qualsiasi dato variabile nell'SQL deve essere concatenato a mano, ed è così che nascono i bug di SQL injection.
Questo capitolo spiega come creare ed eseguire uno Statement, i tre metodi di esecuzione e quando usarne ciascuno, come regolare il cursore e leggere le chiavi generate, e — soprattutto — quando fermarsi e usare invece un PreparedStatement. Se sei nuovo a JDBC, inizia con l'introduzione a JDBC.
Creazione ed esecuzione
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");
}I tre metodi di esecuzione
| Metodo | Usare per | Restituisce |
|---|---|---|
executeQuery(sql) | SELECT | un ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int righe interessate |
execute(sql) | risultati sconosciuti / multipli | boolean (true se un ResultSet) |
Usa executeQuery e executeUpdate ogni volta che sai in anticipo quale tipo di statement stai eseguendo — restituiscono direttamente il tipo corretto. Ricorri a execute solo in strumenti generici (una console SQL, un runner di migrazioni) dove l'SQL non è noto fino al runtime; dopo di esso chiami getResultSet() o getUpdateCount() per recuperare il risultato.
executeUpdate restituisce 0 per DDL come CREATE TABLE, e per INSERT/UPDATE/DELETE restituisce il numero di righe interessate — utile per confermare che un aggiornamento abbia effettivamente corrisposto a una riga.
Regolazione del cursore e chiavi generate
Quando crei uno statement puoi scegliere come si comporta il cursore risultante con createStatement(resultSetType, resultSetConcurrency) — ad esempio TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (il default e il più veloce). Chiedi TYPE_SCROLL_INSENSITIVE solo quando hai bisogno di scorrere indietro attraverso il risultato, e CONCUR_UPDATABLE solo quando intendi modificare le righe tramite il cursore; entrambi costano di più.
Per gli inserimenti, passa Statement.RETURN_GENERATED_KEYS e poi leggi la chiave primaria assegnata dal database con 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);
}
}
}Senza quel flag la chiamata ha successo ma getGeneratedKeys() restituisce un ResultSet vuoto, quindi non puoi recuperare il nuovo id.
Quando NON usare Statement
Nel momento in cui qualsiasi parte dell'SQL proviene da una variabile — un nome utente, un id, un termine di ricerca — fermati e usa invece PreparedStatement. Concatenare valori in una stringa Statement è insicuro: un valore contenente un apice può cambiare il significato del comando. PreparedStatement memorizza anche il piano di analisi, quindi una query eseguita in un ciclo è più veloce come prepared statement. Il prossimo capitolo è dedicato a quell'alternativa sicura; per le stored procedure, vedi CallableStatement.
Riserva Statement per SQL fisso e privo di valori: configurazione dello schema (CREATE TABLE …), DDL una tantum, o un SELECT hard-coded senza parti variabili.
Non chiudere mai uno Statement mentre hai ancora bisogno del suo ResultSet — chiudere lo statement chiude qualsiasi risultato prodotto. Usa un blocco try-with-resources, come negli esempi sopra, in modo che ciascuno venga chiuso nell'ordine corretto.
Un esempio pratico: le costanti del cursore e la trappola dell'injection
Questo programma stampa le costanti di tuning ResultSet/Statement che passi quando crei uno statement, poi dimostra concretamente perché l'SQL costruito con stringhe è pericoloso — mostrando cosa fa un valore malevolo al testo del comando.
Cosa ricavare dall'esecuzione:
- Le costanti del cursore sono semplici
intche passi acreateStatement.TYPE_FORWARD_ONLY+CONCUR_READ_ONLYè il default e il più economico; chiedi un cursore scrollabile o aggiornabile solo quando ne hai davvero bisogno. Statement.RETURN_GENERATED_KEYSè il flag che consente a unINSERTdi restituirti il nuovo id auto-increment tramitegetGeneratedKeys()— senza di esso non puoi recuperare la chiave assegnata dal database.- La prima query concatenata è innocua perché
Acmenon ha metacaratteri SQL. Questo è esattamente il motivo per cui la concatenazione di stringhe sembra funzionare nei test — e poi si rompe in produzione con input del mondo reale. - Il secondo valore contiene un apice e un punto e virgola, quindi il singolo
SELECTprevisto diventa unSELECTseguito da unDROP TABLE. Il dato è uscito dai suoi apici ed è diventato SQL eseguibile — la definizione classica di injection. - La soluzione non è mai "fare l'escape degli apici tu stesso". È smettere di costruire SQL dai valori del tutto e lasciare che
PreparedStatementinvii il template e i dati separatamente — argomento del prossimo capitolo.