JDBI : se faciliter l’utilisation de JDBC

#framework#java#outil

Introduction

Lorsqu’il s’agit de récupérer des données depuis une base relationnelle et de manipuler les objets correspondants, il est souvent fait mention des ORMs.

L’utilisation d’un ORM n’est pas toujours la solution retenue ou la plus adaptée et dans l’univers Java, la solution la plus directe est l’interface JDBC. Cependant, l’utilisation de cette API de bas-niveau peut s’avérer fastidieuse.

JDBI propose un accès pratique et idiomatique aux bases de données relationnelles par l’intermédiaire de JDBC. La version de JDBI présentée ci-après est la version 3, tirant profit des fonctionnalités de la version 8 de Java.

Utilisation par l’exemple

Prenons pour exemple une application permettant de gérer des listes de courses. Une liste de courses se caractérise par un identifiant, un état (ouverte ou terminée), ainsi qu’un commentaire. Elle est composée de produits à acheter en une certaine quantité.


Figure 1 : Modèle de données

 

Ce modèle se traduit par le schéma SQL suivant :

Connexion et démarrage

La classe JDBI est le point d’entrée de la librairie. Cette classe permet d’abstraire la source de données JDBC faisant référence à la base de données à utiliser.

Il est possible de créer cette instance grâce à une référence directe à la base de données ou encore d’utiliser une source de données et ainsi de pouvoir également bénéficier d’un pool de connexions :

Premières requêtes

L’exécution de requêtes passe par la manipulation de l’objet « Handle », représentant une connexion active à la base de données.

Cet objet permet d’exécuter tous les types de requêtes ainsi que de gérer les transactions. Il fournit des méthodes chaînées (fluent API), permet d’associer les valeurs nécessaires aux arguments des requêtes, d’exécuter l’instruction SQL et d’en récupérer le résultat (sous forme d’objets ou types primitifs).

L’instruction « createUpdate » exécute une requête modifiant les données (Insert, Update ou Delete) et offre la possibilité de paramétrer les éléments de la requête.

La récupération de donnée se fait de façon similaire avec la méthode « createQuery » :

La méthode « findOnly() » considère que le résultat de la requête doit être composé d’une seule et unique ligne. Dans le cas contraire, une exception sera levée.

La méthode « findOne() » retourne la première ligne du résultat de la requête (si elle existe) sous la forme d’un « Optional ». Si le résultat contient plusieurs lignes, seule la première ligne est traitée, les autres sont ignorées.

Enfin une méthode « list() » récupère l’ensemble des lignes du résultat sous forme de liste. Cette liste peut être vide si le résultat de la requête l’est également.

Notons ici la différence entre les méthodes « useHandle » et « withHandle ». La méthode « useHandle » prenant en paramètre un « Consumer » ne renvoie aucun résultat, alors que la méthode « withHandle » attendant en paramètre un « Callback » permet le retour d’un résultat.

A noter également qu’il aurait été possible pour l’instruction d’insertion d’utiliser un « Callback » afin de récupérer le nombre d’éléments affectés par l’instruction « createUpdate » (retour de la méthode« execute() »). Cette fonctionnalité est particulièrement utile dans le cas d’une requête de mise à jour pour connaître le nombre d’enregistrements modifiés par la requête.

L’objet « Handle » propose aussi des méthodes pour les traitements « batch » avec « createBatch » et « preparedBatch » et l’exécution de procédures stockées avec « createCall ».

Paramétrage des requêtes

JDBI permet de spécifier les arguments des requêtes de plusieurs manières. La plus courante est la possibilité de valoriser les arguments par leur position (index) dans la requête, ou par leur nom.

JDBI est également capable de lier la valeur des attributs d’un objet aux paramètres nommés de la requête.

Un objet est fourni pour le paramétrage de la requête. Les paramètres sont valorisés par les attributs de l’objet.

Une autre variante est d’utiliser une simple « Map » ayant pour clef le nom des paramètres.

Finalement, il est possible d’utiliser le résultat de l’appel des méthodes de l’objet, le nom du paramètre doit alors correspondre au nom d’une méthode de l’objet. Ce mécanisme est permis grâce à « bindMethods » et à la réflexion.

