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 One to Many Mapping


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

  1. 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 + "}";
         }
     }
    
       
    
  2. 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 Entity Invoice - 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/persist Invoice having the InvoiceLine, it persists into table invoice obviously and also into invoice_line table.

Use of orphanRemoval = true in Parent Entity Invoice - 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 an InvoiceLine from the Invoice and save/persist the Invoice, that specific invoice_line record will be deleted from invoice_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.

DB Schema Picture


In sum, we have 2 entities Invoice, InvoiceLine having a dedicated repository interface for each entity like InvoiceRepository and InvoiceLineRepository.

  • Having CascadeType.ALL in Invoice enables to save/persist InvoiceLine when Invoice is saved to DB.
  • And, with configuration orphanRemoval=true enables Invoice entity to delete InvoiceLine through InvoiceRepository.

Refer my GitHub repo otm-and-mto-jpa-mapping-with-spring-boot for complete sample.


References

  1. 1-Many relationship
  2. cascadeType.All vs orhpanRemoval=true
  3. Should entities implement Serializable
  4. Deleting entity object