Ch. 4 — JDBC évolué
Gagner en efficacité avec JDBC : clés générées automatiquement, curseurs de mise à jour, requêtes batch, PreparedStatement et sa boucle de paramètres, image en base — et la méta-base pour décrire la base depuis le code.
Le cours 3.03 donnait le strict nécessaire : se connecter, exécuter, lire, fermer. Ce chapitre montre comment un programme professionnel se sert de JDBC — celui qui gère des clés générées, des mises à jour pendant la lecture, des insertions en masse et des requêtes paramétrées réutilisables. Objectif affiché : gagner en efficacité.
1. JDBC a beaucoup évolué
- JDBC 1.0 — les classes de base (le cours précédent) ;
- JDBC 2.0 — Batch Updates, curseurs scrollables, types
BlobetClob. Depuis JDBC 2, l'API est coupée en deux parties :java.sql(la core API, les classes et interfaces nécessaires) et les extensions J2E —javax.sql— pour les transactions, les pools,DataSource… ; - JDBC 3.0 — support de SQL 99, types booléens, clés générées ;
- JDBC 4.0 — autoload des drivers (plus besoin de
Class.forNameavec un pilote récent !), gestion fine des transactions.
Attention aux drivers : certains sont récents, d'autres pas du tout. Une fonctionnalité décrite ici peut manquer à un pilote ancien.
2. Récupérer les numéros automatiques
La gestion des numéros automatiques — serial en PostgreSQL, auto_increment en H2,
generated as identity dans la norme — est parfois délicate, notamment quand la clé de la
ligne ajoutée doit être réutilisée ailleurs (pour insérer des lignes liées, typiquement).
Depuis JDBC 3, plus besoin de refaire un select pour la retrouver :
executeUpdateest réalisé avec l'optionStatement.RETURN_GENERATED_KEYS;- la récupération se fait avec
getGeneratedKeys(), qui renvoie unResultSetcontenant le nouveau numéro.
import java.sql.*;
public class InsertWithSerial {
public static void main(String args[]) throws Exception {
String url = "jdbc:postgresql://localhost/mabase";
Connection con = DriverManager.getConnection(url, "mathieu", "xxx");
Statement stmt = con.createStatement();
stmt.executeUpdate("insert into client(nom,prenom,age) values('Durand','paul',30) ",
Statement.RETURN_GENERATED_KEYS);
ResultSet numeros = stmt.getGeneratedKeys();
if (numeros.next()) {
int idGenere = numeros.getInt(1); // 1 = première colonne du ResultSet de clés
System.out.println("clé : " + idGenere);
}
con.close();
System.out.println("All is ok !");
}
}3. La mise à jour sur curseur
Pour éviter plusieurs parcours de la table, il est parfois nécessaire de faire des lectures et des modifications en même temps. Depuis JDBC 2.0, un curseur updatable le permet.
Par défaut, les ResultSet sont mono-directionnels et non updatables. On change cela à
la création du Statement :
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
String query = "select * from client";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
int a = rs.getInt("age");
if (a < 40) {
rs.updateInt("age", a + 1); // mettre à jour dans le ResultSet…
rs.updateRow(); // …puis pousser la modification
} else {
rs.deleteRow();
}
}Attention : la présence d'une clé primaire sur la table est impérative pour que la mise à jour sur curseur fonctionne — le SGBD a besoin d'identifier la ligne sans ambiguïté.
4. Les requêtes batch
Depuis JDBC 2.0, l'objet Statement permet de réaliser des traitements par lots de
requêtes (hétérogènes). Il exécute toutes les requêtes en une seule communication au SGBD.
Si tous les ordres sont exécutés, la méthode renvoie un tableau de compteurs (le nombre de
lignes touchées par chaque ordre).
import java.sql.*;
public class InsertBatch {
public static void main(String args[]) throws Exception {
String url = "jdbc:postgresql://localhost/mathieu";
Connection con = DriverManager.getConnection(url, "mathieu", "xxx");
Statement stmt = con.createStatement();
stmt.addBatch("insert into clients values('Durand','paul',30) ");
stmt.addBatch("insert into clients values('Dupont','jean',50) ");
int[] count = stmt.executeBatch(); // une seule communication réseau
con.close();
System.out.println("All is ok !");
}
}5. Les requêtes précompilées
L'objet PreparedStatement permet une pré-compilation de la requête avant
exécution. Il hérite de Statement (donc de ses capacités) et peut être réutilisé plusieurs
fois en changeant juste les paramètres — sans repayer l'analyse lexicale et syntaxique à
chaque appel.
- comme
Statement, il possèdeexecuteQuery()etexecuteUpdate()— mais cette fois sans paramètre : la requête est déjà dans l'objet ; - particulièrement adapté à l'exécution répétée d'une même requête avec des paramètres différents (boucles d'insertion, import de données…) ;
- bien adapté aux types non standard (
Blob,Clob, dates) et aux caractères spéciaux (une apostrophe dans un nom ne casse plus la requête).
Le passage de paramètres se fait avec des ? dans la requête, et des setters :
String query1 = "select libelle from produits where nprod = ? ";
PreparedStatement ps1 = con.prepareStatement(query1);
ps1.setInt(1, 128);
ps1.executeUpdate();
String query2 = "select * from clients where nom = ? ";
PreparedStatement ps2 = con.prepareStatement(query2);
ps2.setString(1, "Durand");
ps2.executeUpdate();Attention : la numérotation commence à 1. Et le nom de la table ne peut pas être passé en paramètre — seules des valeurs le peuvent.
L'utilisation classique, dans une boucle :
import java.sql.*;
public class InsertPS {
public static void main(String args[]) throws Exception {
DS bdd = new DS(); // petite classe maison qui fournit la connexion
Connection con = bdd.getConnection();
// préparation de la requête
String query = "insert into clients values (?,?,?)";
PreparedStatement ps = con.prepareStatement(query);
for (int i = 10; i <= 20; i++) {
ps.setInt(1, i);
ps.setString(2, "Durand" + i);
ps.setString(3, "paul" + i);
System.out.println(ps);
int nb = ps.executeUpdate();
}
con.close();
}
}Notez la bascule : la requête n'est plus dans executeUpdate, elle est dans le
PreparedStatement ; la boucle ne fait que changer les paramètres.
6. Batch et précompilé, ensemble
Les Batch Updates et les requêtes pré-compilées se couplent : PreparedStatement possède
lui aussi addBatch et executeBatch. Le squelette type :
PreparedStatement ps = con.prepareStatement("insert into clients values (?,?,?)");
for (int i = 0; i < 1000; i++) {
ps.setInt(1, i);
// … les autres paramètres …
ps.addBatch();
}
int[] counts = ps.executeBatch(); // 1000 insertions en une seule communication7. Cas pratique : insérer une image
Avec setBinaryStream, on peut envoyer un fichier binaire (image…) dans une colonne
bytea — un bon exemple de « type non standard » que PreparedStatement gère bien :
import java.sql.*;
import java.io.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
public class InsertImg {
public static void main(String[] args) throws Exception {
DS ds = new DS();
Connection con = ds.getConnection();
File file = new File("chat.png");
BufferedImage img = ImageIO.read(file);
FileInputStream input = new FileInputStream(file);
PreparedStatement ps =
con.prepareStatement("insert into images(width,height,image) values(?,?,?)");
ps.setInt(1, img.getWidth());
ps.setInt(2, img.getHeight());
ps.setBinaryStream(3, (InputStream) input, (int) file.length());
ps.executeUpdate();
con.close();
System.out.println("All is ok !");
}
}La table d'accueil : create table images (id serial, width int, height int, image bytea);
8. Le catalogue : la méta-base
La majorité des SGBD gèrent leurs propres objets (tables, colonnes, utilisateurs, droits…) dans un catalogue, aussi appelé méta-base :
- le catalogue contient la table des tables, la table des colonnes, la table des utilisateurs… ;
- chaque ordre du DDL et du DCL effectue des mises à jour du catalogue (par des
INSERT,UPDATE,DELETE— comme n'importe quelle table !) ; - un simple
SELECTsur le catalogue permet d'obtenir toutes les informations sur les tables, les vues, les utilisateurs, les droits… ; - c'est l'outil privilégié du DBA (DataBase Administrator).
JDBC réifie la méta-base
JDBC offre deux classes pour l'interroger depuis le code :
DatabaseMetaData— renseignements sur la base en général (les tables, les vues, les index, les droits…) ;ResultSetMetaData— projection du précédent sur leResultSetobtenu après une requête :ResultSetMetaData rsmd = rs.getMetaData();
Il existe de très nombreuses méthodes dans ces classes (plus de 130 !) :
getColumnCount(), getColumnName(i), getColumnTypeName(i), getPrecision(i),
isReadOnly(i), isAutoIncrement(i), isNullable(i)…
import java.sql.*;
class Meta {
public static void main(String args[]) throws Exception {
DS ds = new DS();
Connection con = ds.getConnection();
Statement stmt = con.createStatement();
String query = "select * from Clients where 1=2"; // astuce : aucune ligne ne remonte
ResultSet rs = stmt.executeQuery(query);
ResultSetMetaData rsmd = rs.getMetaData();
int nbCols = rsmd.getColumnCount();
System.out.println("Cette table contient " + nbCols + " colonnes");
for (int i = 1; i <= nbCols; i++) {
System.out.println("Colonne " + i);
System.out.println("Nom : " + rsmd.getColumnName(i));
System.out.println("Type : " + rsmd.getColumnTypeName(i));
System.out.println("Prec : " + rsmd.getPrecision(i));
System.out.println("Read only : " + rsmd.isReadOnly(i));
System.out.println("Auto num : " + rsmd.isAutoIncrement(i));
System.out.println("Null accepté : " + rsmd.isNullable(i));
}
con.close();
}
}Et côté DatabaseMetaData, la liste des tables d'une base :
DatabaseMetaData dbmd = con.getMetaData();
String catalog = con.getCatalog();
ResultSet rs = dbmd.getTables(catalog, null, null, new String[] { "TABLE", "VIEW" });
// puis rs.getString("TABLE_NAME") sur chaque ligne…C'est le mécanisme qui permet tous les outils génériques : navigateurs de bases, générateurs de code, clients SQL qui affichent la structure d'une table sans la connaître. Des centaines de méthodes existent sur cet objet — la documentation est votre amie.
9. Statement, PreparedStatement, Batch : lequel choisir ?
La conclusion du cours en une phrase par outil :
Statement— pour exécuter des requêtes SQL statiques simples, sans paramètres, codées en dur (select,insert,update,delete) ;PreparedStatement— pour des requêtes paramétrées, précompilées par le SGBD : idéal dans les boucles et pour les données complexes (son, image…) ;- batch — pour exécuter efficacement un grand nombre d'instructions similaires (souvent
avec
PreparedStatement) en une seule communication avec la base, en réduisant ainsi les allers-retours réseau.
A retenir
- les quatre familles d'améliorations : clés générées (
RETURN_GENERATED_KEYS+getGeneratedKeys()), curseurs updatables (clé primaire obligatoire), batch (addBatch/executeBatch), requêtes précompilées (?+ setters, numérotés à partir de 1) ; PreparedStatementhérite deStatementet se combine avec le batch ;- la méta-base décrit la base elle-même ; JDBC l'expose via
DatabaseMetaDataetResultSetMetaData— la base des outils génériques.
cours4-jdbc-evolue.pdf
transp_cours304 — JDBC évolué · 21 diapositives
Chargement du document…
Dernière mise à jour le
Ch. 3 — JDBC : du programme à la base
Connecter un programme Java à une base : le pilote et son classpath, l'URL de connexion propre à chaque SGBD, Statement et ResultSet, la fermeture propre en try-with-resources — et la lecture des messages d'erreur.
Ch. 5 — L'accès concurrent
Transactions, ACID, anomalies de concurrence, niveaux d'isolation, MVCC et verrous explicites : comment PostgreSQL garde la base cohérente quand plusieurs clients travaillent en même temps.