Remarque préliminaire : nous décrivons ici synthétiquement l'usage de l'API JDBC permettant l'accès à des bases de données relationnelles depuis des programmes Java. On suppose que le lecteur possède des bases préalables d'utilisation de bases relationnelles avec une connaissance du langage SQL.
JDBC ?
-
Comme son nom l'indique l'API JDBC permet de se connecter à une base de données (relationnelle)
- Elle permet d'exécuter des requêtes SQL sur des tables de la base de données
- L'API offre une abstraction permettant de se connecter à n'importe quel type de base SQL
- Mais il est néanmoins nécessaire de disposer du pilote JDBC pour le type de base manipulé
- Lorsque l'on change de type de base, on ne change que le pilote utilisé
- JDBC fait partie de l'ensemble des APIs de Java Enterprise Edition
Pilotes JDBC
Oracle distingue 4 types de pilotes JDBC permettant de d'interfacer avec une base :
-
Le type 1 qui repose sur une pilote JDBC faisant office de passerelle vers un pilote Open DataBase Connectivity (ODBC)
- ODBC est une API d'abstraction initiée par Microsoft en 1992 avant la création de Java et offrant des fonctionnalités similaires à JDBC pour des langages tels que le C, C++...
- La passerelle JDBC-ODBC ne devrait être utilisée que s'il n'existe pas de pilote JDBC pour la base accédée
- Le support de la passerelle JDBC-ODBC n'est plus assuré depuis Java 1.8
- Le type 2 qui utilise une bibliothèque d'accès native à la base avec un pilote Java d'interfacage allégé réalisant des appels JNI
- Le type 3 qui est un pilote JDBC écrit nativement en Java et ne reposant sur aucune bibliothèque externe
- Le type 4 qui est un pilote JDBC exploitant des connexions réseau pour contacter la base
Comment créer un pilote JDBC ?
- Il faut consulter la spécification JDBC
- On ne s'intéressera pas ici à la création de pilote (mais plutôt à l'utilisation de l'API JDBC)
Pilotes disponibles
Les bases de données relationnelles SQL les plus populaires disposent de pilotes JDBC :
- MySQL et MariaDB (fork de MySQL) : moteur de BDD souvent utilisé par des sites web
- PostgreSQL
- Oracle
- HSQLDB : moteur de BDD léger écrit en langage Java
- SQLite : BDD légère orientée fichier très utilisée pour des applications embarquées (intégrée notamment sous Android)
SQL
- SQL = Structured Query Language
- Il s'agit d'un langage standardisé de requêtage de base de données relationnelles
-
7 types principaux de requêtes sont supportés par la majorité des bases relationnelles :
- SELECT pour interroger une table de la base et récupérer des enregistrements
- INSERT pour insérer un nouvel enregistrement dans une table
- UPDATE pour mettre à jour des enregistrements d'une table
- DELETE pour supprimer des enregistrements d'une table
- CREATE pour créer une nouvelle base, table ou index
- ALTER pour modifier le schéma d'une table (ajout ou suppression de colonnes)
- DROP pour supprimer une base, table ou index
-
Il existe d'autres types d'instructions SQL plus ou moins supportés permettant :
- de manipuler des vues (sortes de tables virtuelles basées sur une requête SELECT)
- d'utiliser des procédures stockées dans la BDD avec des triggers : les procédures sont déclenchées à la survenue de certains événements (ajout, modification d'enregistrement...)
- de gérer des utilisateurs avec une politique de droits d'accès (GRANT, REVOKE...)
- de réaliser des opérations de maintenance sur la base (compression des données, défragmentation, réorganisation des index...)
- de réaliser des sauvegardes de la base
- ...
- JDBC permet d'exécuter n'importe quelle instruction SQL supportée par la base appelée
- JDBC propose une API plus agréable que l'utilisation du langage SQL brut pour les instructions les plus courantes sur les enregistrements
Connexion à la base de données
Pour se connecter à la base de données, il faut récupérer un objet Connection en indiquant l'URL de connexion (dont le format varie selon le pilote de BDD utilisé, par exemple jdbc:postgresql://localhost:5432/MyDatabase):
Connection conn = DriverManager.getConnection(url, user, passwd);
Avant d'initier la connexion, il faut charger la classe correspondant au pilote JDBC, par exemple pour le pilote PostgreSQL :
Class.forName("org.postgresql.Driver");
Cette instruction peut lever ClassCastException si le pilote n'est pas trouvable notamment si on a oublié de rajouter le JAR contenant le pilote dans le classpath utilisé par le chargeur de classe (option -cp de java ou variable d'environnement CLASSPATH).
Idéalement, les données nécessaires à la connexion à la BDD (URL, utilisateur, mot de passe...) doivent être conservées dans un fichier de configuration avec droits d'accès limités (ne surtout pas stocker statiquement ces données dans le code source !).
La plupart des instructions impliquant l'API JDBC sont susceptibles de lever l'exception vérifiée SQLException qu'il faut donc capturer ou propager dans la pile d'appel. Plus d'informations sur les différents types d'exception sont disponibles ici.
Optimisation avec un pool de connexions
- Créer une connexion a un coût, il est conseillé de les réutiliser si possible
- On peut utiliser l'API Oracle gérant des piscines de connexions (voir ici pour un exemple)
PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource(); pds.setConnectionFactoryClassName("org.postgresql.ds.PGPoolingDataSource"); pds.setURL("jdbc:postgresql://localhost:5432/MyDatabase"); pds.setUser(user); pds.setPassword(password); //Setting pool properties pds.setInitialPoolSize(5); pds.setMinPoolSize(5); pds.setMaxPoolSize(10); //Borrowing a connection from the pool Connection conn = pds.getConnection();
Émission de requêtes SQL brutes
Pour exécuter une requête SQL :
- On créé tout d'abord un Statement depuis la connection avec la BDD : Statement s = conn.getStatement()
- On exécute la requête sur le Statement : s.execute("SELECT * from mytable WHERE name = 'foo'")
- Des commandes peuvent être exécutées en lot en les ajoutant avec s.addBatch("...") puis en demandant l'exécution du lot de commandes avec int[] s.executeBatch() (qui retourne un tableau avec le nombre d'enregistrements impactés par chaque requête)
-
On appelle des getters pour travailler sur le résultat de la requête :
- ResultSet getResultSet() pour obtenir les enregistrements retournés par la requête, ce qui n'est pertinent que pour les requêtes de type SELECT (dans les autres cas, null est retourné)
- ìnt getUpdateCount() pour connaître le nombre d'enregistrements impacté par une requête de type UPDATE
- Il peut y avoir potentiellement plusieurs résultats (ResultSet ou indications de nombre d'update) ; la méthode getMoreResults() permet de se déplacer au résultat suivant
- La méthode SQLWarning getWarnings() permet de connaître d'éventuels avertissements survenus lors de l'exécution
- ResultSet getGeneratedKeys() est une méthode utile pour des commandes INSERT afin de connaître les clés primaires auto-générées pour les enregistrements insérés
- A l'issue du travail réalisé sur le Statement, on peut appeler sa méthode close() pour libérer les ressurces qui lui sont associées ; on peut initialiser un Statement dans un bloc try-with-resource
Récupération des resultats d'un SELECT avec un ResultSet
- ResultSet est un curseur d'enregistrements (une sorte d'itérateur) typiquement obtenu par une requête SELECT
- On peut se déplacer d'enregistrement en enregistrement avec la méthode next() ou previous() qui retournent un booléen indiquant si le déplacement a été réalisé ; les méthodes first() et last() permettent de se rendre au premier et dernier enregistrement
-
On utilise des getters afin de consulter les données d'un enregistrement (i.e. la valeur de chaque colonne) ; chaque getter présent pour un type spécifique de données existe en deux versions selon son argument : un int indiquant le numéro de colonne (commençant à 0) ou alors un String indiquant le nom de la colonne.
- Array getArray(...)
- boolean getBoolean(...)
- byte[] getBytes(...)
- Date getDate(...)
- double getDouble(...), float getFloat(...)
- int getInt(...), long getLong(...)
- Object getObject(...)
- String getString(...)
- ...
- Si on ne connaît pas le type d'une colonne getObject() peut être utilisé (le type Java le plus approprié sera utilisé)... mais il est toujours préférable d'avoir une connaissance préalable du schéma des tables de la BDD.
- Depuis JDBC 2.0, ResultSet peut également être utilisé en écriture afin d'insérer, mettre à jour ou supprimer des enregistrements
Utilisation de PreparedStatement
- PreparedStatement permet de créer une requête paramétrée pouvant être exécutée plusieurs fois (avec des paramètres différents)
- On initialise un PreparedStatement depuis une Connection : conn.prepareStatement("...") (on remplace ... par la requête SQL à préparer. Chaque paramètre est représenté par un ? indiquant son emplacement.
- Une fois PreparedStatement créé, on peut la paramétrer avec des setters du style setX(int parameterIndex, X parameterValue) où X est le type du paramètre manipulé (cf les types des getters de colonnes sur ResultSet).
- Une fois paramétrée, on exécute la requête avec boolean execute() (qui retourne true si un ResultSet est récupérable, si false, il faut récupérer le nombre d'enregistrements impacté avec getUpdateCount())
-
PreparedStatement doit toujours être préférée à son ancêtre Statement pour deux raisons principales :
- Cela permet la réutilisation de la même requête avec des paramètres différents
- Cela permet le formatage automatique de la requête SQL en fonction du type des paramètres
- Cela limite les risques d'injection SQL si les paramètres sont obtenus par une source extérieure non-vérifiée
- CallableStatement (qui hérite de PreparedStatement) peut être utilisé pour exécuter des procédures stockées sur la base
Exemple pour l'insertion d'un enregistrement
public class StudentManager implements AutoCloseable { private Connection connection; ... private PreparedStatement studentInsertion; public synchronized boolean insertStudent(String name, int birthYear, double grade) throws SQLException { if (studentInsertion == null) { studentInsertion = connection.prepareStatement("INSERT INTO student (name, birthYear, grade) VALUES (?,?,?)"); // the statement is prepared, it will be reused for the future calls to insertStudent } studentInsertion.setString(0, name); studentInsertion.setInt(1, birthYear); studentInsertion.setDouble(2, grade); studentInsertion.execute(); return studentInsertion.getUpdateCount() == 1; } public static void main(String[] args) throws IOException, SQLException { Scanner s = new Scanner(System.in); try (StudentManager m = new StudentManager()) { m.init(); // we initialize the connection to the database while (scanner.hasNextLine()) { // we ask the data of the student to add System.out.println("Enter the name of the student to add"); String name = scanner.nextLine(); System.out.println("Enter her/his birth year"); int birthYear = scanner.nextInt(); System.out.println("Enter her/his grade"); double grade = scanner.nextDouble(); System.out.println("Thanks, we insert the student in the database"); m.insertStudent(name, birthYear, grade); } } } }
Si l'on avait utilisé un Statement plutôt qu'un PreparedStatement, il aurait été nécessaire de bien vérifier que les apostrophes (caractère ') soient bien déspécialisés en les doublant car leur rôle spécial est de délimiter les valeurs. Cette bande dessinée résume bien le problème.
Utilisation d'une transaction
Une transaction permet de grouper l'exécution de plusieurs requêtes SQL de façon atomique. Cela signifie que soit toutes les requêtes sont exécutées, soit aucune (il ne peut y avoir d'exécution partielle). Dans certaines situations, les transactions sont indispensables pour garantir l'intégrité des données en base.
Par défaut le mode autocommit est activé : cela signifie que chaque requête SQL donne lieu à une transaction. Si l'on souhaite regrouper plusieurs requêtes dans une transaction, on désactive d'abord ce mode :
connection.setAutoCommit(false);
On peut ensuite exécuter les requêtes. On peut clore la transaction de deux façons :
- soit en appelant connection.commit() pour la valider et impacter les modifications sur la base
- soit en utilisant connection.rollback() pour annuler la transaction : aucune modification n'est alors répercutée sur la base ; il est hautement conseillé d'inclure les instructions SQL réalisée dans un bloc try et d'appeler connection.rollback() en cas de capture d'exception