lundi 21 mars 2016

[ABD][Pratique] Statement et PreparedStatement : pour éviter les problèmes liés aux caractères spéciaux

Une application centrée sur une base des données relationnelle doit implémenter les différentes requêtes de manipulation des données nécessaires à son bon fonctionnement.
En effet, la partie LMD du SQL trouve sa place au sein des applications, contrairement à la partie LDD utilisée pour créer la base des données pendant le développement et/ou le déploiement de l'application. Le langage LMD permet d'exécuter les quatre opérations de base : l'insertion, la modification, la suppression (physique, même si elle est déconseillée) et la consultation.
Garantir ces opérations nécessite la présence d'un échange dans les deux sens entre l'application et le SGBD. L'application envoie la requête (une chaîne de caractère - comme tout langage de programmation) et le SGBD répond par :

  1. Le résultat de la requête : l'ensemble des données qui répondent aux critères définis dans la requête "Select",
  2. Un entier : le nombre des enregistrements (lignes, occurrences, tuples) affectés par les requêtes "Insert", "Update" et "Delete".
En Java, il est nécessaire de faire appel à deux classes et un pilote pour pouvoir exécuter une requête. Le pilote dépend du SGBD utilisé; la plus part des SGBD disposent d'un pilote JDBC qui est sous la forme d'une (ou de plusieurs) fichier(s) JAR (bibliothèques externes) faciles à intégrer dans le projet. Les deux autres classes sont :
  1. La classe Connection (du package java.sql) : comme son nom l'indique, il nous permet de se connecter à un SGBD et comme la logique l'impose, l'objet est créé en utilisant le pilote dédié au SGBD ainsi que les paramètres de connexion : l'adresse du serveur, le nom de l'utilisateur et son mot de passe.
  2. La classe Statement : c'est la classe qui nous permet d'exécuter les requêtes et de récupérer leurs résultats, pour cela, nous faisons appel aux méthodes .executeQuery() pour exécuter des requêtes de type "Select" et .executeUpdate() pour exécuter des requêtes de type "Insert into", "Update" et "Delete".
Prenons un exemple, je vais me connecter à une base des données qui s'appelle "test" sur ma machine locale en utilisant mon compte personnel. Je vais tenter par la suite d'insérer un enregistrement (ou tuple ou occurrence, comme vous voulez l'appeler).
La table que vais utiliser est une table simple avec trois champs : une clé primaire (un numéro séquentiel), un titre et un contenu (tous les deux des chaînes de caractères mais la taille du contenu est très grande) :

create table Article (
id Integer primary key auto_increment,
titre char(100),
contenu text
);

La classe d'insertion sera ainsi :

import java.io.*;

import java.sql.*;

public class Exemple01{
 
  Connection con;
  Statement st;
  
  final String URLSGBD = "jdbc:mysql://localhost:3306/test";
  final String USER = "tarek";
  final String PASSWORD = "tarek";
  
  public Exemple01(){
  }
  
  public void testInserer(String titre, String contenu){
    
    try {
    
      Class.forName("com.mysql.jdbc.Driver");
      con = DriverManager.getConnection(URLSGBD, USER, PASSWORD);
      st = con.createStatement();
      int rs = st.executeUpdate("Insert into Article (titre, contenu) Values ('" + titre + "', '" + contenu +"')");

      if(rs > 0){
             System.out.println("Insertion avec succès");
      }
      
      st.close();
      con.close();

    }catch(Exception e){
      e.printStackTrace();
    }
  }
  
}

Notez le chargement du driver avant la création de l'objet Connection :

Class.forName("com.mysql.jdbc.Driver");

Pur exécuter ce code (sans une interface graphique), je vais créer une petite classe :
public class Executer{
 public static void main(String args[]){
  String titre = "Voici un premier article";
  String contenu = "Et voici son contenu";
  
  Exemple01 ex01 = new Exemple02();
  ex01.testInserer(titre, contenu); 
 }
}

Et je vais exécuter la ligne suivante :

java -cp "./mysql-connector-java-5.1.38-bin.jar:." Executer

Le paramètre -cp (pour CLASSPATH) indique le lieu où Java va chercher les différentes bibliothèques et fichiers class du projet. Dans cette exemple, la CLASSPATH se compose de deux parties séparées par les deux points ":" :
  1. mysql-connector-java-5.1.38-bin.jar : le pilote dédié au SGBD MySQL.
  2. . : c'est à dire le répertoire en cours où se trouve ma classe.
Le résultat affiché montre que l'insertion s'est déroulée avec succès.
En vérifiant du côté MySQL, la requête "Select" permet de voir le contenu de la table :


Est ce que c'est tout ? Non. Le deuxième objectif de cet article est de mettre le point sur un problème très fréquent : les caractères spéciaux. Ces caractères peuvent poser des problèmes avec les requêtes générée se qui nous donne une requête incorrecte. A titre d'exemple, je vais changer la valeur titre de l'exemple précédent comme suit :

String titre = "Cette apostrophe ' va causer le problème";

La même commande d'exécution me donne cette fois ci :

com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'va causer le problème', 'Et voici son contenu')' at line 1
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
        at com.mysql.jdbc.Util.getInstance(Util.java:387)
        at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:939)
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3878)
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3814)
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2478)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2547)
        at com.mysql.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1541)
        at com.mysql.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2605)
        at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1469)
        at Exemple01.testInserer(Exemple01.java:24)
        at Executer.main(Executer.java:7)

