Accéder au contenu principal

Comment manipuler les données d'une base de données avec Java et Springboot

Accéder aux bases de données pour en manipuler les données est une fonctionnalité commune dans diverses applications. Avec Java il existe plusieurs techniques pour manipuler et accéder aux données d'une base de données. Et ces techniques sont encore plus simples en utilisant le framework spring et plus particulièrement sa mouture springboot.

Dans le tutoriel ci dessous nous allons voir comment créer, modifier supprimer ou lire les données d'une base de données rapidement et sans effort.



1. Qu'est ce que JPA (Java Persistence API)

JPA est une API mis en place n java qui permet de mapper un POJO (Plain Old Java Object) ou Java Bean vers une ou plusieurs tables d'une base de données. L'objectif de JPA étant de simplifier l'exécution des requêtes SQL dans la base de données. JPA met à disposition du développeur un PersistenceContext et aussi fait le mapping entre l'objet java et la table en base de données via des annotations.

Schématiquement, JPA a pour but de sauvegarder comme tel des POJO dans la base de données et de retourner des données de la base de données sous forme de POJO. Toute la mécanique sous-jacente est transparente pour le développeur.

La connection à la base de données, la création des statements, la création des resulset, et leurs fermetures respectives sont toutes déléguées à l'API donc à la charge de toute implémentation de cette API.

Plusieurs implémentations de l'API JPA existent:

2. Qu'est ce qu'un ORM (Object Relational Mapper)

Un ORM est un framework ou librairie permettant de faire le mapping entre un bean Java (POJO) et une table de base de données. Le concept dans l'absolu est le même que celui exposé par la spécification JPA. Les ORM sont nés bien avant la spécification JPA et proposaient des approches et des modes d'utilisation totalement différents.

Ensuite est arrivé la spécification JPA qui a en quelque sorte "unifié" les modes d'utilisation des ORM pour l'intéraction avec la base de données.

Utiliser un ORM ou JPA pour être le plus abstrait possible, vous facilitera grandement les actions de sauvegarde, modification, suppression et lecture de données dans une base de données.

3. Qu'est ce que Spring Data

Spring Data est une couche d'abstraction mis en place par le framework spring et qui a pour but de d'uniformiser et de faciliter l'utilisations des technologies sous-jacentes telles que: JPA, pur JDBC, Document Data, Indexed Data, etc...

L'une des forces de Spring Data est qu'avec une convention de nommage des noms de méthodes, il est capable de générer des requêtes JPQL / SQL qui sont ensuite exécutées dans la base de données. Cette fonctionnalité accélère grandement les accès aux bases de données.

Dans la suite du tutoriel nous utiliserons Spring Data JPA

4. Les dépendances

Nous allons créer une API CRUD qui nous permettra de créer, modifier, Sélectionner ou Supprimer les données d'une base de données

A l'aide de Spring Initializr, nous créons notre projet qui utilisera: Spring Data JPA, Spring Web

Dépendances Gradle

plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.javatutorialshub.market'
version = '1.0-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}


repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
useJUnitPlatform()
}


Dépendances Maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.javatutorialshub.market</groupId>
<artifactId>market-instument</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>market-instument-crud-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

5. La configuration

L'autoconfiguration de springboot, nous facilite pleinement la tache, en matière de configuration. En effet, il suffit dans le fichier application.yaml (ou application.properties) de déclarer les propriétés d'accès à la base de données:

