Introduction à Jakarta Persistence API (JPA)

Université de Toulon

LIS UMR CNRS 7020

2025-01-30

Source
Branch
  • develop (c88a48e)
  • 2024/03/08 11:41:10
Java
  • OpenJDK Temurin-21.0.5+11
  • Apache Maven 3.9.9
Docker
  • Client: 27.3.1 - buildx v0.18.0 - compose v2.30.3
  • Server: 27.3.1

Exemples

Les exemples présentés sont disponibles dans l’entrepôt: https://github.com/ebpro/sample-hellojpa/

Un exemple complet de base est disponible dans l’entrepôt: https://github.com/ebpro/minimal-jpa/

Introduction à Jakarta Persistence API (JPA)

  • Jakarta Persistence API (JPA) est une spécification Java pour la gestion de la persistance des données dans les applications Jakarta EE.
  • Objectif : faciliter le mapping entre les objets Java et les tables de base de données relationnelles.
  • Simplifie les opérations CRUD (Create, Read, Update, Delete) sur les objets persistants.
  • Avantages : portabilité des applications, abstraction de la couche de persistance, réduction de la dépendance au code SQL spécifique au fournisseur de base de données.
  • Exemple : utilisation de JPA avec des outils populaires tels que Hibernate, EclipseLink, ou OpenJPA pour simplifier le développement d’applications Jakarta EE.

Spécification

  • JPA : spécification Java pour la liaison entre le modèle orienté objet de Java et le modèle relationnel (ORM).
  • Version actuelle : JPA 3.1.
  • Repose sur JDBC (Java Database Connectivity): génère automatiquement les requêtes SQL et leur mise en oeuvre.
  • Offre une solution normalisée et robuste pour la gestion des données dans les applications Java.

En pratique

Il faut donc ajouter les éléments suivant au pom.xml de Maven.

  <!-- L'API Standard -->
  <!-- a priori inclue par transitivité avec l'une des suivantes -->
  <dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
  </dependency>

  <dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.3.1.Final</version>
  </dependency>

  <!-- L'implantation de référence -->
  <!--
  <dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>4.0.1</version>
  </dependency>
  -->

JPA possède trois composantes principales

  • La description des “entités” persistentes.
  • L’interaction de base avec la persistence via les les opérations CRUD (Create, Read, Update, Delete).
  • L’interrogation native ou via un langage dédié.

Pour utiliser JPA, il faut une base de données relationnelle. Nous utiliserons ici PostgreSQL via l’image Docker officielle.

Les exemples sont disponibles sur ce repository GitHub.

Les Entités dans Jakarta Persistence API (JPA)

  • Entité : Une classe dont les instances doivent être persistantes.
  • Critères pour être une entité :
    • Annotée avec @Entity.
    • Au moins un attribut annoté avec @Id.
    • Doit avoir au moins un constructeur sans paramètre (au plus protected).
  • Association avec une relation :
    • La classe est associée à une relation du même nom.
    • Cette relation possède une clé primaire, atomique ou composite.

Premier exemple d’entité

La classe suivante est donc entité JPA :

@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Setter
@ToString
@Getter

@Entity
public class Customer {
    @Id
    @GeneratedValue
    private long id;

    @NonNull
    private String email;

    private String name;
}

associée à la relation :

La table correspondante

                    Table "ex_simple.customer"
 Column |          Type          | Collation | Nullable | Default 
--------+------------------------+-----------+----------+---------
 id     | bigint                 |           | not null | 
 email  | character varying(255) |           |          | 
 name   | character varying(255) |           |          | 
Indexes:
    "customer_pkey" PRIMARY KEY, btree (id)

Exemple étendu d’entité

Le mapping peut être contrôllé finement par un ensemble d’annotations. La classe suivante illustre celles de base.

@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "CUSTOMER",
       indexes = {@Index(name = "idx_email", columnList = "email", unique = true)})
public class Customer {
    @Builder.Default
    @Column(updatable = false)
    private LocalDateTime creationDate = LocalDateTime.now();
    
    @Id
    @Column(name = "ID")
    @GeneratedValue
    private long id;

    @Column(length = 50, nullable = false, unique = true)
    @NonNull
    private String email;

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

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

    @Transient
    private String displayName;

    @Setter
    private LocalDate birthDate;

    @Lob
    @Basic(fetch = FetchType.LAZY)
    @ToString.Exclude

