Les associations entre entités avec JPA

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

Université de Toulon

LIS UMR CNRS 7020

2026-10-14

[postgres] starting...
[postgres] 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 %jars OK
[diag] Postgres driver registered OK
[diag] 5/5 imports JPA/DatabaseManager OK
17:20:43.587 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Database settings: URL=jdbc:postgresql://dind/notebook-db, User=dba
17:20:43.595 [IJava-executor-0] INFO  f.u.b.d.jpa.hello.DatabaseManager -- Starting JPA with URL: jdbc:postgresql://dind/notebook-db and User: dba
17:20:44.794 [IJava-executor-0] INFO  com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
17:20:45.039 [IJava-executor-0] INFO  com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@23aa2f2d
17:20:45.041 [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

```xbayay execute: false //| output: true //| echo: false //| label: fig-rel-onetoone //| fig-cap: L’unique relation CUSTOMER //| vscode: {languageId: java} // PRINT SCHEMA String script=“%”;

IJava.getKernelInstance().getMagics().applyCellMagic(“rdbmsSchema”, List.of(“ex_biography”),script); return null;


:::
::::

::: {.content-visible when-profile="notes"}
Pour cela JPA propose des annotations spécifiques :

[\@Embeddable](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/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](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/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.
:::


::: {.content-visible when-profile="slides"}
- Pour cela, JPA propose :
    - [\@Embeddable](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/embeddable): Indique que les membres d'une classe peuvent être persistés en tant qu'attributs de la classe qui les utilise.
    - [\@Embedded](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/embedded): Utilisé avec `@Embeddable` pour intégrer ses propriétés dans une autre entité
:::

::: {.content-visible when-profile="slides"}
## Un client a une et une seule biographie {.smaller}
:::

:::: {.columns}

::: {.column width="50%"}

::: {#cell-fig-class-embeddable .cell filename='null' quarto-private-1='{"key":"vscode","value":{"languageId":"java"}}' execution_count=4}

::: {#fig-class-embeddable .cell-output .cell-output-display .cell-output-markdown}
```Java
@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;
}

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

No JDBC connection available. Set system properties 'jdbc.url' (and optionally 'jdbc.user'/'jdbc.password'), or provide a Connection in the kernel environment.
Figure 2

One to Many (1-N)

Figure 3: 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<>();

}

xbayay execute: false //| output: true //| echo: false //| label: fig-rel-onetomanyb //| fig-cap: Les relations LINE et EORDER //| tags: [remove-input] //| vscode: {languageId: java} // PRINT SCHEMA String script="%"; IJava.getKernelInstance().getMagics().applyCellMagic("rdbmsSchema", List.of("ex_one_to_many_b"),script); return null;

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:20:50.373 [IJava-executor-0] INFO  notebook -- Order(id=1, date=2026-10-14T17:20:50.212876) [Line(id=1, product=Pen, price=1.0), Line(id=3, product=Scissor, price=8.0), Line(id=2, product=Paper, price=5.0)]

```xbayay execute: false //| output: true //| echo: false //| label: fig-rel-onetomanya //| fig-cap: Les relations LINE et EORDER //| tags: [remove-input] //| vscode: {languageId: java} // PRINT SCHEMA String script=“%”;

IJava.getKernelInstance().getMagics().applyCellMagic(“rdbmsSchema”, List.of(“ex_one_to_many_a”),script); return null;


::: {#fig-rel-onetomanya-query .cell quarto-private-1='{"key":"vscode","value":{"languageId":"java"}}' execution_count=16}

::: {.cell-output .cell-output-stdout}

No JDBC connection available. Set system properties ‘jdbc.url’ (and optionally ‘jdbc.user’/‘jdbc.password’), or provide a Connection in the kernel environment.

:::
:::


## Many to Many (N-M)

::: {.content-visible when-profile="notes"}
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.
:::

::: {.content-visible when-profile="slides"}
- \@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.
:::

::: {#cell-fig-uml-manytomany .cell quarto-private-1='{"key":"vscode","value":{"languageId":"java"}}' execution_count=17}

::: {.cell-output .cell-output-display}
![Un Customer a plusieurs adresses (qui peuvent être associées à d'autres)](02_java_jpa_L_associations_files/figure-revealjs/fig-uml-manytomany-output-1.svg){#fig-uml-manytomany}
:::
:::


```{java} execute: false
//| output: true
//| echo: false
//| label: fig-rel-manytomany
//| fig-cap: Les relation CUSTOMER, ADDRESS et la relation de d'association
//| tags: [remove-input]
//| vscode: {languageId: java}
// PRINT SCHEMA
String script="%";

IJava.getKernelInstance().getMagics().applyCellMagic("rdbmsSchema",
  List.of("ex_many_to_many"),script);
