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

[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
07:48:32.118 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Database settings: URL=jdbc:postgresql://dind/notebook-db, User=dba
07:48:32.125 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Starting JPA with URL: jdbc:postgresql://dind/notebook-db and User: dba
07:48:33.294 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
07:48:33.552 [IJava-executor-0] INFO  com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@2224a356
07:48:33.553 [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
@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)

07:48:39.624 [IJava-executor-0] INFO  notebook -- Order(id=1, date=2026-11-02T07:48:39.463459) [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-02 07:48:39.463459 1 Scissor 8.0
1 2026-11-02 07:48:39.463459 2 Paper 5.0
1 2026-11-02 07:48:39.463459 3 Pen 1.0
2 2026-11-02 07:48:39.474263 4 Car 20000.0
2 2026-11-02 07:48:39.474263 5 Moto 8000.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);
    }

}
07:48:44.919 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=O64BFv_tidDAFcH3EsIMVYdT8S0rou64evJEabcWZ3k-1793605724.888351-1.0.1.1-Y6BEusmv3788uB4AiZ3MVsEBTAbJpL5PJDyQqsUze6p5M4qGZn4QW4Fz0VMZdxJlfn31oJGQkb546.XOb9dB1e2gLNVawYoFDkfRe1_VDnVX1WBoUYQEF5l2.fyb0PpB; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:44 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:44 GMT
07:48:45.039 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=mAil4a615Df3lroaWqWgbDQ1fm3GAcxilkvUidudjyE-1793605725.0181267-1.0.1.1-T8QEhIq4vPVE91POdnza1IBZhsn1bDS0.lS2M2W2H_f3h6CC93aFwkdnuyIK7UrYy2Wb8WZi8CPh_ML5Zn9a13BNJXZOxl52tgWhjUZXvJUDu1aOxc.uOJeXIC39csaB; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:45.085 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=QNBLc7R6lbMnmwrrXhjh3P1e9ge8o1JAPW8vawLQDxM-1793605725.0591228-1.0.1.1-HBL5CJJsw9nJJRiP.jS3sxUiXILxPQEnyFjgrQ.oa95n5rW9VmYYDv8SmRCuAbrDaZnm3OGol65FlomESegV9.ERaPJRNqW2gfxq8D4KUOrKzWpAUyfyIAGcjVfn1._Z; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:45.169 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=Pedh2gbdXAkqGB_dQx0EHw0z5kyM2axuI1.TW0yWTro-1793605725.1477327-1.0.1.1-BW.eg_2dK_ygj74a40T1NknV07B4ler89PLkSHQkL.DQKoH26LZ.ZWx38N0gZmLDQZIATCqn.gffaeah3NMWDKimmd0jK3G_LGSqHPYGWikhl8RAmp9z5Jm3XcZwZ9Vk; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:45.218 [BasicRepositoryConnector-repo.maven.apache.org-0-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=oVNr5rY.Rn63CjOD9MItWLNLOEDV0HWZDb3Ggxu_FBk-1793605725.1963098-1.0.1.1-ce4OzELYwECWpo_BtNL5ypHxkuJ2auvO0HMj2qLvR2ivas3cTSxXRn9MlL4tNOBSxtnSnss8LynH1eHPBP.4tejGEC45LzLrFIbO_2SeIyatRRflMfOSS9YNLP4_UJjQ; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:48.566 [BasicRepositoryConnector-repo.maven.apache.org-0-1] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=9RsV.zuqswmv.Tw95Olz1M9jfAzfsw0.gPxL2px2TBY-1793605725.2421339-1.0.1.1-UMbRLQczSMOU.JGYakL9kniQLYt7mYsSUx4c.eowGboHgcOX1PdOnsUQ6DLozApZPg0PNuPrlEe1Nxp12XKyTMLU2Kpoq44bxwR1C7.Wj1Qmq0nJYoXQMbBuzHlsrZzB; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:48.574 [BasicRepositoryConnector-repo.maven.apache.org-0-2] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=ZAbogQb_BP_H2qskhMULtPygd4UpUTVO2NNFsSahf1g-1793605725.2427394-1.0.1.1-mOyYT.Zqpt8lDyOjzF5yDSm859lMe_x9td04KPyg291PrVgd9JT5lSlUXv7v8hLvMe1c5dr_GegncdIKLb1NcdjzPH6psvsnkcNnLMyYylJVaEKnZx_M_SHOLRCIZbsR; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:45 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:45 GMT
07:48:48.657 [BasicRepositoryConnector-repo.maven.apache.org-0-3] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=gYkqXxCwKQ4YxbO01KATVoJSnEf6195BRRzTjgUqWJ0-1793605728.6285725-1.0.1.1-gEi783d18XlKbIlQ_mXBuuUqJRao249LwfnVxc.XTuclb8BzBIOZrUh8sN8UflQi86hFz83vn04TiS3sOsTBw_RegRwsjHRMfHVavO9A6oE8Gd6u.gZp2plS4h5bot01; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Mon, 02 Nov 2026 08:18:48 GMT". Invalid 'expires' attribute: Mon, 02 Nov 2026 08:18:48 GMT

Many to Many (2/2)

id name
2 Lyndia Jast IV
3 Mr. Bobby O'Hara
4 Mrs. Millie Schimmel
5 Isidro Bergstrom
6 Lucina Wuckert
Figure 11: La table CUSTOMER
occupants_id places_id
2 1
3 1
4 1
5 1
2 2
3 2
4 2
6 2
Figure 12: La table de jointure
id addressdetail
1 Suite 885 14671 Stehr Fall, Ronaldburgh, NJ 17885
2 3412 Lorette Ridge, Marcchester, OK 76299
Figure 13: La table ADDRESS