spring:
datasource:
url: ${APP_DB_URL:jdbc:postgresql://localhost:5432/instrument-db}
username: ${APP_DB_USER:instrument-db-user}
password: ${APP_DB_PASSWORD:instrument-db-pass}

Avec cette configuration, springboot créera l'EntityManagerFactory (autrement dit PersistenceContext) automatiquement afin d'exécuter des requêtes dans la base de données.

Ensuite, il nous faudra nécessairement gérer les transactions dans notre application, dans le cas contraire, Spring Data JPA considèrera que toutes nos actions sont en auto-commit. De ce fait, dans notre classe de configuration:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.javatutorialshub.market"})
@EntityScan(basePackages = "com.javatutorialshub.market")
@ComponentScan(basePackages = {"com.javatutorialshub.market"})
public class Config {
@Bean
@Primary
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}

Dans cette configuration, nous indiquons le package à scanner pour les entités (@EntityScan), pour les composants (@ComponentScan) et pour les repository (@EnableJpaRepositories)

6. Les entités

La première étape pour effectuer des opérations avec la base de données est la création des entités, car nous utilisons un ORM via la spécification JPA, donc il nous faut mapper nos tables de base de données en objets java (POJO)

@Entity
@Table(name = "tbl_quote")
@Data
@NoArgsConstructor
public class Quote {
@Id
@Column(name = "quote_pk")
private Long id;

@Column(name = "quotation_date")
private LocalDateTime quotationDate;

@Column(name = "quotation_price")
private Double price;
@ManyToOne
@JoinColumn(name = "stock_fk")
private Stock stock;
}

@Entity: il s'agit de l'annotation la plus importante, car elle permet de marquer une classe java comme permettant le mapping entre les champs de la classe et les colonnes de la table

@Table: Cette annotation permet d'indiquer le nom de la table concernée dans la base de donnée. Si cette annotation est absente, l'implémentation de JPA utilisée (en l'occurence Hibernate dans notre cas) tentera de créer/trouver une table ayant le même nom que la classe Java.

@Id: Toute entité doit indiquer le champ qui sera pris comme étant l'identifiant de l'entité.

@Column: Permet d'indiquer le mapping entre la colonne de la table en base de données et le champ de la classe java. si cette annotation est absente, alors une colonne du meme nom que le champ de la classe sera attendue.

Ensuite dans la création des entités, il est primordial d'indiquer les relations (associations) entre elles afin que cela soit matérialisé en base de donnée par des relation entre table dans une base de données relationnelle. Dans notre exemple l'entité Quote concerne une et une seule entité Stock, alors que cette dernière possède une list de Quote. D'où l'utilisation de la relation: @ManyToOne combiné avec le nom de la colonne de jointure: @JoinColum.

@Entity
@Table(name = "tbl_stock")
@Data
@NoArgsConstructor
public class Stock {
@Id
@Column(name = "stock_pk")
private Long id;
@Column(name = "isin")
private String isin;

@Column(name = "name")
private String name;

@Column(name = "description")
private String description;
@OneToMany(mappedBy = "stock")
private List<Quote> quotes;
}

@OneToMay: Cette annotation permet d'indiquer qu'une entité est associée à une autre via une relation de 1 à plusieurs. Il est alors obligatoire que le champ soit de type collection (List, Set, etc...)

@ManyToOne: Il s'agit de la relation inverse de la précédente. Qui indique que plusieurs entités de la même classe peut être associées à une autre entité.

JPA définie plusieurs autres associations que vous trouverez ici: OpenJPA documentation

7. Les Repository

La seconde étape consiste à créer les repository pour chaque entité et qui serviront en quelque sortes de DAO (Data Access Object). Comme nous utilisons Spring Data JPA, nous repository seront des interfaces simples qui hériterons de l'interface JpaRepository en indiquant l'entité concernée par ce repository et le type du champ Id.

public interface QuoteRepository extends JpaRepository<Quote, Long> {
}

public interface StockRepository extends JpaRepository<Stock, Long> {
}

De plus avec Spring Data, aucun besoin d'annoter la classe repository avec l'annotation @Repository, car grace à @EnableJpaRepositories(basePackages = {"com.javatutorialshub.market"}) les interfaces repository seront pris en compte.

En l'état, les repository sont utilisables pour savegarder, supprimer, mettre à jour  ou sélectionner plusieurs éléments sans aucune implémentation des repository, comme nous allons le voir au niveau des services.

8. Les services

Dans cette couche, nous allons mettre en oeuvre l'utilisation des repository pour l'accès aux base de données. Pour ce faire, nous utiliserons MapStruct (que nous verrons en détail dans un autre tutoriel) qui nous aideras à mapper nos DTO (Data Transfer Object: ce sont des objets qui collecterons l'entrée saisie par les utilisateurs).


@Service
@RequiredArgsConstructor
@Slf4j
public class CreateStockService {
private final StockRepository stockRepository;

public StockDTO create(StockDTO stockDTO) throws CreateStockException {
try {
Stock stock = StockMapper.MAPPER.toStock(stockDTO);
Stock result = stockRepository.save(stock);
return StockMapper.MAPPER.toStockDTO(result);
} catch (Exception e){
String message = "Unable to create the stock due to:" + e.getMessage();
log.warn(message);
throw new CreateStockException(message, e);
}
}
}

Dans l'exemple ci-dessus, le service CreateStockService utilise un le repository StockRepository et fait appel à sa méthode save, qui sauvegardera l'objet stock en base de donnée. Aucunement, nous n'avons eu besoin d'implémenter la méthode save et tout fonctionne.

De même si nous souhaitons retourner un stock en utilisant son Id comme paramètre d'entrée, nous n'avons rien à faire au niveau du repository. Le service ne fera qu'invoquer la méthode findById disponible dans le repository.

@Service
@RequiredArgsConstructor
@Slf4j
public class FindStockByIdService {
private final StockRepository stockRepository;

public StockDTO find(Long id) throws StockNotFoundException, FindStockByIdException {
try {
Optional<Stock> result = stockRepository.findById(id);
if(result.isPresent()) {
return StockMapper.MAPPER.toStockDTO(result.get());
} else {
throw new StockNotFoundException("stock with id:" + id + " is not found");
}
} catch (StockNotFoundException e) {
throw e;
} catch (Exception e){
String message = "Unable to find the stock due to:" + e.getMessage();
log.warn(message);
throw new FindStockByIdException(message, e);
}
}
}

Il faut noter que les repository ne peuvent sauvegarder, modifier ou retourner des entités, dans la très grande majorité des cas. Dans certains cas, il est possible de sélectionner et retourner des objects qui ne sont pas forcément représentés comme étant des entités.

Dans le cas de l'update d'une entité, nous utiliserons la même méthode save(...) du repository, car cette dernière permet d'exécuter une requête insert ou update. Seulement il nous utiliser un marqueur pour indiquer à Spring Data quand faire un insert et quand faire un update. Ce marqueur est matérialisé par l'interface Persistable que doivent implémenter les entité comme suit:

public abstract class AbstractEntity<I> implements Persistable<I> {

private boolean isNew = true;

@Override
public boolean isNew() {
return isNew;
}

public void markNotNew() {
isNew = false;
}
}

@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "tbl_quote")
@Data
@NoArgsConstructor
public class Quote extends AbstractEntity<Long> {
@Id
@Column(name = "quote_pk")
private Long id;

@Column(name = "quotation_date")
private LocalDateTime quotationDate;

@Column(name = "quotation_price")
private Double price;

@ManyToOne
@JoinColumn(name = "stock_fk")
private Stock stock;
}

@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "tbl_stock")
@Data
@NoArgsConstructor
public class Stock extends AbstractEntity<Long> {
@Id
@Column(name = "stock_pk")
private Long id;

@Column(name = "isin")
private String isin;

@Column(name = "name")
private String name;

@Column(name = "description")
private String description;

@OneToMany(mappedBy = "stock")
private List<Quote> quotes;
}

Ensuite dans le service UpdateStockService, nous invoquerons la méthode markNotNew() avant l'appel à la méthode save(...)

@Service
@RequiredArgsConstructor
@Slf4j
public class UpdateStockService {
private final StockRepository stockRepository;

public StockDTO update(StockDTO stockDTO) throws UpdateStockException {
try {
Stock stock = StockMapper.MAPPER.toStock(stockDTO);
stock.markNotNew();
Stock result = stockRepository.save(stock);
return StockMapper.MAPPER.toStockDTO(result);
} catch (Exception e){
String message = "Unable to update the stock due to:" + e.getMessage();
log.warn(message);
throw new UpdateStockException(message, e);
}
}
}

La suppression quant à elle se fera la plus part du temps via l'Id de l'entité. De ce fait, nous appelons la méthode deleteById déjà disponible dans le repository comme suit:

@Service
@RequiredArgsConstructor
@Slf4j
public class DeleteStockByIdService {
private final StockRepository stockRepository;

public void delete(Long id) throws DeleteStockByIdException {
try {
stockRepository.deleteById(id);
} catch (Exception e){
String message = "Unable to delete the stock due to:" + e.getMessage();
log.warn(message);
throw new DeleteStockByIdException(message, e);
}
}
}

9. Les conventions de nommage de Spring Data JPA

Imaginons maintenant que nous souhaitions maintenant sélectionner tous les stock par le nom. Cette fois ci, nous déclarons la méthode, findByName dans le repository comme suit:

public interface StockRepository extends JpaRepository<Stock, Long> {
Collection<Stock> findByName(String name);
}

Vu que la méthode suit le formalisme: 

  • Find (pour indiquer qu'on veut sélectionner)
  • By (pour indiquer le nom du champ dans la classe d'entité)
  • Name (le nom du champ dans la classe d'entité)
Spring Data JPA génèrera tout seule la requête à exécuter via l'EntityManager dans la base données.

De même si nous souhaitons, sélectionner tous les stocks par le nom et la description. Nous utilisons le même formalisme, pour la méthode findByNameAndDescription:

public interface StockRepository extends JpaRepository<Stock, Long> {
Collection<Stock> findByName(String name);
Collection<Stock> findByNameAndDescription(String name, String description);

} 

Spring Data JPA défini une palette importante de formalisme pour l'écriture des noms de méthodes. Nous pouvons trouver cette documentation ici: Spring Data JPA Query Methods

10. Les requêtes personnalisées

Parfois, il arrive que les conventions de nommage prévues par Spring Data JPA ne suffisent pas définir correctement la requête qui convient à notre besoin. Dans ce cas, il est tout à fait possible d'utiliser l'annotation @Query au dessus d'une méthode de repository afin d'indiquer exactement la requête à exécuter.

public interface QuoteRepository extends JpaRepository<Quote, Long> {
@Query("select sum(q.price) from Quote q where q.quotationDate = :dateTime")
Double sumAllQuotationsAtDate(@Param("dateTime") LocalDateTime dateTime);
}

Dans le repository ci-dessus, nous indiquons la requête à exécuter à l'appel de la méthode sumAllQuotationsAtDate, via l'annotation @Query. Quant à l'annotation @Param, elle permet de mapper les paramètres de la méthodes avec les paramètres dans la requête JPQL. Nous pouvons aussi écrire des requêtes SQL avec l'annotation @Query. Dans ce cas, il faudrait l'indiquer avec l'attribut nativeQuery = true.

En cas de requête de modification de la données en base de données (UPDATE, DELETE, ou INSERT), il est nécessaire d'adjoindre l'annotation @Modifiying à l'annotation @Query

11. Les transactions

Nous reviendrons en détail dans un tutoriel dédié aux transactions. Par contre pour l'instant, nous devons retenir qu'afin que les modifications, les créations et les suppressions de nos entités soient effectives dans la base de données, il nous faut annoter les méthodes de nos services avec l'annotation @Transactional.

Toute méthode, annotée avec cette annotation, exécutera un "commit" dans la base de données lorsque tout ce passe bien, et fera un "rollback" en cas d'exception.

Il est possible d'indiquer les classes d'exception, pour lesquelles nous souhaitons un rollback. En l'absence de cette indication, toute exception engendrera un "rollback" dans la base de données.

Il est même très fortement conseillé d'annoter aussi les méthode de selection, de lecture de données dans les services avec l'annotation @Transactional, mais en indiquant qu'il s'agit de méthode readOnly.

Attention à bien sélectionner l'annotation @Transactional de Spring Data et non celle de JPA (Jakarta Persistence). Car en effet, nous souhaitons que les transactions soient gérées automatiquement par Spring Data.

@Service
@RequiredArgsConstructor
@Slf4j
public class CreateStockService {
private final StockRepository stockRepository;

@Transactional(rollbackFor = Exception.class)
public StockDTO create(StockDTO stockDTO) throws CreateStockException {
try {
Stock stock = StockMapper.MAPPER.toStock(stockDTO);
Stock result = stockRepository.save(stock);
return StockMapper.MAPPER.toStockDTO(result);
} catch (Exception e){
String message = "Unable to create the stock due to:" + e.getMessage();
log.warn(message);
throw new CreateStockException(message, e);
}
}
}


@Service
@RequiredArgsConstructor
@Slf4j
public class FindStockByIdService {
private final StockRepository stockRepository;

@Transactional(readOnly = true)
public StockDTO find(Long id) throws StockNotFoundException, FindStockByIdException {
try {
Optional<Stock> result = stockRepository.findById(id);
if(result.isPresent()) {
return StockMapper.MAPPER.toStockDTO(result.get());
} else {
throw new StockNotFoundException("stock with id:" + id + " is not found");
}
} catch (StockNotFoundException e) {
throw e;
} catch (Exception e){
String message = "Unable to find the stock due to:" + e.getMessage();
log.warn(message);
throw new FindStockByIdException(message, e);
}
}
}

Le code source de ce tutoriel est présent sur github à cette adresse:market-instument-crud-service

Ne pas hésiter à commenter ou à poser une question ou à demander de l'aide autours de java et des technologies connexes. Nous nous ferons un plaisir de vous répondre.

Commentaires

Posts les plus consultés de ce blog

Comment valider les données utilisateur dans votre application Java : Guide pratique avec Jakarta Bean Validation

Lorsque vous développez une application qui interagit avec un utilisateur, en lui demandant de saisir des données, il est primordial de vérifier et valider ces données avant tout traitement. Et ceci pour plusieurs raisons: vous aurez la possibilité d'éviter des traitements inutiles si les données reçues ne sont pas conformes aux données attendues et aussi cela permet de protéger votre application en cas d'attaque orchestrée par des hackers. Dans le langage java, il existe une spécification qui vous permet de valider tout type de données transmis dans un bean: Java Bean Validation (ou plus récemment: Jakarta Bean Validation). Dans ce tutoriel, au travers d'une petite application de gestion d'une bibliothèque de quartier, nous allons voir ensemble comment utiliser Jakarta Bean Validation. 1. Pourquoi Jakarta Bean Validation L'objectif de Jakarta Bean Validation est de standardiser la validation des champs au travers de specification (JSR 380). Aussi cette standardisat...

Découvrez comment SpringBoot facilite la parallélisation des tâches

Parfois il nous arrive d'avoir besoin de lancer des taches en parallèles afin d'accélérer le traitement d'une action. Ces taches s'exécuteront de manière indépendante dans des threads différents. Ces threads seront gérés par votre application principale. Ce processus de parallélisation (multithreading) est largement faciliter par springboot. Nous allons donc voir dans ce tutoriel, la parallélisation des taches à l'aide de springboot. 1.  Pourquoi paralléliser les taches avec springboot (multithreading) Comme nous l'avons déjà vu dans l'un de nos tutoriels , springboot vous simplifiera la mis en place et le prototypage de votre application en vous permettant de gérer aisément les taches parallèles, les pools de threads etc... Vous aurez la possibilité de déclarer via une méthode l'asynchornisme d'une tache, et soit attendre ou non la fin de toutes les taches parallèles. La méthode principale d'exécution de l'application sera l'orchestrateu...

Tout ce que vous devez savoir sur l'utilisation des collections en Java

Une collection est une structure de données qui permet à tout développeur java de stocker des données en mémoire et facilement accessible. Les collections peuvent dans certains cas être vues comme des tableaux de données évolués car ces dernières proposent d'innombrables méthodes d'accès afin de faciliter l'accès et la manipulation des données. 1. Qu'est qu'une collection Dans le langage Java, une collection est un objet qui implémente l'interface de base java.util.Collection. Une collection n'est pas une Map. Une collection est un objet dont la structure sou-jacente est un tableau à accès indexé. Il existe plusieurs catégories de collection. 2. Les List Il s'agit du type de collection le plus utilisé. Aucune contrainte sur les objets, aucune contrainte sur la taille de la liste. De plus les objets contenues dans une telle collection sont accessibles directement par un index. L'interface java.util.List est implémentée par plusieurs classes telles que...