Les associations entre entités avec JPA

Contrôles des relations entre entités avec JPA.

Lecture
Containers
Java
JPA
Persistence
ORM
Apprenez à gérer les associations entre entités en utilisant Jakarta Persistence API (JPA) avec des exemples pratiques en Java.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2026-11-01

[postgres-ci] starting...
[postgres-ci] OK
[maven] starting...
[maven] OK
[diag] 1/5 classpath du kernel exposée
[diag] 2/5 imports slf4j/logback OK
[diag] 3/5 contexte logback OK
[diag] 4/5 40 jars ajoutés au classpath
[diag] Postgres driver registered OK
[diag] 5/5 imports JPA/DatabaseManager OK
21:30:38.417 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Database settings: URL=jdbc:postgresql://dind/notebook-db, User=dba
21:30:38.423 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Starting JPA with URL: jdbc:postgresql://dind/notebook-db and User: dba
21:30:39.475 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
21:30:39.696 [IJava-executor-0] INFO  com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@2224a356
21:30:39.698 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Start completed.
--> Connexion JPA réussie (Tentative 1) !
[diag] cell init terminée ✓

JPA offre une prise en charge complète pour la gestion des relations entre entités de différents types, que ce soit en mode un-vers-un (OneToOne), un-vers-plusieurs (OneToMany), ou plusieurs-vers-plusieurs (ManyToMany). Ces relations peuvent être configurées de manière unidirectionnelle ou bidirectionnelle.

One To One (1-1)

Dans certaines situations, lors de la modélisation orientée objet, il est nécessaire d’associer deux classes dans une relation un-à-un, mais en gardant une seule relation.

Figure 1: Un Customer a une Biography
Figure 2: L’unique relation CUSTOMER

Pour cela JPA propose des annotations spécifiques :

@Embeddable: Cette annotation indique que les membres d’une classe Java peuvent être persistés en tant qu’attributs de la classe qui les utilise. En d’autres termes, elle spécifie qu’une classe peut être utilisée comme composant réutilisable dont les propriétés peuvent être incorporées dans une autre entité.

@Embedded: Utilisée au niveau d’un membre de classe annoté avec @Embeddable, cette annotation indique que le membre annoté doit être intégré dans la relation. Cela signifie que les propriétés de la classe annotée avec @Embeddable seront incluses dans la table de la classe qui les utilise.

@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor

@Embeddable
public class Biography {
    @Column(name = "BIO_BRIEF")
    private String brief;

    @Lob
    @Column(name = "BIO_EXTENDED")
    private String extended;
}
Figure 3: Embeddable.java
@Getter
@Setter
@ToString
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "CUSTOMER", schema = "ex_biography")
public class Customer {
    @Id
    @GeneratedValue
    private long id;

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

    @Embedded
    @Column()
    private Biography biography;
}

Customer.java

On peut donc rendre persistente une instance de Customer associé à une instance de Biography.

Creation et persistence d’un Customer
import fr.univtln.bruno.demos.jpa.hello.samples.ex_biography.Customer;
try (EntityManager entityManager = emf.createEntityManager()) {
   entityManager.getTransaction().begin();

   Customer customer = Customer.of("Jim");
   customer.setBiography(Biography.builder()
      .brief("bla")
      .extended("bla bla")
      .build());
   entityManager.persist(customer);

   entityManager.getTransaction().commit();
}

dans une seule relation

id name bio_brief bio_extended
1 Jim bla 16818
Figure 4: La table CUSTOMER

One to Many (1-N)

Figure 5: Une Order a plusieurs Line
@Getter
@Setter
@ToString
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Embeddable
@Table(name = "LINE", schema = "EX_ONE_TO_MANY_B")
public class Line {
    @NonNull
    private String product;

    private double price;
}
@Getter
@Setter
@ToString
@NoArgsConstructor
@Builder
@AllArgsConstructor

@Entity
@Table(name = "EORDER", schema = "EX_ONE_TO_MANY_B")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    @Builder.Default
    @Column(nullable = false)
    private LocalDateTime date = LocalDateTime.now();

    @ElementCollection
    @CollectionTable(name = "LINE", schema = "EX_ONE_TO_MANY_B")
    @Singular
    private Set<Line> lines = new HashSet<>();

}
Figure 6: Les relations LINE et EORDER

Avec deux entités

Dans le cas général, l’attribut de type Collection<T> est annoté avec @OneToMany.