return null;

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:20:51.797 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=cHvAo3WS6bzFbGe0bcdY1i7elgAqMWuOvpXKtVyzxCk-1791998451.7555158-1.0.1.1-_xkoYchWz1V_fELE2Pe3ihqL2c40PsEDtsBa2TrsuV_2AoqIvY0ZxZy35yaGQdHXdp.nXA5_in6Im1L.pBnZTKU1lv1B.UGflYyctx6p00hnOmcMaSj40GKYc3iWcmHA; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:51 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:51 GMT
17:20:51.903 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=BQGaevAk3V2V1JcMjGRiaogAPdXwxMLv7KQE.KAZ1v0-1791998451.8755271-1.0.1.1-Ib2K3jVSxrEvmccLAjR9cMJBHs.Hl052m2DZLU0ZfUjN08EfFJl0RV0SvUoVxQ2Iu1e03pBzl0VE3tR4_bgSNPb_EH0m_8IxZmZtPFMFwKhN8FfxAqAUtwhx.2fBVy93; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:51 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:51 GMT
17:20:51.950 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=ytsG3kHjMRHkgxKcb0znTVOtuOs1hoEghFwJeCypatY-1791998451.9215276-1.0.1.1-YD.mQcJsML8RoUbA_QpqFegecR2jiMzt7UfGO8d4ioPgsJYlBTLtk4ZO8I4KrSFNYSgxMJ9Y6iRtx_G35xmIw3n6pmP3Ayh5Q4bq8d1svjBAaFuKtgBnl1ea5lULpcYR; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:51 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:51 GMT
17:20:51.993 [IJava-executor-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=gx1y_gwj9GLo9puAO61knpWlIuc7jybyAvmn6b9ajC4-1791998451.9679925-1.0.1.1-hFpiR6OQ_Y8EnaNU8.vFCVYLi1p.yirWg7QMrZ2faVkahFz2QH6qsiHrpZiL9aQlMiQlFwXSUsHj90EEApxUr2vEgu4JE68k2QQk4iLSrmiShaXX0E0cVVSRRMU8dU03; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:51 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:51 GMT
17:20:52.039 [BasicRepositoryConnector-repo.maven.apache.org-0-1] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=u0TffMGj4E9I.D2fKWgux9dcoLjvJzhe5n4yxp0J9eE-1791998452.016704-1.0.1.1-WwIAzs.LwqXFEEv9QU00mjn021_WJORAA9EF7JbeMLIgBcOmym4m7qKYlh4_I.MGowLRdRbH_CgtGLuqDtHlp9bnFa7rbroLLHI6G5TOyeAkcgLYEfUWUZWQf1JEuwDB; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:52 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:52 GMT
17:20:52.094 [BasicRepositoryConnector-repo.maven.apache.org-0-0] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=keXWv4lazwVxm8h.VunjbTvgZTReE81rNI5JLIQ2.uY-1791998452.0669894-1.0.1.1-vb8ScoLqrP6.C0n5Nhn0esQqCmtavR7Wi_xq_WpSf7rd64gDXLVVnJcLUJEnbC9DYC_fVHhTr1eyhalHCBpyoeiyfZl1ECcOdHyh3V_CnpU2xbjJ7E6ljwMf8DVA5AC_; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:52 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:52 GMT
17:20:52.095 [BasicRepositoryConnector-repo.maven.apache.org-0-3] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=03T8DwdrwC6hzgSyWCEonDTlGNcMdqqAdfU7mvDZC5E-1791998452.065174-1.0.1.1-ZBgwmR0yFYp06qqUyLoluQo1haGI8SycuKFU7xDX9k5ziurMTIGBU6KQQcjjjNtgILIxPzbSnS312b8.aZ4TpjrYFj9JaFUW_iYfxstdqvZRoGtauvmXsam4CzPLmboD; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:52 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:52 GMT
17:20:52.100 [BasicRepositoryConnector-repo.maven.apache.org-0-2] WARN  o.a.h.c.p.ResponseProcessCookies -- Invalid cookie header: "set-cookie: __cf_bm=HwymWgGBBYbL7HcCXaOUf0w1w4.DrSz4XdrWBfCI5EM-1791998452.0742066-1.0.1.1-eRjtXKlZmPRZ76fRIDRjryy2yV.X7Fj_zoTznVWpKRPniFHvg9DTLp4o7mukvw8N0tA9MXctc1VCoCjVGM8mEKOXVV6KerXjQZT8ZVKLaa9g8cpSb1vEp532TXlUOUhZ; HttpOnly; SameSite=None; Secure; Path=/; Domain=repo.maven.apache.org; Expires=Wed, 14 Oct 2026 17:50:52 GMT". Invalid 'expires' attribute: Wed, 14 Oct 2026 17:50:52 GMT

Many to Many (2/2)

No JDBC connection available. Set system properties 'jdbc.url' (and optionally 'jdbc.user'/'jdbc.password'), or provide a Connection in the kernel environment.
Figure 4
No JDBC connection available. Set system properties 'jdbc.url' (and optionally 'jdbc.user'/'jdbc.password'), or provide a Connection in the kernel environment.
Figure 5
No JDBC connection available. Set system properties 'jdbc.url' (and optionally 'jdbc.user'/'jdbc.password'), or provide a Connection in the kernel environment.
Figure 6