ORM, entités et mapping de base

Première leçon du cours Persistance avec JPA et Hibernate, en accès libre : environ 20 minutes, 3 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Expliquer le rôle d'un ORM, de JPA et d'Hibernate
  • Écrire une entité JPA avec @Entity, @Id, @GeneratedValue, @Column et @Enumerated
  • Configurer la source de données et comprendre la génération du schéma (ddl-auto)

JPA (Jakarta Persistence)HibernateEntités et mapping

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 non final : 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) : null signifie « pas encore enregistré ».
  • @Enumerated(EnumType.STRING) est indispensable : avec la valeur par défaut ORDINAL, 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.

  1. CodeDifficulté 2Sandbox serveur2 indices

    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) :

    1. remplacez les . par _ ;
    2. 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) ;
    3. 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));
            }
        }
    }
  2. CodeDifficulté 3Sandbox serveur2 indices

    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 attribut nom: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) (ou varchar(N)), BigDecimal → numeric(38,2), Boolean/boolean → boolean, LocalDate → date, LocalDateTime → timestamp(6). Les primitifs (long, int, boolean) sont not null. L’id s’écrit bigint 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 null puis unique.

    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
        }
    }
  3. TâcheDifficulté 22 indices

    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) et com.h2database:h2 (scope runtime), ou générez un nouveau projet avec curl 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. Configurez spring.jpa.hibernate.ddl-auto=create-drop et spring.jpa.show-sql=true. Créez l’entité Produit (id IDENTITY, nom obligatoire de 100 caractères maximum, prix BigDecimal, statut en @Enumerated(EnumType.STRING), date de création). Démarrez l’application et retrouvez dans les journaux le create table gé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

  1. 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
  2. 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
  3. Quelle valeur de spring.jpa.hibernate.ddl-auto convient à la production ?

    • create-drop
    • update
    • validate (ou none) avec des migrations

La suite du cours

  1. Repositories Spring Data JPA et requêtes20 min
  2. Relations entre entités22 min
  3. Transactions, contexte de persistance et problème N+122 min
  4. Migrations Flyway, verrouillage optimiste et tests JPA22 min

Persistance avec JPA et Hibernate · Avancé · 2 h 46