L’attribut optionnel mappedBy indique le nom de l’attribut correspondant dans le type T si l’on souhaite une relation bi-directionnelle. L’autre entité (de Type T) est alors annotée avace @ManyToOne

@Getter
@Setter
@ToString
@NoArgsConstructor

@Entity
@Table(name = "EORDER", schema = "EX_ONE_TO_MANY_A")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private LocalDateTime date = LocalDateTime.now();

    @OneToMany(mappedBy = "order",
            cascade = {CascadeType.PERSIST,
                    CascadeType.REMOVE},
            orphanRemoval = true)
    @ToString.Exclude
    private Set<Line> lines = new HashSet<>();

    public Order addLine(Line line) {
        line.setOrder(this);
        lines.add(line);
        return this;
    }

    public Order removeLine(Line line) {
        line.setOrder(null);
        lines.remove(line);
        return this;
    }
}
@Getter
@Setter
@ToString
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "LINE", schema = "EX_ONE_TO_MANY_A")
public class Line {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    @NonNull
    private String product;

    @NonNull // just for requiredargsconstructor
    private double price;

    @ManyToOne
    @JoinColumn(nullable = false)
    @ToString.Exclude
    private Order order;

    public void setOrder(Order order) {
        if (this.order != null) {
            this.order.getLines().remove(this);
        }
        this.order = order;
        if (order != null) {
            order.getLines().add(this);
        }
    }
}
21:30:45.953 [IJava-executor-0] INFO  notebook -- Order(id=1, date=2026-11-01T21:30:45.813350) [Line(id=3, product=Pen, price=1.0), Line(id=2, product=Paper, price=5.0), Line(id=1, product=Scissor, price=8.0)]
Figure 7: Les relations LINE et EORDER
order_id order_date line_id product price
1 2026-11-01 21:30:45.81335 1 Scissor 8.0
1 2026-11-01 21:30:45.81335 2 Paper 5.0
1 2026-11-01 21:30:45.81335 3 Pen 1.0
2 2026-11-01 21:30:45.823244 4 Car 20000.0
2 2026-11-01 21:30:45.823244 5 Moto 8000.0
Figure 8: SELECT * FROM EORDER INNER JOIN LINE

Many to Many (N-M)

L’annotation @ManyToMany en Java Persistence API (JPA) est utilisée pour modéliser une relation many-to-many entre deux entités. La table d’association sera alors générée et utilisée implicitement.

Figure 9: Un Customer a plusieurs adresses (qui peuvent être associées à d’autres)
Figure 10: Les relation CUSTOMER, ADDRESS et la relation de d’association
@Getter
@Setter
@ToString
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "CUSTOMER", schema = "EX_MANY_TO_MANY")
public class Customer {
    @Id
    @GeneratedValue
    private long id;

    @NonNull
    private String name;

    @ManyToMany
    @ToString.Exclude
    @JoinTable(schema = "EX_MANY_TO_MANY")
    private Set<Address> places = new HashSet<>();

    public void addPlace(Address address) {
        this.places.add(address);
        address.getOccupants().add(this);
    }

    public void removePlace(Address address) {
        this.places.remove(address);
        address.getOccupants().remove(this);
    }
}
@Getter
@Setter
@ToString
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "ADDRESS", schema = "EX_MANY_TO_MANY")
public class Address {
    @Id
    @GeneratedValue
    private long id;

    @NonNull
    private String addressDetail;

    @ManyToMany(mappedBy = "places")
    @ToString.Exclude
    private Set<Customer> occupants= new HashSet<>();

    public void addOccupant(Customer customer) {
        this.occupants.add(customer);
        customer.getPlaces().add(this);
    }

