Utiliser une base de données
Dans cette section, nous verrons comment utiliser une base de données avec Java et nous réaliserons nos DAO.
Préparation
Pour la suite de ce tutoriel, nous utiliserons une base de données comportant les tables : - travel, - user, - reservation (pour la relation d'un utilisateur inscrit pour un voyage) - wait_list (pour la relation d'un utilisateur inscrit dans la file d'attente d'un voyage).
Récupérez les scripts de la base de données
-
Téléchargez les scripts d'initialisation :
Cette archive contient quatre scripts :
db/V1.0.0__travel_user.table.sql
: Définit les tablesTravel
etUser
.db/V1.0.1__travel_user.data.sql
: Ajoute quelques entrées aux tables mentionnées ci-dessus
-
Placez les scripts dans un nouveau dossier appelé
sql
, à la racine de votre répertoire de travail.
Mise en place de la base de données
Nous allons utiliser une base de données PostgreSQL.
Pour des raisons de simplicité, la base de données tourne dans un conteneur Docker et nous vous fournissons tous les fichiers d'installation.
-
Installez Docker Engine.
Vous pouvez ignorer cette étape si Docker est déjà installé sur votre système. Dans le cas contraire, voici comment procéder à son installation : installer Docker Engine
-
Téléchargez les fichiers
docker-compose
et placez-les à la racine de votre projet : Download docker-compose-dbCette archive contient trois fichiers :
docker-compose.yml
: Définit un servicedb
qui démarre une instance PostgreSQL, initialisée avec les scripts que nous avons placés précédemment dans le dossiersql
.docker-compose.override.yml
: Définit les paramètres Docker pour le développement, expose la base de données au port5432
et génère un Adminer pour gérer la base de données avec une GUI.
-
Exécutez la base de données avec la commande
docker compose
. -
Une fois l'exécution réussie, vous pouvez vous connecter à la base de données à l'aide de l'Adminer sur http://localhost:18080 en utilisant :
- nom d'utilisateur=madmin
- mot de passe=madmin
- nom de la base de données=agencymanagement_db Ce sont les valeurs par défaut spécifiées dans le fichier docker-compose.
Assurez-vous d'avoir quatre tables : travel
, user
, reservation
et wait_list
.
JDBC
La connexion à une base de données avec Java est normalisée par la spécification JDBC pour "Java Database Connectivity".
C'est une API fondamentale pour la connectivité des bases de données dans l'écosystème Java. Elle offre les outils nécessaires pour établir des connexions, exécuter des requêtes SQL, récupérer et mettre à jour des données dans les bases de données, tout en gérant efficacement les connexions.
Info
- Pour faire simple, JDBC offre un objet
Connection
, ainsi que le strict minimum pour exécuter des instructions SQL (Statement
,PreparedStatement
). - Dans des projets plus avancés, on utilise des bibliothèques avancées telles que Hibernate ou jOOQ au lieu du simple JDBC pour gérer nos bases de données SQL.
Installez le pilote JDBC / JDBC Driver
JDBC est compatible avec les SGBD (Systèmes de Gestion de Base de Données) les plus populaires.
- Ajoutez la dernière dépendance
PostgreSQL
à votrepom.xml
.
Affichez le pom.xml résultant
Le pilote JDBC est une bibliothèque qui fournit l'implémentation spécifique pour se connecter à un SGBD. En d'autres termes, il est nécessaire à l'exécution mais n'est jamais référencé directement dans le code. Par conséquent, la portée de Maven pour les pilotes JDBC doit être runtime
.
JDBC Connection
JDBC définit un objet Connection
qui représente une seule connexion à la base de données. Vous pouvez obtenir cet objet Connection
avec l'instruction suivante :
public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/agencymanagement_db", "user", "password");
}
Nous allons utiliser l'objet Connection
à plusieurs endroits, mais comme il est préférable de ne pas copier-coller cette méthode dans tout le code, nous allons définir une classe ConnectionManager
pour s'en occuper.
Le singleton ConnectionManager :
Nous n'avons besoin que d'un seul ConnectionManager
pour gérer l'objet Connection
. Pour ce cas d'utilisation,
le design pattern Singleton convient parfaitement.
- Créez une nouvelle classe appelée
ConnectionManager
. Cette classe utilise le modèle singleton. - Ajoutez une méthode appelée
getConnection()
permettant de récupérer la connexion.
Tip
Comme l'a popularisé Joshua Block dans son livre Effective Java, il est recommandé d'écrire des singletons à l'aide des Java enums.
package io.takima.agencymanagement
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public enum ConnectionManager {
INSTANCE;
private final String JDBC_URL = "jdbc:postgresql://localhost:5432/agencymanagement_db";
private final String JDBC_USER = "madmin";
private final String JDBC_PASSWORD = "madmin";
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL,JDBC_USER,JDBC_PASSWORD);
}
}
DAO
La récupération et le mapping des données de la base de données vers Java doivent être effectués par des DAO. Comme on l'a déjà mentionné, les DAO (Data Access Objects) servent d'interface intermédiaire entre la logique métier et la base de données. Leur rôle principal est de simplifier l'accès aux données en isolant la logique d'accès, formant ainsi une couche de persistance.
Étant donné que des DAO vous ont été fournis lors du jour 1, c'est maintenant à votre tour de créer vos propres DAO.
PreparedStatement
L'objet Connection
offre plusieurs méthodes pour exécuter des requêtes SQL. Dans cette étape, nous allons utiliser
Connection.prepareStatement
.
Pourquoi privilégier les PreparedStatement plutôt que les Statement ?
Contrairement à la classe Statement
, la classe PreparedStatement
permet de définir des paramètres de manière dynamique et offre une protection contre les injections SQL. De plus, elle est mise en cache par le SGBD, ce qui la rend plus efficace lorsqu'elle est exécutée plusieurs fois.
Codons ensemble la fonction nous permettra d'insérer des données, on fera ça pour un travel.
public void save(Travel travel) throws SQLException {
// Récupérer la connexion
Connection connection = cm.getConnection();
// Ecriture de la requête SQL
PreparedStatement ps = connection.prepareStatement("INSERT INTO travel(id, name, departure_airport, " +
" arrival_airport, departure_date, arrival_date, destination, capacity, price) " +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
// Attribution des paramêtres
ps.setLong(1, travel.getId());
ps.setString(2, travel.getName());
ps.setString(3, travel.getDepartureAirport().getAcronym());
ps.setString(4, travel.getArrivalAirport().getAcronym());
// Il faut transformer le Instant en Timestamp
ps.setTimestamp(5, java.sql.Timestamp.from(travel.getDepartureDate()));
ps.setTimestamp(6, java.sql.Timestamp.from(travel.getArrivalDate()));
ps.setString(7, travel.getDestination());
ps.setInt(8, travel.getCapacity());
ps.setDouble(9, travel.getPrice());
// Exécution de la requête
ps.executeQuery();
}
À vous de jouer
- Créez une classe nommée
JdbcTravelDao
qui utilise JDBC et implémente les fonctions suivantes :
On veut commencer par lister tous les voyages proposés par notre agence.
- Créez la méthode
List<Travel> findAll();
. - Créez une
PreparedStatement
pour sélectionner tous les voyages. - Créez la méthode
List<Travel> findTravelsByDestination(String destination)
pour lister tous les voyages ayant une destination précise. - Exécutez cette instruction, itérez sur le
ResultSet
et imprimez les noms des voyages.
Indice
Note
- Envisagez de nommer une méthode findById() au lieu de geById() :
N'appelez pas la méthode getById() si elle risque de ne pas donner de résultat. Si la méthode renvoie un résultat null dans le cas d'une ressource non trouvée, il est préférable d'appeler la méthode findById().
- Renvoyez un Optional
plutôt que null :
Si une méthode renvoie null, vous devez vérifier la nullité à chaque fois que vous appelez la méthode. Vous prenez le risque d'oublier cette vérification et d'avoir une NullPointerException
.
- N'oubliez pas de fermer les ressources une fois qu'elles ne sont plus nécessaires :
Comme indiqué dans la documentation, la fermeture de la Connection
ferme également les PreparedStatement
et ResultSet
associés.
- N'oubliez pas de fermer la
Connection
en cas d'exception également. CommeConnection
implémenteAutoClosable
, la méthode try-with-resources est parfaitement adaptée à ce cas d'utilisation.
À vous de jouer
- À votre tour, implémentez
List<User> findAllParticipantsByTravelId(long travelId);
pour lister tous les voyageurs inscrits à un voyage précis. - Respectez la structure proposée lors du premier jour et créez votre service
TravelService
, utilisant les fonctions du DAO. - Testez votre code en appelant le service à partir de votre méthode main.
Connection pool
Dans les applications d'entreprise réelles, la base de données a souvent beaucoup de lecture/écriture à faire et peut devenir un "goulot d'étranglement".
Pour atténuer ce problème, nous utilisons souvent plusieurs objets Connection
pour équilibrer la charge.
Avec votre code actuel, il serait facile d'appeler DriverManager.getConnection(jdbcUrl)
à chaque fois que vous en avez besoin, mais cela pose un certain nombre de problèmes :
- Il est difficile de contrôler le nombre de connexions ouvertes.
- Un trop grand nombre de connexions ouvertes ralentit l'application.
- L'ouverture et la fermeture d'une connexion est un processus lourd.
Idéalement, nous voulons ouvrir un nombre fixe de connexions et les réutiliser à l'infini.
C'est exactement ce à quoi sert un connexion pool.
HikariCP
HikariCP est en effet un pool de connexions pour Java, utilisé pour gérer et optimiser l'utilisation des connexions à une base de données.
À vous de jouer
- Ajoutez la dépendance HikariCP à votre
pom.xml
.
Afficher le pom.xml résultant
- Modifiez le
ConnectionManager
pour utiliser HikariCP.
Service / DAO
Maintenant que nous avons compris comment les interactions avec notre base de données fonctionnent, revenons à notre diagramme. Vous remarquerez que les DAO sont appelés uniquement par leur service correspondant. D'autre part, les services peuvent être appelés entre eux.
Exemple :
Nous avons une fonction findAll
dans notre DAO. Pour pouvoir l'utiliser dans le service, il faut faire :
À vous de jouer
- Pour toutes les fonctions créées dans votre service (durant le jour 1), réalisez une fonction DAO que vous appellerez selon vos besoins :
Fichiers modifiés
docker-compose.yml
docker-compose.override.yml
pom.xml
src/main/java/io/takima/agencymanagement/Application.java
N'oubliez pas de Commit
votre travail !