    private byte[] photo;

    @Builder.Default
    //@Enumerated(EnumType.STRING)
    private Status status = Status.LEAD;

    @Version
    protected Integer version;

    public enum Status {ACTIVE, LEAD}
}

La table correspondante

                            Table "public.customer"
    Column    |              Type              | Collation | Nullable | Default 
--------------+--------------------------------+-----------+----------+---------
 birthdate    | date                           |           |          | 
 status       | smallint                       |           |          | 
 version      | integer                        |           |          | 
 id           | bigint                         |           | not null | 
 creationdate | timestamp(6) without time zone |           |          | 
 email        | character varying(50)          |           | not null | 
 firstname    | character varying(50)          |           | not null | 
 lastname     | character varying(50)          |           | not null | 
 photo        | oid                            |           |          | 
Indexes:
    "customer_pkey" PRIMARY KEY, btree (id)
    "customer_email_key" UNIQUE CONSTRAINT, btree (email)
Check constraints:
    "customer_status_check" CHECK (status >= 0 AND status <= 1)

Annotations de base pour la Persistance

  • @Entity: Indique une classe entité.
  • @Id: Définit la clé primaire.
  • @GeneratedValue: Contrôle la génération de clés.
  • @Table: Contrôle le mapping de la table.
  • @Column: Contrôle les colonnes.
  • @Basic: Pour les attributs simples.
  • @Transient: Non persistant.
  • @Lob: Pour les grands objets.
  • @Version: Verrouillage optimiste.

Le gestionnaire d’entités

Exemple de persistence.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="3.1">

    <persistence-unit name="hellojpaPU"
                      transaction-type="RESOURCE_LOCAL">
        <description>Hello JPA Persistance Unit</description>

        <!-- The provider for the persistence unit -->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <!-- The classes to be managed by the persistence unit -->
        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <!-- database connection for Java SE -->
            <property name="jakarta.persistence.jdbc.driver"
                                  value="org.postgresql.Driver" />
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:postgresql://localhost/hellojpa-db" />
            <property name="jakarta.persistence.jdbc.user"
                      value="dba" />
            <property name="jakarta.persistence.jdbc.password"
                      value="secretsecret" />

            <!-- Database creation: none, create, drop-and-create, drop -->
            <property name="jakarta.persistence.schema-generation.database.action"
                      value="drop-and-create" />

            <!-- Sources and order for DB schema generation: script and metadata -->
            <property name="jakarta.persistence.schema-generation.create-source"
                      value="script-then-metadata" />
            <!-- Sources and order for DB schema destruction: script and metadata -->
            <property name="jakarta.persistence.schema-generation.drop-source"
                      value="script-then-metadata" />

            <!-- The scripts provided in META-INF for initialization -->
            <property name="jakarta.persistence.schema-generation.create-database-schemas"
                      value="true" />
            <property name="jakarta.persistence.schema-generation.create-script-source"
                      value="sql/provided-create.ddl" />
            <property name="jakarta.persistence.schema-generation.drop-script-source"
                      value="sql/provided-drop.ddl" />
            <property name="jakarta.persistence.sql-load-script-source"
                      value="sql/provided-data.sql" />

            <!-- The scripts generated by JPA -->
            <property name="jakarta.persistence.schema-generation.scripts.create-target"
                      value="generated-create.ddl" />
            <property name="jakarta.persistence.schema-generation.scripts.drop-target"
                      value="generated-drop.ddl" />

        </properties>
    </persistence-unit>
</persistence>

EM et persistence d’une entité

  • Utilisation de l’EntityManagerFactory
    • Obtention d’un EntityManager via l’EntityManagerFactory, en spécifiant le nom de la persistence unit.
    • La génération automatique du schéma a lieu lors de la première utilisation.
  • Opérations avec l’EntityManager
    • Commencer une transaction avec begin, persister une instance avec persist, terminer avec commit ou rollback.
    • L’EM met automatiquement à jour l’entity avec l’ID généré par la base de données.
    • L’EM : utilisé localement et fermé après utilisation (AutoCloseable).
try (EntityManagerFactory emf = Persistence.createEntityManagerFactory("hellojpa-pu")) 
{
    try (EntityManager entityManager = emf.createEntityManager()) {
    }
}    

Usage avec un Singleton EMF

EntityManagerFactory emf = DatabaseManager.getEntityManagerFactory();
Logger log = LoggerFactory.getLogger("notebook");

