2025-01-30
Source | |
Branch |
|
Java |
|
Docker |
|
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/
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>
-->
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.
La classe suivante est donc entité JPA :
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)
META-INF/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>
begin
, persister une instance avec persist
, terminer avec commit
ou rollback
.AutoCloseable
).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)
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(Class<T> entityClass, Object primaryKey)
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)]
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.
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)
@EqualsAndHashCode
: Vérifier les implications avec les relations Hibernate, voir https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/@Data
: Contrôler précisément les méthodes générées pour éviter des comportements inattendus.@ToString
: Prévenir les problèmes de chargement paresseux inattendus lors de l’affichage.@NoArgsConstructor(access= AccessLevel.PROTECTED)
: Utiliser en conjonction avec @Builder
ou @AllArgsConstructor
pour garantir des comportements de construction cohérents.E. Bruno - Introduction à Jakarta Persistence API (JPA)