1-Many and Many-1 JPA Mapping with Spring Boot
This post focuses on establishing 1-Many and Many-1 relationship between two entities in JPA.
Terminology
- Owning Side or Child Entity: From a database standpoint it’s a table having the foreign key reference of the other tables. And, from JPA standpoint it is an entity having the
@JoinColumn
annotation - Non-Owning Side or Inverse Side or Parent Entity: From JPA perspective, it’s an entity having a reference of owning side with
mappedBy
attribute passed to any of the annotations say@OneToOne
,@OneToMany
or@ManyToMany
Database Schema:
Here is the relationship between the tables invoice
, and invoice_line
JPA Entities:
Spring Boot application with spring-boot-starter-data-jpa
dependency comes along with Hibernate
as JPA implementation.
Here is the code excerpts of entities
-
Invoice
package otm.mto; import java.util.Set; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; @Entity(name = "Invoice") @Table(name = "invoice") @NoArgsConstructor @Getter public class Invoice { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "invoice_id_pk") private Long id; @Column(name = "total_price") private Double totalPrice; @OneToMany( mappedBy = "invoice", cascade = CascadeType.ALL, // Defaults to no operations being cascaded. orphanRemoval = true, // default is false fetch = FetchType.EAGER) // default is FetchType.LAZY /*- Note: With LAZY fetch it would result in this exception Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: otm.mto.Invoice.invoiceLines: could not initialize proxy - no Session */ /*- What if the invoiceLines count is huge for a given Invoice * may not applicable for this example * Point is use eager fetching judiciously * Recommended to apply @EntityGraph(attributePaths = {"invoiceLines"}) * on each custom fetch method in InvoiceRespository */ private Set<InvoiceLine> invoiceLines; public Invoice(Double totalPrice) { this.totalPrice = totalPrice; } public void setTotalPrice(Double totalPrice) { this.totalPrice = totalPrice; } public void setInvoiceLines(Set<InvoiceLine> invoiceLines) { this.invoiceLines = invoiceLines; } @Override public String toString() { return "{ invoice_id=" + id + ", totalPrice=" + totalPrice + "}"; } }
-
InvoiceLine
package otm.mto; import java.util.Objects; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; @Entity(name = "InvoiceLine") @Table(name = "invoice_line") @NoArgsConstructor @Getter public class InvoiceLine { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "invoice_line_id_pk") private Long id; @Column(name = "item") private String item; @Column(name = "unit_price") private Double unitPrice; @Column(name = "quantity") private Integer quantity; public InvoiceLine(String item, Double unitPrice, Integer quantity, Invoice invoice) { this.item = item; this.unitPrice = unitPrice; this.quantity = quantity; this.invoice = invoice; } @ManyToOne @JoinColumn(name = "invoice_id_fk", referencedColumnName = "invoice_id_pk", nullable = false) private Invoice invoice; @Override public String toString() { return "{ item=" + item + ", unitPrice=" + unitPrice + ", quantity=" + quantity + "}"; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (!(o instanceof InvoiceLine)) return false; InvoiceLine that = (InvoiceLine) o; return this.getItem().equals(that.getItem()) && this.getUnitPrice().equals(that.getUnitPrice()) && this.getQuantity().equals(that.getQuantity()); } @Override public int hashCode() { return Objects.hash(item, unitPrice, quantity); } }
This example depicts the Bidirectional association or two-way mapping between Invoice
and InvoiceLine
entities. In simple English, given an Invoice
we can fetch all the associated InvoiceLine
and vice versa.
Significance of CascadeType
, FetchType
, orphanRemoval
jakarta.persistence.FetchType
From the javadoc: Defines strategies for fetching data from the database. The EAGER strategy is a requirement on the persistence provider runtime that data must be eagerly fetched. The LAZY strategy is a hint to the persistence provider runtime that data should be fetched lazily when it is first accessed. The implementation is permitted to eagerly fetch data for which the LAZY strategy hint has been specified.Use of
CascadeType.ALL
in Parent EntityInvoice
- Child entities will be affected by all these operations on the parent entity. For example, if we delete the parent entity, the associated child entities will also be deleted, similarly if we save/persistInvoice
having theInvoiceLine
, it persists into tableinvoice
obviously and also intoinvoice_line
table.Use of
orphanRemoval = true
in Parent EntityInvoice
- When we remove a child entity from the parent’s collection, it will be automatically removed (deleted) from the database. For example if we remove anInvoiceLine
from theInvoice
and save/persist theInvoice
, that specificinvoice_line
record will be deleted frominvoice_line
table
Here is the H2 db schema and its state created from the application having configuration spring.jpa.hibernate.ddl-auto=create-drop
set in application.yaml
.
In sum, we have 2 entities Invoice
, InvoiceLine
having a dedicated repository interface for each entity like InvoiceRepository
and InvoiceLineRepository
.
- Having
CascadeType.ALL
inInvoice
enables to save/persistInvoiceLine
whenInvoice
is saved to DB. - And, with configuration
orphanRemoval=true
enablesInvoice
entity to deleteInvoiceLine
throughInvoiceRepository
.
Refer my GitHub repo otm-and-mto-jpa-mapping-with-spring-boot
for complete sample.