Java JDBC Statement
Exécutez du SQL en Java avec l'interface Statement — quand l'utiliser plutôt que PreparedStatement.
Un Statement envoie une chaîne SQL complète et fixe à la base de données. Vous en créez un à partir d'une Connection, lui passez du SQL et récupérez soit un ResultSet (pour les requêtes), soit un nombre de lignes modifiées (pour les changements). C'est le plus simple des trois types d'instructions JDBC — et celui que vous devriez utiliser le moins, car toute donnée variable dans le SQL doit être concaténée manuellement, ce qui est à l'origine des failles d'injection SQL.
Ce chapitre explique comment créer et exécuter un Statement, les trois méthodes d'exécution et quand chacune s'applique, comment configurer le curseur et lire les clés générées, et — surtout — quand s'arrêter et utiliser un PreparedStatement à la place. Si vous débutez avec JDBC, commencez par l'introduction à JDBC.
Création et exécution
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");
}Les trois méthodes d'exécution
| Méthode | Utilisation | Retourne |
|---|---|---|
executeQuery(sql) | SELECT | un ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int lignes affectées |
execute(sql) | inconnu / plusieurs résultats | boolean (true si un ResultSet) |
Utilisez executeQuery et executeUpdate quand vous savez à l'avance quel type d'instruction vous exécutez — ils retournent directement le bon type. Recourez à execute uniquement dans des outils génériques (une console SQL, un exécuteur de migrations) où le SQL n'est pas connu avant l'exécution ; après cela, appelez getResultSet() ou getUpdateCount() pour récupérer le résultat.
executeUpdate retourne 0 pour les DDL tels que CREATE TABLE, et pour les INSERT/UPDATE/DELETE, il retourne le nombre de lignes affectées — utile pour confirmer qu'une mise à jour a bien correspondu à une ligne.
Configuration du curseur et des clés générées
Lorsque vous créez un statement, vous pouvez choisir le comportement du curseur résultant avec createStatement(resultSetType, resultSetConcurrency) — par exemple TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (valeur par défaut, la plus rapide). Demandez TYPE_SCROLL_INSENSITIVE uniquement si vous avez besoin de parcourir le résultat à rebours, et CONCUR_UPDATABLE uniquement si vous prévoyez de modifier des lignes via le curseur ; les deux coûtent plus cher.
Pour les insertions, passez Statement.RETURN_GENERATED_KEYS puis lisez la clé primaire assignée par la base de données avec 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);
}
}
}Sans ce flag, l'appel réussit mais getGeneratedKeys() retourne un ResultSet vide, vous ne pouvez donc pas récupérer le nouvel id.
Quand NE PAS utiliser Statement
Dès qu'une partie du SQL provient d'une variable — un nom d'utilisateur, un id, un terme de recherche — arrêtez-vous et utilisez PreparedStatement à la place. Concaténer des valeurs dans une chaîne Statement est dangereux : une valeur contenant un guillemet peut modifier la signification de la commande. PreparedStatement met également en cache son plan d'analyse, donc une requête exécutée en boucle est plus rapide sous forme de prepared statement. Le chapitre suivant est consacré à cette alternative sécurisée ; pour les procédures stockées, consultez CallableStatement.
Réservez Statement pour du SQL fixe sans valeur variable : configuration de schéma (CREATE TABLE …), DDL ponctuel, ou un SELECT codé en dur sans partie variable.
Ne fermez jamais un Statement tant que vous avez encore besoin de son ResultSet — fermer le statement ferme tout résultat qu'il a produit. Utilisez un bloc try-with-resources, comme dans les exemples ci-dessus, afin que chacun soit fermé dans le bon ordre.
Exemple complet : les constantes de curseur et le piège de l'injection
Ce programme affiche les constantes de configuration ResultSet/Statement que vous passez lors de la création d'un statement, puis démontre concrètement pourquoi le SQL construit par concaténation est dangereux — en montrant ce qu'une valeur malveillante fait au texte de la commande.
Ce qu'il faut retenir de l'exécution :
- Les constantes de curseur sont de simples
intque vous passez àcreateStatement.TYPE_FORWARD_ONLY+CONCUR_READ_ONLYest la valeur par défaut et la moins coûteuse ; vous ne demandez un curseur défilant ou modifiable que si vous en avez réellement besoin. Statement.RETURN_GENERATED_KEYSest le flag qui permet à unINSERTde vous retourner le nouvel id auto-incrémenté viagetGeneratedKeys()— sans lui, vous ne pouvez pas récupérer la clé assignée par la base de données.- La première requête concaténée est inoffensive car
Acmene contient pas de métacaractères SQL. C'est exactement pourquoi la concaténation de chaînes semble fonctionner lors des tests — puis casse en production avec des données réelles. - La deuxième valeur contient un guillemet et un point-virgule, de sorte que le
SELECTunique prévu devient unSELECTsuivi d'unDROP TABLE. Les données ont échappé à leurs guillemets et sont devenues du SQL exécutable — la définition classique de l'injection. - La solution n'est jamais « échapper les guillemets vous-même ». C'est de cesser complètement de construire du SQL à partir de valeurs et de laisser
PreparedStatementenvoyer le modèle et les données séparément — le sujet du prochain chapitre.