JDBC
JDBC (Java DataBase Connectivity) est l'API standard pour interagir avec les bases données relationnelles en Java. JDBC fait partie de l'édition standard et est donc disponible directement dans le JDK.
Préambule : try-with-resources
L'API JDBC donne accès à des objets qui correspondent à des ressources de base de données que
le développeur doit impérativement fermer par l'appel à des méthodes close()
.
Ne pas fermer correctement les objets fournis par JDBC est un bug qui conduit habituellement
à un épuisement des ressources système, empêchant l'application de fonctionner correctement.
Java 7 a introduit l'interface AutoCloseable
ainsi qu'une nouvelle syntaxe dénommée try-with-resources.
L'API JDBC utilise massivement l'interface AutoCloseable
et autorise donc le try-with-resources.
Ainsi, les deux codes ci-dessous sont équivalents puisque la classe
java.sql.Connection
implémente AutoCloseable
:
La version utilisant la syntaxe du try-with-resources est plus compacte et prend en charge automatiquement l'appel à la méthode close()
.
Tout au long de ce chapitre sur JDBC, nous utiliserons alternativement l'une ou l'autre des syntaxes.
Le pilote de base de données
JDBC est une API indépendante de la base de données sous-jacente. D'un côté, les développeurs implémentent les interactions avec une base de données à partir de cette API. D'un autre côté, chaque fournisseur de SGBDR livre sa propre implémentation d'un pilote JDBC (JDBC driver). Pour pouvoir se connecter à une base de données, il faut simplement ajouter le driver (qui se présente sous la forme d'un fichier jar) dans le classpath lors de l'exécution du programme.
Des pilotes JDBC sont disponibles pour les SGBDR les plus utilisés : Oracle DB, MySQL, PostgreSQL, Apache Derby, SQLServer, SQLite, HSQLDB (HyperSQL DataBase), ...
Pour un projet géré par Maven, le pilote JDBC est une dépendance logicielle comme une autre. Pour l'intégrer
dans le livrable, il suffit de le déclarer dans le fichier pom.xml dans la section dependencies
:
On peut rechercher le pilote souhaité sur le site du Maven Repository.
Pour l'utilisation de JDBC dans un serveur d'application Java EE, il est aussi possible d'installer le
pilote JDBC directement dans le serveur plutôt que de l'ajouter comme dépendance de l'application. Pour TomEE,
il suffit de télécharger le fichier jar du pilote JDBC et de le copier dans le répertoire lib
du répertoire
d'installation du serveur.
Création d'une connexion
Une connexion à une base de données est représentée par une instance de la classe Connection. Il existe plusieurs façons d'obtenir une instance de cet objet. Nous simplifierons légèrement le propos en considérant deux cas d'utilisation : la création d'une connexion sans Java EE et la création d'un connexion avec Java EE.
Création d'une connexion sans Java EE
Comme nous l'avons précisé au début de ce chapitre, JDBC fait partie de l'API standard du JDK. Il n'est donc pas nécessaire de disposer d'un conteneur Java EE ou d'un serveur d'application pour l'utiliser. Pour une utilisation hors de Java EE, la classe DriverManager permet d'enregister un pilote JDBC et de créer une connexion :
Lorsque la connexion n'est plus nécessaire, il faut libérer les ressources allouées en la fermant avec la méthode close()
.
La classe Connection
implémente AutoCloseable
, ce qui l'autorise à être utilisée dans un try-with-resources.
Création d'une connexion avec Java EE
Dans un serveur d'application, l'utilisation du DriverManager
est remplacée par celle
de la DataSource
.
L'interface DataSource
n'offre que deux méthodes :
Il n'est pas possible de spécifier l'URL de connection à la base de données avec une DataSource
.
Par contre une DataSource
peut être injectée dans n'importe quel composant Java EE (Servlet, Bean CDI, EJB)
grâce à l'annotation @Resource
:
L'annotation @Resource
permet de spécifier le nom de la DataSource grâce à l'attribut name.
Mais comment le serveur d'application fait-il pour lier une DataSource avec une connexion vers une base de données ?
Malheureusement, il n'existe pas de standard et chaque serveur d'application dispose de sa procédure. Pour TomEE,
une DataSource se configure dans le fichier tomee.xml. Ce fichier se trouve dans le répertoire conf
du répertoire d'installation du serveur. On peut ainsi déclarer une source de données directement dans le serveur.
Il est également possible d'ajouter un fichier resources.xml dans le répertoire WEB-INF
de son application.
Ce fichier a le même format que le fichier tomee.xml mais il fournit une définition des sources de données uniquement pour cette application.
Le nom de la source de données est indiqué par l'attribut id
de la balise Resource
.
La documentation officielle de TomEE contient des informations intéressantes à consulter :
- DataSource Configuration (documentation des paramètres de la balise
Resource
) - Common DataSource Configurations (exemples de configuration de
DataSources
pour divers SGBDR)
Ce système de configuration est certes plus compliqué que l'utilisation du DriverManager
mais il permet
à l'application d'ignorer les détails de configuration. L'utilisation des DataSources
dans un serveur
d'application apporte généralement des fonctionnalités supplémentaires telles que la mise en cache et la réutilisation
de connexions (pour améliorer les performances), les tests permettant de vérifier que les connexions sont correctements
établies, la supervision des connexions, ...
L'URL de connexion et la classe des pilotes
Comme nous l'avons vu à la section précédente, pour établir une connexion, nous avons besoin de connaître la classe du pilote et l'URL de connexion à la base de données. Il n'existe pas vraiment de règle en la matière puisque chaque fournisseur de pilote décide du nom de la classe et du format de l'URL. Le tableau suivant donne les informations nécessaires suivant le SGBDR :
SGBDR | Nom de classe du pilote | Format de l'URL de connexion |
---|---|---|
Oracle DB | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@[host]:[port]:[schema] Ex : jdbc:oracle:thin:@localhost:1521:maBase |
MySQL | com.mysql.jdbc.Driver | jdbc:mysql://[host]:[port]/[schema] Ex : jdbc:mysql://localhost:3306/maBase |
PosgreSQL | org.postgresql.Driver | jdbc:postgresql://[host]:[port]/[schema] Ex : jdbc:postgresql://localhost:5432/maBase |
HSQLDB (mode fichier) | org.hsqldb.jdbcDriver | jdbc:hsqldb:file:[chemin du fichier] Ex : jdbc:hsqldb:file:maBase |
HSQLDB (mode mémoire) | org.hsqldb.jdbcDriver | jdbc:hsqldb:mem:[schema] Ex : jdbc:hsqldb:mem:maBase |
Les requêtes SQL (Statement)
L'interface Connection permet,
entre autres, de créer des Statements
. Un Statement
est une interface qui permet
d'effectuer des requêtes SQL. On distingue 3 types de Statement
:
Statement
: Permet d'exécuter une requête SQL et d'en connaître le résultat.PreparedStatement
: Comme leStatement
, lePreparedStatement
permet d'exécuter une requête SQL et d'en connaître le résultat. LePreparedStatement
est une requête paramétrable. Pour des raisons de performance, on peut préparer une requête et ensuite l'exécuter autant de fois que nécessaire en passant des paramètres différents. LePreparedStatement
est également pratique pour se prémunir efficacement des failles de sécurité par injection SQL.CallableStatement
: permet d'exécuter des procédures stockées sur le SGBDR. On peut ainsi passer des paramètres en entrée duCallableStatement
et récupérer les paramètres de sortie après exécution.
Le Statement
Un Statement
est créé à partir d'une des méthodes createStatement
de l'interface
Connection. À partir
d'un Statement
, il est possible d'exécuter des requêtes SQL :
Attention, un Statement
est une ressource JDBC et il doit être
fermé dès qu'il n'est plus nécessaire :
La classe Statement
implémente AutoCloseable
, ce qui l'autorise à être utilisée dans un try-with-resources.
Pour des raisons de performance, il est également possible d'utiliser un Statement
en mode batch. Cela signifie, que l'on accumule l'ensemble des requêtes SQL côté client, puis
on les envoie en bloc au serveur plutôt que de les exécuter séquentiellement.
Le ResultSet
Lorsqu'on exécute une requête SQL de type select
, JDBC nous donne accès à
une instance de ResultSet
.
Avec un ResultSet
, il est possible de parcourir ligne à ligne les résultats de la requête
(comme avec un itérateur) grâce à la méthode
ResultSet.next().
Pour chaque résultat, il est possible d'extraire les données dans un type supporté par Java.
Le ResultSet
offre une liste de méthodes de la forme :
XXX représent le type Java que la méthode retourne. Si on passe un numéro en paramètre, il s'agit du numéro de la colonne dans l'ordre du select. Attention, le numéro de la première colonne est 1.
Attention, un ResultSet
est une ressource JDBC et il doit être
fermé dès qu'il n'est plus nécessaire :
La classe ResultSet
implémente AutoCloseable
, ce qui l'autorise à être utilisée dans un try-with-resources.
Le PreparedStatement
Un PreparedStatement
est créé à partir d'une des méthodes prepareStatement
de l'interface
Connection.
Lors de l'appel à prepareStatement
, il faut passer la requête SQL à exécuter. Cependant, cette requête
peut contenir des ? indiquant l'emplacement des paramètres.
L'interface PreparedStatement
fournit des méthodes de la forme :
XXX représente le type du paramètre, parameterIndex sa position dans la requête SQL (attention, le premier paramètre a l'indice 1) et x sa valeur.
Attention, un PreparedStatement
est une ressource JDBC et il doit être
fermé dès qu'il n'est plus nécessaire :
La classe PreparedStatement
implémente AutoCloseable
, ce qui l'autorise à être utilisée dans un try-with-resources
Le PreparedStatement
reprend une API similaire à celle du Statement
:
- une méthode
execute
pour tous les types de requête SQL - une méthode
executeQuery
(qui retourne unResultSet
) pour les requêtes SQL de type select - une méthode
executeUpdate
pour toutes les requêtes SQL qui ne sont pas des select
Le PreparedStatement
offre trois avantages :
- il permet de convertir efficacement les types Java en types SQL pour les données en entrée
- il permet d'améliorer les performances si on désire exécuter plusieurs fois la même requête avec des paramètres différents. À
noter que le
PreparedStatement
supporte lui aussi le mode batch - il permet de se prémunir de failles de sécurité telles que l'injection SQL
Le CallableStatement
Un CallableStatement
permet d'appeler des procédures ou des fonctions stockées.
Il est créé à partir d'une des méthodes prepareCall
de l'interface
Connection.
Comme pour le PreparedStatement
, il est nécessaire de passer la requête lors de l'appel à prepareCall
et l'utilisation de ? permet de spécifier les paramètres.
Cependant, il n'existe pas de syntaxe standard en SQL pour appeler des procédures ou des fonctions stockées. JDBC définit tout de même une syntaxe compatible avec tous les pilotes JDBC :
Un CallableStatement
permet de passer des paramètres en entrée avec des méthodes de type setXXX
comme pour le PreparedStatement
. Il permet également de récupérer les paramètres en sortie avec des méthodes
de type getXXX
comme on peut trouver dans l'interface ResultSet
. Comme pour le PreparedStatement
,
on retrouve les méthodes execute
, executeUpdate
et executeQuery
pour réaliser l'appel
à la base de données.
Attention, un CallableStatement
est une ressource JDBC et il doit être
fermé dès qu'il n'est plus nécessaire :
La classe CallableStatement
implémente AutoCloseable
, ce qui l'autorise à être utilisée dans un try-with-resources
La transaction
La plupart des SGBDR intègrent un moteur de transaction. Une transaction est définie par le respect de quatre propriétés désignées par l'acronyme ACID :
- Atomicité : la transaction garantit que l'ensemble des opérations qui la composent sont soit toutes réalisées avec succès soit aucune n'est conservée.
- Cohérence : la transaction garantit qu'elle fait passer le système d'un état valide vers un autre état valide.
- Isolation : deux transactions sont isolées l'une de l'autre. C'est-à-dire que leur exécution simultanée produit le même résultat que si elles avaient été exécutées successivement.
- Durabilité : la transaction garantit qu'après son exécution, les modifications qu'elle a apportées au système sont conservées durablement.
Une transaction est définie par un début et une fin qui peut être soit une validation des modifications (commit), soit une annulation des modifications effectuées (rollback). On parle de démarcation transactionnelle pour désigner la portion de code qui doit s'exécuter dans le cadre d'une transaction.
Avec JDBC, il faut d'abord s'assurer que le pilote ne commite pas sytématiquement à chaque requête SQL (l'auto commit). Une opération de commit à chaque requête SQL
équivaut en fait à ne pas avoir de démarcation transactionnelle. Sur l'interface
Connection
, il existe les méthodes
setAutoCommit(boolean)
et
getAutoCommit()
pour nous aider à gérer
ce comportement. Attention, dans la plupart des implémentations des pilotes JDBC, l'auto commit est activé par défaut (mais ce n'est pas une règle).
Dans les serveurs d'application Java EE, l'activation ou non de l'auto commit par défaut et souvent configurable au niveau de la DataSource
.
C'est le cas pour TomEE, puisque l'attribut defaultAutoCommit
peut être positionné sur une balise Resource
. Cet attribut vaut
true
par défaut.
À partir du moment où l'auto commit n'est plus actif sur une connexion, il est de la responsabilité du développeur
d'appeler sur l'instance de Connection
la méthode commit()
(ou rollback()
).
Le contrôle de la démarcation transactionnelle par programmation est surtout utile lorsque l'on souhaite garantir l'atomicité d'un ensemble de requêtes SQL.