ce singleton retourne l’instance de l’EMF qui n’est qu’à la première demande.

import fr.univtln.bruno.demos.jpa.hello.samples.ex_simple.*;

try (EntityManager entityManager = emf.createEntityManager();) {
        entityManager.getTransaction().begin();

        Customer customer = Customer.of("pierre.durand@ici.fr");
        log.info("BEFORE PERSIST "+customer.toString());

        entityManager.persist(customer);
        log.info("AFTER PERSIST: "+customer.toString());

        entityManager.getTransaction().commit();
}
18:09:35.100 [IJava-executor-0] INFO  notebook -- BEFORE PERSIST Customer(id=0, email=pierre.durand@ici.fr, name=null)
18:09:35.117 [IJava-executor-0] INFO  notebook -- AFTER PERSIST: Customer(id=1, email=pierre.durand@ici.fr, name=null)

Mise à jour

  • Mise à jour implicite
  • Transaction nécessaire pour les mises à jour
long newId;
Customer customer = Customer.of("marie.dupond@la.fr");

try (EntityManager entityManager = emf.createEntityManager();) {
    entityManager.getTransaction().begin();
    entityManager.persist(customer);
    
    customer.setName("Marie Dupond");
        
    entityManager.getTransaction().commit();
    log.info("AFTER COMMIT: "+customer.toString());
}

newId=customer.getId();
18:09:35.216 [IJava-executor-0] INFO  notebook -- AFTER COMMIT: Customer(id=2, email=marie.dupond@la.fr, name=Marie Dupond)

Find by Id

try (EntityManager entityManager = emf.createEntityManager();) {
    Optional<Customer> customer = Optional.ofNullable(entityManager.find(Customer.class, newId));
    
    if (customer.isEmpty()) 
        log.info("Id %d not in database.".formatted(newId));
    else {
        log.info("Search cust. Id %d: %s".formatted(newId,customer));
     }
}
18:09:35.312 [IJava-executor-0] INFO  notebook -- Search cust. Id 2: Optional[Customer(id=2, email=marie.dupond@la.fr, name=Marie Dupond)]

Suppression

  • remove supprimer une entité de la base de données.
try (EntityManager entityManager = emf.createEntityManager();) {
    Optional<Customer> customer = Optional.ofNullable(entityManager.find(Customer.class, newId));
    
    if (customer.isEmpty()) 
        log.error("Id %d not in database.".formatted(newId));
    else {
        log.info("Removed Id %d from the database.".formatted(newId));
        entityManager.getTransaction().begin();
        entityManager.remove(customer.get());
        entityManager.getTransaction().commit();
    }
}
18:09:35.383 [IJava-executor-0] INFO  notebook -- Removed Id 2 from the database.

Fonctions supplémentaires de l’EntityManager

  • merge: Attache un objet au contexte de persistence (par son ID).
  • refresh: Annule les modifications en cours de transaction sur une entité.
  • detach: Indique à l’EM de ne plus gérer une entité.
Customer customer = Customer.of("???");
customer.setId(newId);
log.info("BEFORE MERGE "+customer.toString());

try (EntityManager entityManager = emf.createEntityManager();) {
    entityManager.getTransaction().begin();
    customer =  entityManager.merge(customer);
    entityManager.refresh(customer);
    entityManager.getTransaction().commit();    
}

log.info("AFTER MERGE+REFRESH "+customer.toString());
18:09:35.564 [IJava-executor-0] INFO  notebook -- BEFORE MERGE Customer(id=3, email=???, name=null)
18:09:35.590 [IJava-executor-0] INFO  notebook -- AFTER MERGE+REFRESH Customer(id=3, email=pierre.durand@ici.fr, name=null)

Conseils pour l’utilisation de JPA avec Lombok

  • Attention avec @EqualsAndHashCode: Vérifier les implications avec les relations Hibernate, voir https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/
  • Éviter d’utiliser @Data: Contrôler précisément les méthodes générées pour éviter des comportements inattendus.
  • Exclure les attributs “Lazy” de @ToString: Prévenir les problèmes de chargement paresseux inattendus lors de l’affichage.
  • Ajouter @NoArgsConstructor(access= AccessLevel.PROTECTED): Utiliser en conjonction avec @Builder ou @AllArgsConstructor pour garantir des comportements de construction cohérents.