    public void removeOccupant(Customer customer) {
        this.occupants.remove(customer);
        customer.getPlaces().remove(this);
    }

}
21:30:51.354 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=bhnIm1k_9biMZKi_FoWslEBWfWDYzoagVX_7dX9MBgM-1793568651.3216243-1.0.1.1-PjfiZctXOyX_RmLGoh6hm8HrGZmqBZfu_PkfqbQIP8ijv4pWF_5TpPNzJ8FJQAo6Wd1.OH6StYnrIy1jH4j9sogJNOmAMQmARcFuFXB1Noxy_Yj5OJNLV2S9KPR2l6MS; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:51.457 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=WQxY1o_bKIHge1ijWlAYjswP0uMx.l26OuiZ4IEb0IQ-1793568651.436449-1.0.1.1-Go_00A1KktlCFx2Va7E0BO71psikE8vH4KBIkDOQFGuH61ZuonOKw6ddtMjuOSolt8b5Fq09aXG0fx0408JlnPIDTYPNXJpaoZiXpvWOSnvyM7D4MkVGVHCmqUG2NNRt; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:51.500 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=J1U_3srct1mWJLSMi6J9DoH6yzKwLrAbB4t.eFPlocM-1793568651.4770033-1.0.1.1-3Jgh3J7X2fJPw8PbKkH_cJihe3ZduWvBjy4aQ3ds.3wbP5e2QGnd.wXDfLeLzDVmGZgIiM6V1zSNrKk7YO40Ph_xYspi8CtE2L0gsSNuLeGXwpyjw6tvQxG7V21VDMb1; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:51.541 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=Aax3M_EfZYYyIZMfLpvo6wZ_eg2yKTriLTEs68z7720-1793568651.5193458-1.0.1.1-bFJfq1BB5aZzOUCNRhS96GNNiKI64hLX.0YOE0HXuIyZtFuNFFUuNKq4ZFqgOyJCgkBzac8x2GPNHh3F2riHfda1rPb3Gx.n7bSs.Fe.FlOGbf5zlxL66iK2Nhf2sG2f; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:51.587 [BasicRepositoryConnector-repo.maven.apache.org-0-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=vbUVTd0MlNxzLqdiQIBA.q9ZQcydYUy1kr4AKi8C0kY-1793568651.5644107-1.0.1.1-VeoknpPeER7rt_Bi8SFqyYGbBGhdOcJgBctlpNN4ysLoyjo6bGKy3eVFxO6bNH.w68ItAv2_M.VH2An8.dysrO9kQOl9qTOtsRQgcYVo8XyGLo4sNgiCRBobkXZczL.s; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:55.045 [BasicRepositoryConnector-repo.maven.apache.org-0-3] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=_O8pqnt2NqKTVt_5Re79To5d5wIoHoLmFwfUKv3uKw4-1793568651.6102548-1.0.1.1-aVOVKCAtgHB0W4tFSJ4P4gp91zAZ.ZlCUVol8kVpjtwyUwX6rbGlvlXwDjiViIOsU6jlxVK58NiWulKjzKgbYksviMuEaGBoDBfKJhFyxkA3D4wR9uvLPzq4dtXAxDhp; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
21:30:55.058 [BasicRepositoryConnector-repo.maven.apache.org-0-1] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=YASE_pGWMLWqYvfUO66F0RhwZMZVWhzEue7gf_GyF0Y-1793568655.0364096-1.0.1.1-3P9wOcxTGIzGjIWJrCRM.9FAigu0iB5JGPlJ39Ict4KKNlpN8C9HomIgkwweFRc8kIkj4CFAsQpLbiYusevrrAzF1PBLSLzctVVtN7NrjOmY0VAcyaX_2rNKfZZj3zE0; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:55 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:55 GMT
21:30:55.078 [BasicRepositoryConnector-repo.maven.apache.org-0-2] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=uMFKdBq9fgnTnFctxgrq0mYByKg8nJDA6kW1T_hU3Vc-1793568651.613071-1.0.1.1-Sdjs.lfFxAPjOxelJR4_yNmH3pfu72MttUnF1carn0a40n_iMGHMYzaTRuBC9XCDAm_erEExHqTdMelcE8UWVw7B2xEOK4miMR_r7Y7VEDM_pZtcHEOz1dqrC5hYQK2Y; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Sun, 01 Nov 2026 22:00:51 GMT". Invalid 'expires' attribute: Sun, 01 Nov 2026 22:00:51 GMT
id name
2 Dr. Keshia Sporer
3 Laureen Bahringer
4 Donita Mills
5 Erwin Hermiston
6 Mr. Darcel Schroeder
Figure 11: La table CUSTOMER
occupants_id places_id
2 1
3 1
4 1
5 1
6 1
2 2
4 2
5 2
6 2
Figure 12: La table de jointure
id addressdetail
1 104 Robbie Mill, New Ralphburgh, IA 69778
2 3165 Edgardo Loaf, South Felton, CA 55876
Figure 13: La table ADDRESS

Réutilisation