Alors comment faire pur régler ce problème ? Vous avez deux possibilités :
  1. Corriger ces caractères spéciaux en ajoutant des méthodes dédiées.
  2. Utiliser la classe PreparedStatement.
Cette classe est très efficace pour exécuter des requêtes avec des paramètres. Elle gère les paramètres ainsi que leurs types des données ce qui permet de décharger le développeur de la tâche pénible.
Cette amélioration ne passe pas inaperçue au niveau code : l'exécution d'une requête, qui s'effectuait en une seule étape avec Statement, devient en trois étapes avec PreparedStatement :
  1. Préparation de la requête : la requête est une chaîne de caractères qui contient des paramètres. Chaque paramètre est désigné par un point d'interrogation (?).
  2. Passage des paramètres :cela s'effectue par les méthodes set<Type de donnée> telles que setInt, setString, etc.. Ces méthodes ressemblent aux méthodes get utilisées par la classe ResultSet.
  3. Exécution de la requête.
Modifions le code du premier exemple pour remplacer Statement par PreparedStatement :

import java.io.*;

import java.sql.*;

public class Exemple02{
 
  Connection con;
  PreparedStatement st;
  
  final String URLSGBD = "jdbc:mysql://localhost:3306/test";
  final String USER = "tarek";
  final String PASSWORD = "tarek";
  
  public Exemple02(){
  }
  
  public void testInserer(String titre, String contenu){
    
    try {
    
      Class.forName("com.mysql.jdbc.Driver");
      con = DriverManager.getConnection(URLSGBD, USER, PASSWORD);
      
      String requete = "Insert into Article (titre, contenu) Values ( ?, ?)";
      st = con.prepareStatement(requete);
      
      st.setString(1, titre);
      st.setString(2, contenu);
      
      int rs = st.executeUpdate();

      if(rs > 0){
             System.out.println("Insertion avec succès");
      }
      
      st.close();
      con.close();

    }catch(Exception e){
      e.printStackTrace();
    }
  }
  
}

Et avec un changement minimal dans la classe Executer :
public class Executer{
 public static void main(String args[]){
  String titre = "Cette apostrophe ' ne va pas causer des problèmes";
  String contenu = "Et voici son contenu inséré grace à PreparedStatement";
  
  Exemple02 ex02 = new Exemple02();
  ex02.testInserer(titre, contenu); 
 }
}

L'utilisation des mêmes données qui causaient le problème avec le premier exemple nous donne maintenant :

Insertion avec succès

La vérification en ligne de commande par une requête "Select" en utilisant l'outil mysql nous donne :


La classe PreparedStatement est une autre classe qui permet d'exécuter des requêtes SQL. Elle ne nécessite pas des connaissances supplémentaires par rapport à la classe Statement mais elle permet de décharger le développeurs de la gestion "manuelle" des types des données et des paramètres d'une requêtes SQL.

Aucun commentaire: