Les associations entre entités avec JPA

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

Université de Toulon

LIS UMR CNRS 7020

2026-11-09

[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
17:44:53.519 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Database settings: URL=jdbc:postgresql://dind/notebook-db, User=dba
17:44:53.525 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Starting JPA with URL: jdbc:postgresql://dind/notebook-db and User: dba
17:44:54.623 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
17:44:54.860 [IJava-executor-0] INFO  com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@2224a356
17:44:54.862 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Start completed.
--> Connexion JPA réussie (Tentative 1) !
[diag] cell init terminée ✓

Types de relations

  • Prise en charge complète: gestion des relations entre entités de différents types.
  • Types de relations
    • One-to-One (Un-vers-Un)
    • One-to-Many (Un-vers-Plusieurs)
    • Many-to-Many (Plusieurs-vers-Plusieurs)
  • Unidirectionnelle ou bidirectionnelle.

One To One (1-1)

  • 1-1 en Java mais une seule relation
Figure 1: Un Customer a une Biography
Figure 2: L’unique relation CUSTOMER
  • Pour cela, JPA propose :
    • @Embeddable: Indique que les membres d’une classe peuvent être persistés en tant qu’attributs de la classe qui les utilise.
    • @Embedded: Utilisé avec @Embeddable pour intégrer ses propriétés dans une autre entité

Un client a une et une seule biographie

@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

Le client lié

@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

Persistence de l’association

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 16775
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.
    • Pour une relation bidirectionnelle, l’autre entité (de type T) est annotée avec @ManyToOne.
    • L’attribut optionnel mappedBy indique le nom de l’attribut correspondant dans le type T si l’on souhaite une relation bidirectionnelle.

1 to N à deux entités (1/3)

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

1 to N à deux entités (2/3)

@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);
        }
    }
}

1 to N à deux entités (3/3)

17:45:00.721 [IJava-executor-0] INFO  notebook -- Order(id=1, date=2026-11-09T17:45:00.563805) [Line(id=2, product=Pen, price=1.0), Line(id=3, product=Scissor, price=8.0), Line(id=1, product=Paper, price=5.0)]

Figure 7: Les relations LINE et EORDER

order_id order_date line_id product price
1 2026-11-09 17:45:00.563805 1 Paper 5.0
1 2026-11-09 17:45:00.563805 2 Pen 1.0
1 2026-11-09 17:45:00.563805 3 Scissor 8.0
2 2026-11-09 17:45:00.575052 4 Moto 8000.0
2 2026-11-09 17:45:00.575052 5 Car 20000.0
Figure 8: SELECT * FROM EORDER INNER JOIN LINE

Many to Many (N-M)

  • @ManyToMany en Java Persistence API (JPA) :
    • Utilisée pour modéliser une relation many-to-many entre deux entités.
    • La table d’association est 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

Many to Many (1/2)

