Connexion aux bases de données avec Exposed
Introduction
Exposed est un framework open source conçu pour s’intégrer à une base de données relationnelle. Entièrement écrit en Kotlin, il bénéficie du soutien de l’entreprise JetBrains à l’origine du langage Kotlin. Exposed est apparu au cours de l’année 2019 en tant qu’alternative plus légère aux frameworks de base de données utilisés dans l’écosystème Java, tels que Hibernate. Ce framework s’appuie sur la syntaxe du langage Kotlin pour simplifier et faciliter l’écriture des requêtes SQL.
Exposed se base sur la librairie JDBC. Cette dernière fournit une couche d’abstraction permettant la connexion aux différents systèmes de gestion de bases de données relationnelles. Chaque base de données relationnelle fournit des drivers implémentant la spécification JDBC, ce qui permet de découpler le code de l’application de la gestion de connexion vers une base données spécifique. Cependant, ce découplage se limite à la connexion et à l’exécution des requêtes. Les requêtes, quant à elles, restent propres à chaque système de base de données. En effet, même si tous ces systèmes utilisent le même langage « SQL », chacun...
Comparaison entre Exposed et Hibernate
Les deux frameworks offrent une méthode fiable pour découpler l’application de la base de données. En étant un langage de la JVM, Kotlin permet d’utiliser facilement tous les frameworks Java existants permettant la gestion de bases de données pour une application. Le choix du framework devrait donc être basé sur les avantages offerts par l’architecture sur laquelle il repose.
1. Maintenabilité
La maintenabilité du code de l’application est très importante. Elle implique la nécessité d’avoir un code clair et compréhensible et la capacité à déboguer l’application facilement en cas de problème. La richesse du framework Hibernate, ainsi que sa complexité, peuvent rapidement affecter la lisibilité du code de l’application. Bien que la représentation du schéma de base de données en mapping d’objet relationnel facilite le développement lorsque le schéma est simple, dès que celui-ci comporte plusieurs relations complexes entre les tables, la complexité du code de l’application augmente et le code SQL généré devient moins lisible. Cela rend le débogage plus complexe.
Grâce au paradigme DSL offert par le framework Exposed, le développeur conserve un contrôle complet...
Installation et configuration
1. Ajout des dépendances
Le framework Exposed est disponible sous forme de plusieurs dépendances. Parmi elles se trouvent :
-
exposed-core : il s’agit du module principal du framework. Il comporte les différentes implémentations des API du framework ainsi que la définition du langage DSL d’écriture des requêtes SQL.
-
exposed-jdbc : il s’agit du module de gestion des connexions aux bases de données. Il repose sur la bibliothèque Java JDBC.
-
exposed-dao : il s’agit du module de gestion du mapping objet relationnel. Il repose sur le patron de conception DAO. Ce module est optionnel.
Pour un projet Gradle, ces trois dépendances doivent être ajoutées comme suit :
implementation("org.jetbrains.exposed:exposed-core:0.45.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.45.0")
implementation("org.jetbrains.exposed:exposed-dao:0.45.0")
Dans le cadre d’un projet Maven, la configuration de ces dépendances est la suivante :
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-core</artifactId>
<version>0.45.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jdbc</artifactId>
<version>0.45.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-dao</artifactId>
<version>0.45.0</version>
</dependency>
Comme Exposed est basé sur JDBC, l’application doit ajouter la dépendance qui correspond au driver JDBC du système de base de données choisi. Par exemple, pour PostgreSQL, il faut ajouter la dépendance suivante :
-
Gradle
implementation("org.postgresql:postgresql:42.7.1")
-
Maven
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
</dependency>
2. Configuration de la connexion
Exposed interagit avec la base de données à travers la classe org.jetbrains.exposed.sql.Database. Cette classe est responsable de toutes les configurations relatives à l’établissement de la connexion avec une base de données. La connexion peut être établie en utilisant la méthode connect de cette classe. L’exemple ci-dessous illustre la configuration d’une connexion à une base de données PostgreSQL locale :
Database.connect("jdbc:postgresql://localhost:5432/
my_db", driver = "org.postgresql.Driver")
Le premier paramètre représente la configuration JDBC, le deuxième paramètre spécifie la classe principale du driver JDBC. Ces deux paramètres varient en fonction de la base de données choisie.
La bibliothèque JDBC définit l’interface java.sql.DataSource. Cette dernière permet de définir la configuration nécessaire pour établir...
Les bases du framework Exposed
1. Représentation des tables
a. Déclarer les tables
Exposed représente les tables d’une base de données à travers la classe org.jetbrains.exposed.sql.Table. Chaque instance de cette dernière est associée à une table dans le schéma de la base de données de l’application. Exposed repose sur le patron de conception singleton et utilise la syntaxe Kotlin pour la modélisation des différentes tables.
Prenons l’exemple de la classe métier suivante :
data class Team(id: Int, name: String)
Cette classe représente une équipe définie grâce à son identifiant, de type Int, et à son nom, de type String. Afin de gérer la persistance des données des équipes dans une base de données, l’application a besoin de définir un schéma de données contenant une table « teams ». Cette table peut être modélisée en utilisant Exposed par l’objet suivant :
import org.jetbrains.exposed.sql.Table
object Teams : Table("teams")
Le nom de la table est passé en tant que paramètre du constructeur de la classe Table. Il s’agit d’un paramètre optionnel, car Exposed utilise par défaut le nom de la classe ou de l’objet héritant de la classe Table. Les colonnes de la table et leurs types sont définis en tant que propriétés de l’objet comme suit :
object Teams : Table("teams") {
val id = integer("id")
val name = varchar("name", 128)
}
La table « teams » contient deux colonnes : id de type integer et name de type varchar. Les noms de ces propriétés peuvent différer de ceux des colonnes de la table dans la base de données. Par exemple, on peut remplacer le nom name par le nom teamName, comme l’illustre l’exemple ci-dessous :
val teamName = varchar("name", 128)
Dans l’exemple précédent, la colonne de cette table est donc nommée name tandis que le nom de la propriété correspondante est teamName. Le type de la propriété est Column<String>. Toutes les colonnes sont représentées par la classe générique org.jetbrains.exposed.sql.Column.
Contrairement au nom de la table, Exposed ne peut pas inférer le nom des colonnes à partir des noms des propriétés de l’objet.
Exposed prend en charge la gestion des différences dans les définitions des types entre les divers systèmes de gestion de base de données. Dans l’exemple précédent, la méthode integer définit une colonne contenant des valeurs entières. Exposed utilisera donc le type INT si l’application utilise PostgreSQL. Il utilisera le type NUMBER(12) si l’application utilise Oracle en tant que base de données. Cette abstraction permet de découpler la définition du schéma des données de l’application du système de gestion de données cible. Ce découplage permet de passer facilement d’un système à un autre sans avoir besoin d’adapter le code de l’application.
La clé primaire de la table est déclarée en redéfinissant la propriété primaryKey héritée de la classe mère Table. Cette propriété a comme type la classe org.jetbrains.exposed.sql.Table.PrimaryKey, son constructeur prend comme paramètre un ou plusieurs objets de type Column. Ces objets doivent correspondre aux colonnes définies dans la même classe. Par exemple, la déclaration de la colonne id comme clé primaire de la table « Team » peut se faire comme suit :
object Teams : Table("teams") {
val id = integer("id")
val name = varchar("name", 128)
override val primaryKey = PrimaryKey(id) ...
Gérer les jointures
Les jointures, désignées par la commande « JOIN » en langage SQL, représentent une notion fondamentale pour les bases de données relationnelles. Cette opération permet de fusionner des données réparties sur deux ou plusieurs tables en se basant sur les liens relationnels entre elles. Le résultat de cette fusion est une vue consolidée regroupant les données combinées des différentes tables. Le contenu de cette vue renvoyée dépend du type d’opération de jointure exécutée. Parmi ces différents types figurent la jointure interne, la jointure à droite, la jointure à gauche et la jointure complète.
-
La jointure interne, ou inner join, sélectionne uniquement les lignes des deux tables qui correspondent exactement aux conditions de jointure spécifiées.
-
La jointure à droite, ou right join, inclut toutes les lignes de la table situées à droite de l’opération de jointure, et uniquement les lignes correspondantes de la table à gauche. Les champs de la table de gauche qui n’ont pas de correspondance sont représentés par des valeurs nulles.
-
La jointure à gauche, ou left join, fonctionne de manière similaire à la jointure à droite. Cependant, elle utilise l’ordre inverse des tables. Elle inclut toutes les lignes de la table de gauche, et seulement les lignes correspondantes de la table de droite. Les champs de la table de droite qui n’ont pas de correspondance sont représentés par des valeurs nulles.
-
La jointure complète, ou full join, combine toutes les lignes des deux tables, que celles-ci trouvent une correspondance ou non. Les champs qui ne trouvent pas de correspondance dans l’autre table sont représentés par des valeurs nulles.
Ces différents types de jointures offrent plusieurs options de combinaisons de données des différentes tables utilisées par l’application.
1. Gérer les jointures avec la syntaxe DSL
Le framework Exposed prend en charge l’utilisation des jointures lors de l’emploi de la syntaxe DSL. Comme pour les autres types d’opérations, cette syntaxe adopte une forme très proche du langage SQL.
a. Les jointures internes
Les jointures sont implémentées par la méthode join de l’objet Table. Cette méthode permet d’exécuter tous les types des jointures entre deux tables. Elle prend en tant que paramètre le nom de la table cible, le type de la jointure ainsi que les deux colonnes permettant d’établir une relation entre les deux tables. Par exemple :
Teams.join(
otherTable = Players,
joinType = JoinType.INNER,
onColumn = Teams.id,
otherColumn = Players.teamId,
)
La requête de jointure précédente représente une jointure interne. Seules les données des joueurs et des équipes ayant une association sont renvoyées par la jointure. Le paramètre onColumn spécifie la colonne de la table source sur laquelle l’opération de jointure sera basée, dans l’exemple précédent, la jointure se base sur la colonne id de la table « Teams ». Tandis que le paramètre otherColumn spécifie la colonne de la table cible qui sera utilisée par l’opération de jointure pour identifier les lignes correspondantes. Dans notre exemple, il s’agit de la colonne teamId de la table « Players ». Cette colonne porte la relation d’association d’un joueur à son équipe.
Prenons comme exemple une base de données avec les données suivantes :
Teams :
- Id : 1, name : France
- Id : 2, name : Spain
Players
- id : 1, teamId : 1
- id : 2, teamId : 1
- id : 3, teamId : 4
Le résultat de la jointure interne définie précédemment correspond aux lignes suivantes :
- players.id...
Utilisation des fonctions SQL
La plupart des systèmes de gestion de base de données offrent des fonctions SQL intégrées pour simplifier la manipulation des données, l’agrégation des résultats et l’exécution de calculs directement à partir d’une requête SQL. Le framework Exposed permet d’exécuter ces diverses fonctions en utilisant sa syntaxe DSL.
À titre d’exemple, pour récupérer le nombre de lignes d’une table, en SQL, il est possible d’utiliser la fonction COUNT. La syntaxe DSL d’Exposed permet d’exécuter cette fonction de la manière suivante :
Teams.id.count()
Comme en SQL, cette fonction nécessite de spécifier la colonne à utiliser pour compter les enregistrements retournés par la requête. Dans l’exemple précédent, la colonne id sera utilisée pour calculer le nombre d’entités de la table « Teams ».
Par exemple, la fonction count peut être utilisée dans une requête de sélection comme suit :
Teams.select(Teams.id.count())
Toutefois, pour pouvoir lire le résultat de la requête, il est nécessaire de déclarer l’utilisation de la fonction dans une variable de la manière suivante :
val teamsCountById = Teams.id.count()
Cette variable peut alors être utilisée dans la requête afin d’exécuter la fonction et récupérer son résultat :
Teams.select(teamsCountById).map { it[teamsCountById] }
Le code précédent exécute une requête de sélection afin de compter le nombre de lignes dans la table « Teams » en utilisant...
Tester les requêtes
Les tests sont indispensables pour valider le bon fonctionnement du code de l’application. Cependant, il est souvent difficile d’écrire des tests pour les modules de l’application qui interagissent avec une base de données. Par conséquent, ces parties du code sont souvent peu ou mal testées. Pour remédier à cela, plusieurs librairies ont vu le jour. Parmi celles-ci figure la bibliothèque Java Testcontainers. Cette bibliothèque s’appuie sur Docker pour créer et gérer un conteneur contenant la base de données cible de l’application. Cette approche, différente de celle de la simulation des requêtes à l’aide d’une base de données en mémoire (H2 par exemple), permet de valider la connexion et les différentes requêtes en utilisant le même type de base de données que celui de l’environnement de production. Cela rend les tests plus fiables et cohérents avec les conditions réelles.
Pour intégrer Testcontainers à un projet, il suffit d’ajouter la dépendance de test suivante :
-
Gradle
testImplementation("org.testcontainers:testcontainers:1.19.7")
-
Maven
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
Comme cette bibliothèque se base sur Docker, elle nécessite que Docker soit installé sur la machine de développement ainsi que toutes les machines de la chaîne d’intégration continue (CI).
Dans le cas où les tests sont lancés dans un conteneur Docker, il est nécessaire d’utiliser une image de base « Docker in Docker » pour pouvoir démarrer un conteneur Docker depuis celui de la machine des tests.
La bibliothèque Testcontainers repose...
Monitoring des requêtes
1. Activation des logs
Lors de l’utilisation d’un ORM ou d’un framework de gestion de base de données, il est souvent nécessaire de récupérer la requête SQL générée par le framework. Cette opération est importante afin de déboguer les requêtes SQL complexes.
Le framework Exposed offre la possibilité d’afficher sous forme de logs l’ensemble des requêtes SQL générées pendant une transaction. Ces logs peuvent être activés à travers la méthode d’extension addLogger de l’objet Transaction :
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.addLogger
transaction {
addLogger(StdOutSqlLogger)
}
L’exemple précédent passe l’objet StdOutSqlLogger à la méthode addLogger. Cet objet est une implémentation de logs basique. Il se base sur la méthode println pour afficher les requêtes SQL dans la console de l’application. Il existe d’autres objets de gestion des logs tels que Slf4jSqlDebugLogger. Ce dernier repose sur la bibliothèque de logging SLF4J pour écrire des messages de logs pour chaque requête SQL générée durant la transaction. Comme son nom l’indique, ces messages sont écrits avec le niveau de log DEBUG.
Prenons comme exemple la transaction suivante :
transaction {
addLogger(StdOutSqlLogger)
Teams.select(Teams.name).where { Teams.id eq 1}.toList()
}
Le message de log généré par la requête précédente est le suivant :
SQL: SELECT teams.name from teams WHERE teams.id = 1
De plus, l’application peut fournir sa propre implémentation du système de logs. Pour cela, il suffit d’implémenter l’interface org.jetbrains.exposed.sql.SqlLogger. Par exemple :
class CustomSqlLogger : SqlLogger...
Conclusion
Exposed est un framework Kotlin conçu pour simplifier la connexion aux bases de données relationnelles et faciliter l’exécution des diverses opérations CRUD depuis l’application. La flexibilité offerte par ses deux approches syntaxiques, DAO et DSL, permet de s’adapter à diverses architectures applicatives. De plus, ces deux approches se complètent et peuvent être combinées afin de formuler des requêtes SQL complexes. Bien qu’il soit un framework récent, Exposed constitue une alternative viable aux ORM Java classiques tels que Hibernate.