Ajouter à cela que les méthodes « bindBean » et « bindMethods » permettent d’accéder aux propriétés d’objets composant l’objet racine. En imaginant que l’objet « Produit » ait pour attribut un objet « Fabricant », il est possible de lier les paramètres aux valeurs de l’objet de cette manière :

Mappers

Lors de la récupération du résultat d’une requête, les enregistrements sont très souvent transformés en objet. Cette transformation est effectuée par l’instruction « .map() » comme illustré lors de la récupération de l’objet « soda » dans le chapitre « Premières requêtes ».

JDBI permet l’externalisation de ces opérations de mapping afin d’éviter de les définir à chaque instruction SQL.

JDBI distingue deux types de mapper, les « RowMapper » permettant de transformer une ligne du résultat de la requête en un objet et les « ColumnMapper » permettant de transformer une colonne de la ligne courante du résultat.

RowMapper

Les « RowMapper » sont de simples classes implémentant l’interface « RowMapper » et ayant une seule méthode « map ». La méthode « map » transforme une ligne de résultat en un objet du type défini par et pour ce mapper.
Exemple avec l’objet « Produit » :

Pour que l’association soit effective, il faut déclarer lors de la configuration de l’instance JDBI que le « ProduitRowMapper » permet de transformer une ligne de résultat en un objet « Produit » :

La récupération du produit « soda » peut être réécrite de la manière suivante :

ColumnMapper

Les « ColumnMapper » sont similaires aux « RowMapper » et permettent de transformer une colonne depuis le type de l’enregistrement de la base de données vers un type Java.
Cependant leur intérêt est plus limité car ils ne peuvent s’appliquer qu’aux requêtes contenant une seule colonne dans leurs résultats. Dans le cas d’une requête ayant plusieurs colonnes, le mapper ne s’appliquera qu’à la première colonne.
Les cas d’usages peuvent concerner la conversion de données monétaires ou encore la transformation de valeurs d’énumération stockées sous forme de chaîne de caractères ou numérique.

Récupérer un graphe d’objets

Les jointures sont des opérations basiques en SQL. Mais lorsque qu’il s’agit de transformer un modèle relationnel en graphe d’objets, les problèmes commencent.

JDBI permet de récupérer tout un graphe d’objets à partir d’une seule requête, grâce à un mécanisme de « reduce » et d’un accumulateur.

Pour récupérer toutes les listes de courses, avec pour chaque liste, la collection des éléments la composant et les produits associés, on utilise la requête suivante :

Grâce à une jointure sur la relation « 1..n » entre l’objet « ListeCourse » et l’objet « Produit », la requête va « exploser » toutes les composantes de chaque liste de course avec autant de lignes pour chaque « ListeCourse » que d’éléments la constituant.

Grâce à l’utilisation de la méthode « reduceRows » sur le résultat de la requête, il est possible de créer un graphe d’objets complet.

Voici le détail des différentes étapes du traitement :

1. On indique à l’API l’enregistrement d’un mapper générique avec préfixe. Ce préfixe sert à déterminer l’appartenance d’un champ du résultat de la requête à un objet à créer. Ici, tous les champs préfixés par « lc_ » appartiennent à un objet « ListeCourse »

2. Cette instruction définit le traitement de « réduction » du résultat de la requête. L’accumulateur est initialisé avec une « Map » stockant les différentes listes de courses avec pour clef leur identifiant.

3. La liste de courses correspondante est extraite de la ligne du résultat si inconnue de l’accumulateur, ou récupérée de la map si connue.

4. L’item de la liste de courses est extrait de la ligne de résultat.

5. Le produit est également extrait de la ligne de résultat puis est assigné à l’item correspondant.

6. L’item est ajouté à la liste de courses courante.

7. L’accumulateur complété est retourné pour être fourni au parcours des lignes suivantes.

8. En fin de traitement, les listes de courses sont récupérées sous la forme d’une collection (ici une liste).

Interface DAO

JDBI possède un plugin nommé « SQLObject » afin de définir les opérations élémentaires des DAO. Les DAO prennent la forme d’interfaces spécifiant des méthodes publiques ainsi qu’une opération SQL associée pour chaque méthode.
L’exemple suivant montre comment définir et utiliser une interface DAO :