@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);
    }

}
17:45:05.849 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=cqKj5f0Rqs2Xs72sHcjKqhW1NmDfYyEEcK9VHBrmWGI-1794246305.8084242-1.0.1.1-TiUT1bhEYqGND13kUTjz_bpRmlpIJW0jMd8QWO5V0tHnaYn8Qyd2EhScsBltK9qOfIYHUm4aa3H1YwwsPMB4rwPBox8Yrn8Dvi3AcoBZ5UdxKJXgD43AQuZpzoF3_PY2; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:05 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:05 GMT
17:45:05.981 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=WN1tdEyFXyVZEhyky9SI1Gq.ZJaorz3KLqDJ5hgsL8Y-1794246305.942242-1.0.1.1-DkNCW3H74LtLY6Pr89pxcjFxCJm2mUZq7ROzhZS_oRpyZVZywXlP7_cqupBAptwUeSCXBoqHbB3wGvy_vMA_S9byPh4n2VDI56Kyd3VJ3PQzKb.PxD29Dt4vVoHPCH4B; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:05 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:05 GMT
17:45:06.038 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=qbp2XLLhqkVB72n0gyF_syP_YeHvESFUE1xbp2Hx_eQ-1794246306.0056064-1.0.1.1-Eazb7kQw3hQW1gEGL.bMF8W0fHzaWH9Us22vCIEOvfHJ7y.kFnkTBc74fA9s5EmVBYOPUQNxcosTf.6DZopvVlmbi1rNOTsfzGdP7cHclpWjwxMB15L1l9cpCyUCJTR5; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:06 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:06 GMT
17:45:06.120 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=KYTL9920eifWc270Gg4TfQCFFLYgFsEBqpDlofIHTV4-1794246306.0600016-1.0.1.1-UXX9gAc2t4raRUx76BK3Dw_67zZTI6jdmf5VJ9tNCueOYq8H1kQ8JfUFP1g3t47SyMPe2Ycco0r.Z45uaHgck5RgmjUuac_vNcsJ6MxZK7Ln95uy7qR8JIL0T46N49sk; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:06 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:06 GMT
17:45:06.180 [BasicRepositoryConnector-repo.maven.apache.org-0-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=IJwM4A4zG4gM53me9LEKYP1TuuN11kAfsAE26pE6GLU-1794246306.148865-1.0.1.1-Uh6ZIH4zg1jlZol1aJXy4EOhWhubu10sxNaPwnFXKDT5qcSR5PjbhLOY2TmAx.wAQhLj7Q5iVuN26va3RgsBz1z_Xh43ZA9pssct7UbI.xPCf9AGpeBcY6pQe0n8Xiwd; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:06 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:06 GMT
17:45:09.553 [BasicRepositoryConnector-repo.maven.apache.org-0-2] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=CQPRLkbI7O_.U4wzSaIJN1Hnv0hNO_3rVgoiHub5wRA-1794246306.196165-1.0.1.1-jAhqWXEwsWM2.O2VQkzzTJ7XDFnTSGZ7AKLFDYEFengde.o2rk.K6DxgifQe16pk3kz0NlsMmjhVII8tfqpxjvW4HRpCN0_HPS8MId9pgrHQjsyQSdHT2WsQN.MJvEMe; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:06 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:06 GMT
17:45:09.569 [BasicRepositoryConnector-repo.maven.apache.org-0-3] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=o6jgnvcMzypveoKY2l_0MjbTWRwo1ARrueK4RoC1VUg-1794246309.5363653-1.0.1.1-Im_qLxQEBlMTJI4qKjFYsyWAd9.9qTZkzk6JhOWevNERQstQq7pBOBCNVNduvbU8FIqqxvIqscPI.xBm9M7s7ShxS64IBXzocdWg3sptmMqaxONLhnM8ICC.ot7ZK2Bd; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:09 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:09 GMT
17:45:09.574 [BasicRepositoryConnector-repo.maven.apache.org-0-1] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=_hi.fdcZpchjyQDNCRFpgc_7sb_7hlt1yX18SqEOBfY-1794246306.1988606-1.0.1.1-nlLWhy1pm0neZuaFEf5hN3FUro2YClK0nGFC08mjBzlv32lRSg6poTNESo7i.SiBPisyE8Gfhz5PUEek0DPrJh7sTqIe4e2MvGZczTVRK1KEDin9QBCTpjrroqM5pv7V; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 09 Nov 2026 18:15:06 GMT". Invalid 'expires' attribute: Mon, 09 Nov 2026 18:15:06 GMT

Many to Many (2/2)

id name
2 Brittanie Rice
3 Kareem Walter
4 Napoleon Auer
5 Korey Kuphal II
6 Blaine McClure IV
Figure 11: La table CUSTOMER
occupants_id places_id
2 1
3 1
5 1
6 1
2 2
3 2
4 2
Figure 12: La table de jointure
id addressdetail
1 Apt. 316 837 Becker Bridge, Terryton, PA 37079
2 Apt. 959 093 Leopoldo Pine, Mayerttown, LA 58278
Figure 13: La table ADDRESS