2. Interface Statement

2.1. Création d'un objet Statement

Un objet de type Statement s'obtient en appelant la méthode createStatement() de l'interface Connection.

Exemple 5. Création d'un objet Statement

Statement smt = connecion.createStatement() ;

On peut créer autant d'objets de ce type par connexion, et tous ces objets peuvent être utilisés de façon concurrente. On peut de plus passer trois paramètres de configuration lors de la création d'une instance de Statement.
  • resultSetType : un entier qui peut prendre les valeurs TYPE_FORWARD_ONLY, TYPE_SCROLL_INSENSITIVE et TYPE_SCROLL_SENSITIVE.
  • resultSetConcurrency : un entier qui peut prendre les valeurs CONCUR_READ_ONLY et CONCUR_UPDATABLE.
  • resultSetHoldability : de même, un entier qui peut prendre les valeurs HOLD_CURSORS_OVER_COMMIT et CLOSE_CURSORS_AT_COMMIT.
Toutes ces valeurs entières sont des entiers publics et statiques de la classe ResultSet. Nous verrons leur signification exacte dans le paragraphe sur les ResultSet. Il est important de fermer un Statement lorsqu'il n'est plus utilisé, par appel à sa méthode close().

2.2. Exécution d'un Statement

Il existe trois méthodes pour exécuter un Statement. Chacune de ces méthodes doit être utilisée dans son propre contexte.
  • executeQuery(String) doit être utilisée si l'exécution du Statement retourne une liste d'objets (cas d'un select) ;
  • executeUpdate(String) doit être utilisée si l'exécution du Statement retourne un nombre d'objets modifiés (cas des requête de création, d'effacement ou de mise à jour) ;
  • execute(String doit être utilisée si le type de Statement exécuté n'est pas connu.
Ces trois méthodes prennent une chaîne de caractères en paramètres. Cette chaîne est la commande SQL qui va être exécutée sur le serveur.

2.2.1. Méthode executeQuery(String)

Cette méthode retourne un objet de type ResultSet. Si la requête SQL passée en paramètre ne correspond pas (par exemple, si elle contient un insert), alors une exception de type SQLException est jetée.

Exemple 6. Méthode Statement.executeQuery(String)

Statement smt = connection.createStatement() ;
ResultSet rs = smt.executeQuery("select nom, prenom from Marins") ;
 // exploitation du resultat
 while (rs.hasNext()) {
   ...
}

2.2.2. Méthode executeUpdate(String)

Toutes les requêtes de mise à jour de la base retournent un entier qui correspond aux nombres d'éléments créés, mis à jour ou effacés. Voyons comment on utilise une telle méthode.

Exemple 7. Méthode Statement.executeUpdate(String)

Statement smt = connection.createStatement() ;
 int count = smt.executeUpdate("insert into Marins(nom, prenom) " + 
                               "values ('Robert', 'Surcouf')") ;
 // exploitation du resultat
 if (count >  0) {
   ...
}

2.2.3. Méthode execute(String)

Cette méthode doit être utilisée si l'on ne connait pas la nature de la requête SQL qui va être effectivement exécutée. Elle retourne true si l'objet retourné est de type ResultSet et false s'il s'agit d'un entier. On peut ensuite obtenir ces objets en appelant les méthodes getResultSet() et getUpdateCount(). De plus, si l'exécution de cette requête a généré plusieurs résultats, la méthode getMoreResult() doit être appelée. Voyons l'utilisation de toute ceci.

Exemple 8. Méthode Statement.execute(String)

Statement stmt = conn.createStatement() ;
 boolean returnedValue = smt.execute(sql) ;  // on ne connaît pas la nature 
                                            // de cette requête
ResultSet rs ;
 int count ;

 do {
    if (returnedValue) {  // le résultat est un result set

      rs = stmt.getResultSet();
       // exploitation du result set
      ...

   }  else {  // le résultat est un entier

      count = stmt.getUpdateCount() ; 
       if (count == -1) {

          // une valeur -1 indique qu'il n'y a plus de résultat à exploiter
          break ;  // sortie du while

      }  else {

          // exploitation du count
         ...

      }
   }

   returnedValue = smt.getMoreResult() ;
}  while (true) ;

Notons que par défaut, l'ouverture d'un nouveau resultSet par appel à la méthode getResultSet() ferme automatiquement l'ancien. Ce comportement peut être changé, si le pilote utilisé le permet, comme nous le verrons dans le paragraphe sur les ResultSet.

2.3. Fermeture d'un objet Statement

Tous les objets de type Statement doivent être fermés par un appel à la méthode close() une fois qu'ils ne sont plus utilisés. La fermeture d'un Statement entraîne automatiquement la fermeture de tous les objets ResultSet qui ont été ouverts sur ce Statement. Notons que même si la fermture d'une connexion (par appel à sa méthode close()) ferme automatiquement tous les Statement ouverts dessus, il est recommandé de le faire à la main. De même que pour la fermeture des ResultSet. Une fois qu'un objet Statement ou ResultSet a été fermé, il est illégal d'invoquer une de ses méthodes. L'appel d'une telle méthode peut jeter systématiquement une exception de type SQLException.

2.4. Création d'un objet PreparedStatement

L'interface PreparedStatement étend l'interface Statement, donc tout ce qui précède et qui concerne l'interface Statement reste valide pour l'interface PreparedStatement. L'interface PreparedStatement ajoute la possibilité de paramétrer des requêtes SQL. Les instances de PreparedStatement s'utilisent quand une même requête doit être exécutée plusieurs fois, avec des paramètres différents. La chaîne de caractères SQL contient donc des marqueurs, qui seront remplacés par des valeurs à chaque exécution. On préfèrera systématiquement l'utilisation de PreparedStatement aux Statement classiques, en particulier pour les requêtes de mise à jour. Ces requêtes comportent le plus souvent des paramètres, qu'il faut ajouter à la chaîne de caractères SQL à exécuter. Utiliser des PreparedStatement permet de passer par les méthodes de paramétrage du pilote de base, ce qui nous décharge de la délicate gestion des erreurs, et évitera les attaques par injection de code SQL.

2.4.1. Création d'un PreparedStatement

Une instance de PreparedStatement se crée de la même façon qu'un Statement normal.

Exemple 9. Création d'un PreparedStatement

PreparedStatement ps = conn.prepareStatement(
    "insert into Marins (nom, prenom, age) values (?, ?, ?)") ;

De même que dans le cas d'un Statement normal, on peut passer des options lors de la création d'un PreparedStatement. On remarque la présence de trois caractères ? dans la chaîne SQL passée en paramètre. Ce sont ces caractères qui doivent être remplacés par des valeurs afin de pouvoir exécuter la requête SQL proprement dite.

2.4.2. Fixer les paramètres d'un PreparedStatement

L'interface PreparedStatement propose un jeu de méthode set<Type>(int, Type) : setInt(int, int), setFloat(int, float), setString(int, String), etc... Chacune de ces méthodes peut être appelée pour fixer la valeur d'un paramètre donné.

Exemple 10. Fixer les paramètres d'un PreparedStatement

PreparedStatement ps = conn.prepareStatement(
    "insert into Marins (nom, prenom, age) values (?, ?, ?)") ;

ps.setString(1,  "Surcouf") ;
ps.setString(2,  "Robert") ;
ps.setInt(3,  32) ;

On notera que la numérotation des paramètres commence à 1. Bien sûr, tous les paramètres doivent avoir une valeur fixée avant que la requête soit exécutée, sous peine de SQLException. On peut effacer la valeur de tous les paramètres d'un PreparedStatement en appelant la méthode clearParameter().

2.4.3. Positionner un paramètre à la valeur null

Positionner un paramètre à la valeur null nécessite l'utilisation d'une méthode particulière : setNull(int, int). Cette méthode prend deux paramètres :
  • le premier est le numéro d'ordre du paramètre ;
  • le second est le type du paramètre, il s'agit de l'une des constantes définies dans la classe java.sql.Types

Exemple 11. Positionner un paramètre à null

PreparedStatement ps = conn.prepareStatement(
    "insert into Marins (nom, prenom, age) values (?, ?, ?)") ;

ps.setString(1,  "Surcouf") ;
ps.setString(2,  "Robert") ;
ps.setNull(3, java.sql.Types.INTEGER) ;

2.5. Exécution d'un PreparedStatement

L'exécution d'un PreparedStatement se déroule de la même manière que pour un Statement, à l'aide des trois méthodes executeQuery(), executeUpdate() et execute().

2.5.1. Méthode getMetaData() et getParameterMetaData()

L'interface PreparedStatement propose deux méthodes :
  • getMetaData() : retourne un objet ResultSetMetaData, qui contient des informations sur les colonnes retournées par la requête SQL ;
  • getParameterMetaData() : retourne un objet ParameterMetaData, qui contient des informations sur les paramètres déclarés dans ce PreparedStatement.
La méthode getMetaData() retourne un objet de type ResultSetMetaData qui propose un jeu d'une vingtaine de méthodes pour déterminer le types de colonnes du résultat de la requête. Voyons ceci sur un exemple.

Exemple 12. Utilisation d'un objet ResultSetMetaData

// on ne connaît pas les colonnes du résultat de cette requête
PreparedStatement pstmt = conn.prepareStatement("select * from Marins where id = ?") ;
ResultSetMetaData rsmd = pstmt.getMetaData() ;

 // lecture du nombre de colonne
 int columnCount = rsmd.geColumnCount() ;
 for (int i =  1 ; i &lt;= columnCount ; i++) {
    int columnType     = rsmd.getColumnType(i) ;
    // label de la colonne
   String columnLabel = rsmd.getColumnLabel(i) ;
    // nom de la colonne
   String columnName  = rsmd.getColumnName(i) ;
}

On remarquera qu'ici aussi les paramètres sont numérotés à partir de 1. La méthode getParameterMetaData() retourne un objet de type ParameterMetaData, qui lui-même propose quelques méthodes pour obtenir des informations sur les paramètres d'un PreparedStatement. Voyons un exemple d'utilisation.

Exemple 13. Utilisation d'un objet ParameterMetaData

// on ne connaît pas les colonnes du résultat de cette requête
PreparedStatement pstmt = conn.prepareStatement("select * from Marins where id = ?") ;
ParameterMetaData pmd = pstmt.getParameterMetaData() ;

 // lecture du nombre de colonne
 int parameterCount = pmd.geParameterCount() ;
 for (int i =  1 ; i &lt;= parameterCount ; i++) {
    // type du paramètre (une constante de java.sql.Types)
    int parameterType = pmd.getColumnType(i) ;
    // mode du paramètre
    int parameterMode = pmd.getParameterMode(i) ;
}

Notons que le mode d'un paramètre, qui peut prendre l'une des valeurs IN, OUT ou INOUT est utilisé pour les CallableStatements.

2.6. Clés générées, méthode getGeneratedKeys()

Lors de l'insertion de lignes dans une table, il arrive que certaines valeurs soient générées automatiquement par la base de données. C'est notamment le cas des colonnes auto-incrémentales que l'on peut utiliser pour générer des clés primaires dans les tables MySQL. JDBC nous donne un moyen d'obtenir la valeur de la clé générée aussitôt la requête d'insertion effectuée. Heureusement, car sans ce moyen, il pourrait être assez compliqué de récupérer la donnée que l'on vient d'insérer. Voyons ceci sur un exemple.

Exemple 14. Méthode getGeneratedKeys()

Statement stmt = connection.createStatement() ;
 // le paramètre RETURN_GENERATED_KEYS va nous permettre de récupérer 
 // la clé générée par MySQL
 int rowCount = smt.executeUpdate(
    "insert into Marins (nom, prenom) values ('Surcouf', 'Robert')", 
   Statement.RETURN_GENERATED_KEYS) ;

ResultSet rs = stmt.getGeneratedKeys();
 if (rs.next()) {
    // récupération de la clé primaire
    int idSurcouf = rs.getInt(1) ;
}

On notera que pour fonctionner correctement, cette méthode ne doit être appelée que si la transaction est en mode non auto-commit .

2.7. Exécution de mises à jour en batch

Il est également possible de grouper des requête SQL dans un Statement et de les exécuter en une seule fois. On appelle ce genre d'exécution l'exécution en batch . En général cela signifie que l'on s'attend à ce que l'exécution totale soit assez longue, et que pendant ce temps, le système peut faire autre chose. L'exécution de commandes SQL en batch n'est possible que pour des requêtes de modification de la base, celles que l'on exécute avec la méthode executeUpdate(String). Toute requête SQL qui peut être exécutée de la sorte, peut également être passée en paramètre de la méthode addBatch(String). L'appel de cette méthode ne fait que stocker la requête SQL, sans l'exécuter. C'est l'appel à la méthode executeBatch() qui déclenche l'exécution. L'exécution en batch peut représenter des gains substantiels en performances. Le coût d'une aller et retour avec la base de données est en lui-même coûteux, et l'on tentera le plus possible de minimiser ces allers et retours.

2.7.1. Cas des Statement

Voyons un exemple d'exécution en batch pour les objets Statement.

Exemple 15. Exécution d'un Statement en batch

// l'exécution en batch doit se faire en mode non auto-commit
connection.setAutoCommit(false) ;
Statement smt = connection.createStatement() ;
smt.addBatch("insert into Marins(nom, prenom) values ('Surcouf', 'Robert')) ;
smt.addBatch("insert into Marins(nom, prenom) values ('Tabarly', 'Eric')) ;
...
 // lancement de l'exécution de toutes nos insertions
 int [] updateCounts = smt.executeBatch() ;

Comme le mode auto-commit a été désactivé, il nous est possible, en cas d'erreur sur l'appel à executeBatch() d'annuler la transaction et de remettre la base dans l'état initial.

2.7.2. Cas des PreparedStatement

Il est aussi possible de lancer des exécutions de mise à jour en batch lorsque l'on travaille avec des PreparedStatement. Le mécanisme est toutefois légèrement différent, puisqu'un PreparedStatement est construit sur une unique requête SQL paramétrée. PreparedStatement propose une méthode addBatch() qui ne prend aucun paramètre. Au moment où cette méthode est appelée, JDBC construit une requête SQL avec la requête du PreparedStatement, et le jeu de paramètres qu'il a à sa disposition. Si l'un des paramètres n'a pas été fixé, une erreur est générée. On peut ainsi ajouter autant de requêtes que l'on veut. L'exécution des requêtes se fait de la même manière et sous les mêmes contraintes que pour un Statement classique.

Exemple 16. Exécution d'un PreparedStatement en batch

// l'exécution en batch doit se faire en mode non auto-commit
connection.setAutoCommit(false) ;
PreparedStatement psmt = 
   connection.prepareStatement("insert into Marins(nom, prenom) " + 
                                "values (?, ?)") ;

psmt.setParameter(1,  "Surcouf") ;
psmt.setParameter(2,  "Robert") ;
 // ajout d'une requête avec les paramètres ('Surcouf', 'Robert')
psmt.addBatch() ;

psmt.setParameter(2,  "Pierre") ;
 // ajout d'une requête avec les paramètres ('Surcouf', 'Pierre')
psmt.addBatch() ;

psmt.setParameter(1,  "Tabarly") ;
psmt.setParameter(2,  "Eric") ;
 // ajout d'une requête avec les paramètres ('Tabarly', 'Eric')
psmt.addBatch() ;
...
 // lancement de l'exécution de toutes nos insertions
 int [] updateCounts = smt.executeBatch() ;

Java Database Connectivity
Retour au blog Java le soir
Cours & Tutoriaux