Adloun

Les données en base avec JPA et Spring Data

Cours · Spring Boot : écrire le serveur (école d'ingénieur), chapitre 5 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

Arrêtez le serveur de la Librairie, relancez-le : le livre ajouté par une requête POST a disparu, et le stock revient à sa valeur de départ. Depuis le chapitre 1, le catalogue vit dans une LinkedHashMap, c'est-à-dire dans la mémoire du processus Java : il meurt avec lui. Il ne peut pas non plus être partagé par deux serveurs, ni interrogé autrement que par les méthodes que nous avons écrites (chercher « tous les livres à moins de 10 euros du rayon Littérature » demande d'écrire une boucle de plus).

Une application sérieuse range ses données dans une base de données relationnelle : des tables, des lignes, des colonnes, le langage SQL, et des garanties de durabilité et de cohérence. Reste un écart : nos données sont des objets Java, la base ne connaît que des lignes. Ce chapitre présente l'outil standard qui comble cet écart, JPA, son implémentation Hibernate, et la couche que Spring pose par-dessus, Spring Data JPA, qui écrit pour nous l'essentiel des requêtes. Nous utilisons ici la base H2, qui tourne dans le processus Java lui-même et ne demande aucune installation ; le chapitre 10 passera à PostgreSQL sans changer une ligne de Java.

Objectifs du chapitre

  • Expliquer ce qu'est un ORM et ce que font JPA, Hibernate et Spring Data.
  • Écrire une entité : identifiant, colonnes, constructeur sans argument.
  • Obtenir un dépôt complet en déclarant une simple interface.
  • Écrire des requêtes dérivées du nom des méthodes, puis des requêtes JPQL, des projections, une pagination et un tri.
  • Lire le SQL qu'émet Hibernate, et comprendre le contexte de persistance (pourquoi une modification est enregistrée sans appel à save).
  • Brancher la Librairie sur une base H2, et l'inspecter avec la console H2.

Les exemples des sections 5.1 à 5.6 sont dans le projet code/ch5/demo-jpa : une petite application sans serveur web, qui joue une « étape » choisie au lancement puis s'arrête.


$ cd code/ch5/demo-jpa
$ ./mvnw -q package -DskipTests
$ java -jar target/demo-jpa-0.0.1-SNAPSHOT.jar --demo.etape=2

Toutes les requêtes SQL montrées ont été écrites par Hibernate lui-même lors de ces exécutions.

5.1 Des objets aux tables : l'ORM

Enregistrer un livre en JDBC, l'API de base de Java pour parler à une base, demande d'écrire la requête, de remplir ses paramètres un par un, puis, à la relecture, de recopier chaque colonne dans un objet. Multipliez par le nombre de classes et de requêtes : c'est long, répétitif, et chaque changement de classe oblige à reprendre le SQL.

Définition 5.1ORM

Un ORM (object-relational mapping, correspondance objet-relationnel) est une bibliothèque qui fait correspondre des classes à des tables et des objets à des lignes. On lui confie des objets ; il écrit et exécute le SQL qui les enregistre, les relit et les modifie.

Trois noms se superposent, qu'il faut savoir distinguer :

Le starter spring-boot-starter-data-jpa apporte les trois couches, et la dépendance com.h2database:h2 la base. Comme toujours, Spring Boot configure le reste en voyant ces bibliothèques : une source de données (le pool de connexions HikariCP), Hibernate, le gestionnaire de transactions. Avec H2 et sans aucune configuration, il crée même une base en mémoire. Les seules lignes que nous ajoutons fixent son nom et rendent le SQL visible :


spring:
  datasource:
    url: jdbc:h2:mem:demo
  jpa:
    show-sql: true                     # écrire chaque requête SQL sur la console
    properties:
      hibernate.format_sql: false      # une requête par ligne
    defer-datasource-initialization: true   # data.sql APRÈS la création des tables
  main:
    banner-mode: off
    web-application-type: none         # pas de serveur web : un programme qui s'arrête
logging:
  level:
    root: warn

Repère historique : JPA, de Java EE à Jakarta EE

Hibernate est né en 2001, quand les « EJB entités » de Java EE étaient réputées pénibles. Son succès a inspiré la norme JPA, en 2006. En 2017, Oracle a confié Java EE à la fondation Eclipse, qui l'a rebaptisé Jakarta EE ; faute de droits sur le nom « Java », les paquets javax.persistence sont devenus jakarta.persistence. Spring Boot 3 (2022) a fait le saut ; un exemple trouvé en ligne qui importe javax.persistence date donc d'avant, et ne compile plus.

5.2 L'entité : une classe qui devient une table