On retrouve dans la classe précédente les annotations suivantes :
• @SqlQuery et @SqlUpdate : de manière similaire aux méthodes « createQuery » et « createUpdate » de l’objet « Handle », ces deux annotations permettent de déclarer les instructions SQL et leurs types d’opérations (lecture, écriture).
• @RegisterRowMapper : cette annotation permet d’associer le « mapper » nécessaire à la transformation des lignes du « ResultSet » SQL en objets Java.
• @Bind : cette annotation permet de faire le lien entre le paramètre de la méthode et le paramètre de la requête SQL correspondante. Il est possible de se passer de cette annotation si le code source est compilé avec le paramètre « -parameters ». Cette option de compilation a pour effet de conserver le nom des paramètres remplacés habituellement par « arg0 », « arg1 », …, « argn » à la compilation.
• @BindBean : l’annotation permet de faire le lien entre les attributs d’un objet et les paramètres de la requête.

L’utilisation des DAO est possible après l’avoir lié à un objet « Handle » :

Externaliser les requêtes SQL

Que l’on utilise les méthodes de l’objet « Handle » ou les interfaces DAO, les requêtes SQL sont écrites directement dans le code.
Il est possible d’externaliser dans des fichiers de ressources toutes les requêtes SQL. Il est même envisageable de modifier les requêtes « à chaud » car JDBI embarque un système de cache invalidant les requêtes un certain temps après le dernier accès.
Pour récupérer simplement une requête grâce à ce mécanisme, on utilise la classe « ClasspathSqlLocator » :

La requête de l’exemple ci-dessus est récupérée du fichier « get.sql » se situant dans le dossier de ressources « fr/osaxis/jdbi/modele/Produit/ ». Ce dossier doit être accessible depuis le classpath de l’application.

Pour une utilisation avec les interfaces DAO, il suffit d’annoter l’interface avec @UseClasspathSqlLocator :

Transactions

Les méthodes « useHandle » et « withHandle » n’ont pas de notion de transaction (hormis celles implicites, gérées par le SGBD).

Ces deux méthodes permettent simplement d’ouvrir une connexion à la base (ou d’en récupérer une depuis le pool de connsexions), et de l’utiliser et de la relâcher (fermeture ou remise à disponibilité du pool).

Pour les besoins transactionnels, l’objet « Handle » propose deux méthodes similaires à « useHandle » et « withHandle » qui sont « useTransaction » et « inTransaction ».

Ainsi, une erreur lors de l’exécution du bloc annule la transaction et aucune donnée n’est insérée.

Dans le cas de l’utilisation des classes DAO gérées par l’extension SqlObject, une annotation est disponible pour indiquer que l’opération doit s’effectuer dans une transaction :

Quelle que soit la méthode utilisée, il est possible d’indiquer le niveau d’isolation de la transaction. Il s’agit d’un paramètre supplémentaire aux méthodes « inTransaction » et « useTransaction » et de l’unique attribut de l’annotation @Transaction.

Autres fonctionnalités

JDBI possède d’autres fonctionnalités comme son intégration avec des librairies tierces. Sous forme de plugins, il faut déclarer leur utilisation lors de la création de l’instance JDBI :

Il existe des plugins spécifiques à quelques moteurs de bases de données (H2, Oracle, PostgreSQL par exemple) afin de profiter de fonctionnalités propres à ces SGBDs.

Autre exemple : un plugin Guava est disponible pour l’utilisation directe des collections et structures de données propre à cette librairie.

Le mot de la fin

JDBI est une librairie très complète qui permet de s’abstraire d’opérations fastidieuses par rapport à l’utilisation directe de l’interface JDBC.

Les différentes possibilités de récupération et transformation des données issues de requêtes pour en créer des objets Java en font une boîte à outils très pratique pour ceux qui ne souhaitent pas mettre en place une solution plus lourde comme un ORM.

Son API de bas niveau assure également un contrôle complet sur les paramètres JDBC ainsi qu’un contrôle fin des éléments du requêtage SQL.
Ressources

Site officiel JDBI et documentation : http://jdbi.org
Code source du projet JDBI : https://github.com/jdbi/jdbi

 

Il est possible de retrouver l’intégralité de cet article dans les numéros 227 et 228 du mois de mars et avril 2019 du magazine « Programmez ».