Votre API Spring Boot stocke ses données dans une Map en mémoire : tout disparaît au redémarrage. Il faut une base de données relationnelle (PostgreSQL, MySQL…). Écrire le SQL à la main pour chaque objet est long et répétitif : un ORM (Object-Relational Mapping) fait le lien entre vos classes Java et les tables.
- JPA (Jakarta Persistence, package
jakarta.persistence) est la spécification standard : des annotations et des interfaces. - Hibernate est l’implémentation la plus utilisée, et celle que Spring Boot configure par défaut. Ce cours utilise Spring Boot 4.1 avec Java 25 : Hibernate 7 y implémente Jakarta Persistence 3.2.
- Spring Data JPA ajoute par-dessus des repositories générés automatiquement (leçon 2).
Configuration
Ajoutez spring-boot-starter-data-jpa, son starter de test spring-boot-starter-data-jpa-test (scope test, pour @DataJpaTest, leçon 5) et le pilote de votre base (org.postgresql:postgresql, ou com.h2database:h2 pour une base en mémoire de test). Spring Initializr ajoute ces dépendances quand on choisit Spring Data JPA et H2 (-d dependencies=web,data-jpa,h2). Puis, dans application.properties :
spring.datasource.url=jdbc:postgresql://localhost:5432/boutique
spring.datasource.username=boutique
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=update # create-drop, update, validate, none
spring.jpa.show-sql=true # affiche le SQL généré (développement)
spring.jpa.open-in-view=false # recommandé, voir leçon 4
ddl-auto contrôle la génération du schéma : create-drop (recréé à chaque démarrage), update (ajoute ce qui manque), validate (vérifie sans modifier), none. En production, on utilise validate ou none et des migrations versionnées (leçon 5).
Une première entité
package fr.boutique.produit;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity // classe persistante
@Table(name = "produit") // facultatif : nom de table explicite
public class Produit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // id généré par la base
private Long id;
@Column(nullable = false, length = 100)
private String nom;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal prix; // jamais de double pour de l'argent
@Enumerated(EnumType.STRING) // stocke "ACTIF", pas 0
private StatutProduit statut = StatutProduit.ACTIF;
private LocalDateTime dateCreation; // colonne date_creation
protected Produit() { } // exigé par JPA
public Produit(String nom, BigDecimal prix) {
this.nom = nom;
this.prix = prix;
this.dateCreation = LocalDateTime.now();
}
public Long getId() { return id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
// autres getters / setters...
}
enum StatutProduit { ACTIF, ARCHIVE }
Points essentiels :
- JPA exige un constructeur sans argument (au moins
protected) et une classe nonfinal: Hibernate instancie l’objet puis remplit les champs, et crée des proxys pour le chargement paresseux. C’est pourquoi un record ne peut pas être une entité ; il reste idéal pour les DTO. - L’identifiant est un type objet (
Long) :nullsignifie « pas encore enregistré ». @Enumerated(EnumType.STRING)est indispensable : avec la valeur par défautORDINAL, insérer une valeur au milieu de l’énumération corromprait silencieusement les données existantes.
Nommage et types
Spring Boot configure une stratégie de nommage qui convertit le camelCase en snake_case : dateCreation devient date_creation, la classe LigneCommande la table ligne_commande. Les types Java sont associés aux types SQL : Long → bigint, String → varchar(255) par défaut, BigDecimal → numeric, LocalDate → date, LocalDateTime → timestamp, boolean → boolean. Les types primitifs (int, boolean) produisent des colonnes not null.
Avec show-sql=true, observez au démarrage le create table généré : c’est le meilleur moyen de vérifier votre mapping.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
La stratégie de nommage camelCase vers snake_case
Reproduisez (de façon simplifiée) la stratégie de nommage par défaut de Spring Boot. Pour chaque nom Java de l’entrée (classe ou attribut) :
- remplacez les
.par_; - insérez un
_avant une majuscule précédée d’une minuscule et suivie d’une minuscule (la première lettre n’est jamais concernée) ; - mettez le tout en minuscules.
Affichez
nom -> nom_sql. Exemple :dateCreation -> date_creation,codeISO -> codeiso(les sigles en majuscules ne sont pas découpés).import java.util.*; public class Main { static String versSnakeCase(String nom) { // TODO return nom; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String n = sc.nextLine().trim(); if (!n.isEmpty()) System.out.println(n + " -> " + versSnakeCase(n)); } } }- remplacez les
Générer le DDL d'une entité
Écrivez un mini-générateur de
create table(version simplifiée de ce que produit Hibernate pour PostgreSQL). La première ligne est le nom de la classe ; chaque ligne suivante décrit un attributnom:Type[:options], les options étant séparées par des virgules :id,notnull,unique,length=N.Types :
Long/long→bigint,Integer/int→integer,String→varchar(255)(ouvarchar(N)),BigDecimal→numeric(38,2),Boolean/boolean→boolean,LocalDate→date,LocalDateTime→timestamp(6). Les primitifs (long,int,boolean) sontnot null. L’id s’écritbigint generated by default as identity. Les noms passent en snake_case (règle de l’exercice précédent). Format exact :create table produit ( id bigint generated by default as identity, nom varchar(100) not null, primary key (id) )L’ordre des suffixes est
not nullpuisunique.import java.util.*; public class Main { static String versSnakeCase(String nom) { StringBuilder sb = new StringBuilder(nom.replace('.', '_')); for (int i = 1; i < sb.length() - 1; i++) { if (Character.isLowerCase(sb.charAt(i - 1)) && Character.isUpperCase(sb.charAt(i)) && Character.isLowerCase(sb.charAt(i + 1))) { sb.insert(i, '_'); i++; } } return sb.toString().toLowerCase(); } static String typeSql(String typeJava, Integer longueur) { // TODO return ""; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); String classe = sc.nextLine().trim(); // TODO } }Persister une première entité
Dans votre projet Spring Boot de l’environnement de travail, ajoutez
spring-boot-starter-data-jpa,spring-boot-starter-data-jpa-test(scope test) etcom.h2database:h2(scope runtime), ou générez un nouveau projet aveccurl https://start.spring.io/starter.zip -d type=maven-project -d dependencies=web,data-jpa,h2 -d javaVersion=25 -d groupId=fr.boutique -d artifactId=boutique -d packageName=fr.boutique -d baseDir=boutique -o boutique.zip. Configurezspring.jpa.hibernate.ddl-auto=create-dropetspring.jpa.show-sql=true. Créez l’entitéProduit(id IDENTITY, nom obligatoire de 100 caractères maximum, prixBigDecimal, statut en@Enumerated(EnumType.STRING), date de création). Démarrez l’application et retrouvez dans les journaux lecreate tablegénéré.- L'entité utilise les annotations jakarta.persistence et possède un constructeur protected sans argument
- Les journaux de démarrage contiennent un create table produit avec les colonnes nom (not null), prix, statut et date_creation
- La colonne statut est de type texte (varchar ou énumération), pas un entier
- L'application démarre sans erreur
Quiz de fin de leçon
Quelle est la relation entre JPA et Hibernate ?
- Ce sont deux noms pour le même produit
- JPA est une spécification, Hibernate une implémentation
- Hibernate est une spécification, JPA une implémentation
Pourquoi préférer @Enumerated(EnumType.STRING) ?
- C'est plus rapide
- Les données restent correctes si l'ordre des valeurs de l'énumération change
- C'est la valeur par défaut
Quelle valeur de spring.jpa.hibernate.ddl-auto convient à la production ?
- create-drop
- update
- validate (ou none) avec des migrations
La suite du cours
- Repositories Spring Data JPA et requêtes20 min
- Relations entre entités22 min
- Transactions, contexte de persistance et problème N+122 min
- Migrations Flyway, verrouillage optimiste et tests JPA22 min