Une entité est une classe dont les objets sont enregistrés en base. Voici celle de la démonstration :


package fr.librairie.demo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;

@Entity
public class Livre {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String titre;

    @Column(nullable = false, length = 100)
    private String auteur;

    @Column(nullable = false, length = 50)
    private String rayon;

    @Column(nullable = false, precision = 8, scale = 2)
    private BigDecimal prix;

    protected Livre() {
        // exigé par JPA : Hibernate crée l'objet vide, puis remplit ses champs
    }

    public Livre(String titre, String auteur, String rayon, BigDecimal prix) {
        this.titre = titre;
        this.auteur = auteur;
        this.rayon = rayon;
        this.prix = prix;
    }

    public Long getId() { return id; }
    public String getTitre() { return titre; }
    public String getAuteur() { return auteur; }
    public String getRayon() { return rayon; }
    public BigDecimal getPrix() { return prix; }

    public void changerPrix(BigDecimal nouveauPrix) {
        this.prix = nouveauPrix;
    }

    @Override
    public String toString() {
        return "Livre[" + id + ", " + titre + ", " + prix + " €]";
    }
}
Proposition 5.2Les règles d'une entité
  • La classe porte @Entity ; par défaut, la table a son nom, converti en minuscules et en snake_case par Spring Boot (LigneCommande devient ligne_commande).
  • Elle a un identifiant marqué @Id : la clé primaire. @GeneratedValue confie son choix à la base.
  • Elle a un constructeur sans argument, au moins protected : pour relire une ligne, Hibernate crée un objet vide, puis remplit ses champs.
  • Elle ne peut être ni final, ni un record : les champs d'un record sont définitifs, et Hibernate a besoin de les remplir après la construction.
  • Chaque champ devient une colonne ; @Column en précise le nom, la longueur, la nullité, la précision.

Au démarrage, Hibernate lit les entités et, en mode développement, crée les tables. Le journal le montre :


Hibernate: drop table if exists livre cascade
Hibernate: create table livre (prix numeric(8,2) not null, id bigint generated by default as identity, rayon varchar(50) not null, auteur varchar(100) not null, titre varchar(200) not null, primary key (id))

On y reconnaît chaque décision de la classe : le type varchar(200) not null pour le titre, numeric(8,2) pour le prix (huit chiffres dont deux après la virgule : un montant exact, comme il se doit pour de l'argent, d'où BigDecimal et non double), et generated by default as identity pour l'identifiant : c'est la base qui le numérotera.

Qui crée les tables ?

Avec une base embarquée (H2), Spring Boot règle spring.jpa.hibernate.ddl-auto à create-drop : les tables sont créées au démarrage et détruites à l'arrêt. Pour une vraie base, la valeur par défaut est none. Les autres valeurs sont create, update (Hibernate ajoute ce qui manque) et validate (Hibernate vérifie, sans rien toucher, que les tables correspondent aux entités). En production, on ne laisse jamais Hibernate modifier le schéma : update n'efface pas une colonne renommée, ne migre pas les données, et ne laisse aucune trace de ce qu'il a fait. Le chapitre 10 confie le schéma à des migrations versionnées, et règle Hibernate sur validate (exercice 5.9).

Les données de départ sont dans src/main/resources/data.sql, que Spring Boot exécute au démarrage d'une base embarquée. L'option defer-datasource-initialization le fait passer après la création des tables par Hibernate :


insert into livre (titre, auteur, rayon, prix) values
  ('Programmation Java', 'I. Alame', 'Informatique', 32.00),
  ('Introduction à l''algorithmique', 'T. Cormen et al.', 'Informatique', 89.00),
  ('Analyse MPSI', 'J.-M. Monier', 'Mathématiques', 45.00),
  ('Physique générale', 'H. Benson', 'Physique', 59.00),
  ('Le Petit Prince', 'A. de Saint-Exupéry', 'Littérature', 8.50),
  ('L''Étranger', 'A. Camus', 'Littérature', 7.90);

5.3 Le dépôt : une interface suffit

Au chapitre 2, nous avions écrit une interface de dépôt et son implémentation en mémoire. Avec Spring Data, on n'écrit plus que l'interface :


public interface LivreDepot extends JpaRepository<Livre, Long> {
}

Les deux paramètres de type sont la classe de l'entité et le type de son identifiant. Au démarrage, Spring Data voit cette interface, fabrique une classe qui l'implémente, et en fait un bean injectable. Le journal le signale (ici et dans la suite, les colonnes de date, de niveau et de processus des lignes du journal sont retirées) :


Bootstrapping Spring Data JPA repositories in DEFAULT mode.
Finished Spring Data repository scanning in 16 ms. Found 1 JPA repository interface.

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →