Cours BUT Info

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é

L'API a beaucoup évolué — attention à l'âge des pilotes. Au programme de ce chapitre : les clés générées (3.0), les curseurs et le batch (2.0), les requêtes précompilées et la méta-base.
  • JDBC 1.0 — les classes de base (le cours précédent) ;
  • JDBC 2.0 — Batch Updates, curseurs scrollables, types Blob et Clob. 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.forName avec 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 :

  • executeUpdate est réalisé avec l'option Statement.RETURN_GENERATED_KEYS ;
  • la récupération se fait avec getGeneratedKeys(), qui renvoie un ResultSet contenant le nouveau numéro.
InsertWithSerial.java
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).

InsertBatch.java
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ède executeQuery() et executeUpdate() — 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 :

InsertPS.java
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 communication

7. 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 :

InsertImg.java
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 SELECT sur 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 le ResultSet obtenu 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)…

Meta.java — décrire une table sans lire une seule ligne
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 ?

Trois outils, trois usages — la conclusion du cours : statique et unique → Statement ; paramétré et répété → PreparedStatement ; en masse → batch. Les trois se combinent.

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) ;
  • PreparedStatement hérite de Statement et se combine avec le batch ;
  • la méta-base décrit la base elle-même ; JDBC l'expose via DatabaseMetaData et ResultSetMetaData — la base des outils génériques.

cours4-jdbc-evolue.pdf

transp_cours304 — JDBC évolué · 21 diapositives

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page