Introduction à Jakarta Persistence API (JPA)

Lecture
Java
JPA
I211
Mapping relationnel/Objet en java avec Jakarta Persistence
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

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

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/

2 Introduction à Jakarta Persistence API (JPA)

Le Mapping Relationnel/Object (ORM) est une approche déclarative visant à établir un lien entre un modèle Orienté Objet, tel qu’un programme Java, et un modèle relationnel, matérialisé par des tables dans une base de données relationnelle. Cette démarche peut comprendre la création du schéma relationnel à partir du programme Java et l’automatisation des fonctions essentielles pour la gestion de la persistance des données. Cela améliore la portabilité des applications par une abstraction de la couche de persistance et donc une réduction de la dépendance au code SQL spécifique au fournisseur de base de données.

3 Spécification

Pour Java, Jakara Persistance API (JPA) est une spécification (actuellement en version 3.1) concernant la liaison entre le modèle orienté objet de Java et le modèle relationnel (Object Relational Mapping - ORM). JPA s’appuie sur JDBC (Java Database Connectivity). JPA fournit une solution robuste et normalisée pour la gestion des interactions entre une application Java et une base de données.

Plusieurs implantations de JPA existent dont l’implantation de référence EclipseLink et une autre très utilisée Hibernate. Cette introduction utilise Hibernate mais en respectant le standard.

4 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>
  -->

5 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.

6 Les Entités dans Jakarta Persistence API (JPA)

Avec JPA une classe dont les instances doivent être persistantes est appelée une entité. Techniquement, une classe est une entité si elle annotée avec @Entity, qu’un ou plusieurs de ses attributs sont annotés avec @Id et qu’elle a au moins un constructeur sans paramètre (au plus protected). La classe sera alors associée à une relation du même nom, ayant une clé primaire (atomique ou composite)

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 :

                    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)

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}
}

                            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)

@Entity indique qu’une classe est une entité et permet de contrôler son nom (par défaut celui de la classe).

@Id est obligatoire et défini le ou les attributs qui compose la clé primaire.

@GeneratedValue contrôle la méthode utilisée pour générer la clé lors de l’insertion (automatique pour s’adapater à la base de données utilisée, identité pour MySQL, séquence pour ceux qui le supporte ou Table qui est portable mais avec les moins bonnes performances et récement UUID).

@Table contrôle finement le mapping au niveau de la table associée (nom spécifique, schema, catalogue, contraintes et index).

@Column contrôle finement la colonne associée au membre comme son nom ou ses contraintes (null, insertion ou mise à jour autorisés ou non ; unicité ; …).

@Basic est l’annotation pour tout ce qui peut etre représenté directement comme un attribut : les primitifs (et leurs classes d’enveloppement), les chaînes de caractères, les types temporels, les octets ou tableaux d’octets, les énumérés et tout ce qui implante l’interface Serialisable. Cette annotation est optionnelle et permet de d’indiquer si l’attribut peut être nul dans la base (optional) et s’il doit être récupéré au changement de classe (par défaut, fetch=EAGER) ou uniquement lors de l’accès au membre (fetch=LAZY).

@Transient permet d’indiquer qu’un membre ne doit pas être persistant (s’il porte le modificateur transient c’est aussi le cas).

@Lob permet d’associer des grands objets binaires ou textes (BLOB et CLOB).

@Version définit un attribut utiliser pour assurer le verrouillage optimiste.

7 Le gestionnaire d’entités

Pour gérer les entité JPA s’appuie sur l’entity manager (EM). Il fournit les opération CRUD de base et une interface pour exécuter des requêtes. L’EM s’appuie sur des unités de persistances (des connexions à une base de données) qui sont définie dans un fichier XML META-INF/persistence.xml à la racine du classpath (donc dans src/main/resources avec Maven). Son schéma est défini dans le standard https://jakarta.ee/xml/ns/persistence.

7.1 Exemple de persistence.xml

Un exemple de base est donné ci-dessous :

<?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>

7.2 Création de l’EM et persistence d’une entité

Pour obtenir un EM, on utilise l’EntityManagerFactory (EMF) en indiquant le nom d’une persistence unit. La génération automatique éventuelle du schéma a lieu lors de la première utilisation de l’EMF.

A partir de l’entity manager, il est alors possible de commencer une transaction (begin), de rendre persistante une instance (persist) et de terminer la transaction (commit ou rollback). On note la mise à jour automatique de l’entity avec l’ID généré par la base de données. L’entity manager est prévu pour une utilisation locale et généralement brève, il doit être fermé après utilisation (autocloseable).

try (EntityManagerFactory emf = Persistence.createEntityManagerFactory("hellojpa-pu")) 
{
    try (EntityManager entityManager = emf.createEntityManager()) {
    }
}    

La création d’un EMF est couteuse, on utilise donc généralement un singleton pour gérer l’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)

A partir du moment ou une entité est liée au contexte de persistence (en venant d’être crée ou récupérée), la mise à jour dans la base est implicite (pas de fonction dédiée de l’entity manager). La transaction n’est nécessaire que pour les modifications.

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)

L’entity manager propose une méthode de recherche par ID find​(Class<T> entityClass, Object primaryKey) qui prend en paramètre la classe de l’entité recherchée et la valeur de la clé.

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

L’entity manager propose une méthode remove pour 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.

merge attache un objet au contexte de persistence (par son id), refresh annule les modifications en cours de transanction sur une entité et 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)

8 Conseils pour l’utilisation de JPA avec Lombok

Lorsque vous utilisez Lombok avec JPA (Java Persistence API), quelques précautions sont nécessaires pour assurer un fonctionnement harmonieux de votre application. Évitez l’utilisation de l’annotation @Data, car elle peut générer des méthodes indésirables dans le contexte de la persistance des données. De plus, faites attention avec @EqualsAndHashCode, car elle peut avoir des implications inattendues sur les relations Hibernate. Lorsque vous utilisez @ToString, assurez-vous d’exclure les attributs “Lazy” pour éviter les problèmes liés au chargement paresseux lors de l’affichage des objets. Enfin, pour garantir une construction cohérente des entités, ajoutez @NoArgsConstructor(access= AccessLevel.PROTECTED) en conjonction avec @Builder ou @AllArgsConstructor. En suivant ces bonnes pratiques, vous pouvez optimiser l’utilisation de Lombok avec JPA tout en évitant les écueils potentiels.

Réutilisation