registered line magics:
- listLineMagic
- jars
- cmd
- read
- git-graph-mermaid
- where, which
- commonshellcmd
- printWithName
- listMagic, list
- addMavenDependencies, maven, addMavenDependency
- pom, loadFromPOM
- tableSchema
- load
- classpath
- javadoc-html
- write
- class-info
- reload-class
- listCellMagic
- classpath-snapshot
- printerPrefix
- addMavenRepo, mavenRepo
- classDiagram
registered cell magics:
- commonshell
- javasrcList
- javasrcClassByName
- classDiagram
- mycompile
- timeit, timeIt, time
- plantUMLFile
- tableSchema
- javasrcMethodByAnnotationName
- sqlAsTable
- javasrcJavadoc
- javasrcMethodByName
- javasrcInterfaceByName
- write
- rdbmsSchema
- compile
- shell
- benchmark
- pom, loadFromPOM
- plantUML
- javasrcConstructorByName
- javasrcFieldByName
Introduction à Jakarta Persistence API (JPA)
Magics
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)
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.
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.
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)
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 :
et à la table :
| Column | Type | Nullable | Default | Remarks |
|---|---|---|---|---|
| id (PK) (UNIQUE) | int8(19) | NOT NULL | ||
| varchar(255) | ||||
| name | varchar(255) |
Primary key: id
Indexes: - customer_pkey (id) UNIQUE
CREATE TABLE ex_simple.customer (
id int8(19) NOT NULL,
email varchar(255),
name varchar(255),
PRIMARY KEY (id)
);
| id | name |
|---|
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}
}| Column | Type | Nullable | Default | Remarks |
|---|---|---|---|---|
| id (PK) | int8(19) | NOT NULL | ||
| name | varchar(50) | NOT NULL | ||
| bio_brief | varchar(255) | |||
| bio_extended | oid(10) | |||
| id (PK) | int8(19) | NOT NULL | ||
| name | varchar(255) | |||
| id (PK) | int8(19) | NOT NULL | ||
| name | varchar(255) | |||
| id (PK) | int8(19) | NOT NULL | ||
| name | varchar(255) | |||
| id (PK) | int8(19) | NOT NULL | ||
| name | varchar(255) | |||
| id (PK) | int8(19) | NOT NULL | ||
| email (UNIQUE) | varchar(255) | |||
| name | varchar(255) | |||
| uuid (PK) | uuid(2147483647) | NOT NULL | ||
| name | varchar(50) | NOT NULL | ||
| birthdate | date(13) | |||
| status | int2(5) | |||
| version | int4(10) | |||
| id (PK) | int8(19) | NOT NULL | ||
| creationdate | timestamp(29) | |||
| email (UNIQUE) | varchar(50) | NOT NULL | ||
| firstname | varchar(50) | NOT NULL | ||
| lastname | varchar(50) | NOT NULL | ||
| photo | oid(10) |
Primary key: id, id, id, id, id, id, uuid, id
Indexes: - customer_email_key (email) UNIQUE - customer_pkey (id, id, id, id, id, uuid, id, id) UNIQUE
CREATE TABLE customer (
id int8(19) NOT NULL,
name varchar(50) NOT NULL,
bio_brief varchar(255),
bio_extended oid(10),
id int8(19) NOT NULL,
name varchar(255),
id int8(19) NOT NULL,
name varchar(255),
id int8(19) NOT NULL,
name varchar(255),
id int8(19) NOT NULL,
name varchar(255),
id int8(19) NOT NULL,
email varchar(255),
name varchar(255),
uuid uuid(2147483647) NOT NULL,
name varchar(50) NOT NULL,
birthdate date(13),
status int2(5),
version int4(10),
id int8(19) NOT NULL,
creationdate timestamp(29),
email varchar(50) NOT NULL,
firstname varchar(50) NOT NULL,
lastname varchar(50) NOT NULL,
photo oid(10),
PRIMARY KEY (id, id, id, id, id, id, uuid, id)
);
| birthdate | status | version | id | creationdate | firstname | lastname | photo |
|---|
@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.
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.
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"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_1.xsd"
version="3.1">
<persistence-unit name="hellojpaPU" transaction-type="RESOURCE_LOCAL">
<description>Hello JPA – Hibernate + PostgreSQL</description>
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- We explicitly list entities -->
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<!-- ===== ENTITIES ===== -->
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_associations.ex_manytomany.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_associations.ex_manytomany.Address</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_simple.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_entity.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_biography.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_uuid.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_hook.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_hook.CustomerWithListener</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage1.Person</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage1.Student</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage1.Teacher</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage2.Person</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage2.Student</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage2.Teacher</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage3.Person</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage3.Student</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_heritage3.Teacher</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_ids.ex_seqid.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_ids.ex_tableid.Customer</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_ids.ex_idclass.Employee</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_ids.ex_embedded.Employee</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_associations.ex_onetomany_a.Order</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_associations.ex_onetomany_a.Line</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.ex_associations.ex_onetomany_b.Order</class>
<class>fr.univtln.bruno.demos.jpa.hello.samples.CustomerBase</class>
<!-- ===== CONFIGURATION ===== -->
<properties>
<!-- JDBC (Hibernate will use HikariCP automatically) -->
<property name="jakarta.persistence.jdbc.url"
value="jdbc:postgresql://db:5432/hellojpa-db"/>
<property name="jakarta.persistence.jdbc.user" value="dba"/>
<property name="jakarta.persistence.jdbc.password" value="secretsecret"/>
<!-- Schema generation (demo/teaching setup) -->
<property name="jakarta.persistence.schema-generation.database.action"
value="drop-and-create"/>
<property name="jakarta.persistence.schema-generation.create-source"
value="script-then-metadata"/>
<property name="jakarta.persistence.schema-generation.drop-source"
value="script-then-metadata"/>
<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"/>
<property name="jakarta.persistence.schema-generation.create-database-schemas"
value="true"/>
<!-- Hibernate behavior / tuning -->
<property name="hibernate.jdbc.batch_size" value="25"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.order_updates" value="true"/>
<!-- SQL logging -->
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.highlight_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>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();
}12:24:22.739 [IJava-executor-0] INFO notebook -- BEFORE PERSIST Customer(id=0, email=pierre.durand@ici.fr, name=null)
12:24:22.779 [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();12:24:22.944 [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));
}
}12:24:23.126 [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();
}
}12:24:23.228 [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());12:24:23.484 [IJava-executor-0] INFO notebook -- BEFORE MERGE Customer(id=3, email=???, name=null)
12:24:23.538 [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
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.