Cheat SheetsHibernate & JPARelationships

Relationships — Cheat Sheet

Hibernate & JPA · 7 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Relationships
Hibernate & JPA7 topicsQuick revision reference
1

One-to-One Mapping

@OneToOne maps two entities with a shared or foreign-key strategy; use @MapsId to share the primary key between parent and child for an efficient 1:1 relationship.

  • @OneToOne with FK: child has a unique FK column; @JoinColumn names the column.
  • @MapsId shares the parent PK as the child PK — no extra column, most efficient strategy.
  • Always declare @OneToOne with fetch = FetchType.LAZY to avoid unintended joins.
  • The non-owning side (mappedBy) cannot be truly lazy without bytecode instrumentation.
  • Use cascade = CascadeType.ALL + orphanRemoval = true to manage child lifecycle via parent.
  • Query via the owning side (FK holder) for correct lazy loading behaviour.
Java — bidirectional @OneToOne with FK
// Parent entity — User
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;

    // Bidirectional — mappedBy points to the field in UserProfile that owns the FK
    @OneToOne(mappedBy = "user",
              fetch = FetchType.LAZY,
              cascade = CascadeType.ALL,
              orphanRemoval = true)
    private UserProfile profile;
}

// Child entity — UserProfile owns the FK
@Entity
@Table(name = "user_profiles")
public class UserProfile {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", unique = true)   // FK column in user_profiles
    private User user;

    private String bio;
    private String avatarUrl;
}

// Create with cascade
User user = new User("alice@example.com");
UserProfile profile = new UserProfile("Java developer", "/avatars/alice.jpg");
profile.setUser(user);
user.setProfile(profile);
userRepository.save(user);  // cascades to UserProfile
2

One-to-Many Mapping

@OneToMany on the parent with @ManyToOne on the child side creates a bidirectional relationship; a join column on the many side holds the FK; mappedBy avoids a redundant join table.

  • The @ManyToOne side is the owning side — it holds the FK column; @OneToMany(mappedBy=) tells JPA not to create an extra join table.
  • Always maintain both sides of a bidirectional relationship: set child.setParent(parent) AND parent.getChildren().add(child).
  • CascadeType.ALL propagates all operations to children; orphanRemoval=true deletes children removed from the collection.
  • Never use CascadeType.REMOVE (or ALL) on large collections — Hibernate loads every child into memory before deleting; use a bulk @Modifying @Query instead.
  • Two @OneToMany JOIN FETCHes in one JPQL query creates a Cartesian product — use separate queries or @EntityGraph subgraphs.
  • Implement equals/hashCode on a business key (UUID or natural key), not the auto-generated ID (which is null before persist).
Java — Bidirectional One-to-Many
// PARENT — one Order has many OrderItems
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(
        mappedBy  = "order",          // "order" = field name in OrderItem
        cascade   = CascadeType.ALL,  // persist/remove propagates to items
        orphanRemoval = true,         // delete orphaned items from DB
        fetch     = FetchType.LAZY    // do NOT load items unless requested
    )
    private List<OrderItem> items = new ArrayList<>();

    // Helper method — keeps BOTH sides consistent
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);  // set the owning side
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
    }
}

// CHILD — many OrderItems belong to one Order
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)   // owning side — FK is here
    @JoinColumn(name = "order_id")       // column name in order_items table
    private Order order;

    private String productName;
    private int quantity;
    private BigDecimal price;
}
3

Many-to-One Mapping

@ManyToOne is the owning side and physically holds the FK column; it is the simplest and most common relationship — e.g., many Orders to one Customer.

  • @ManyToOne is always the owning side and holds the FK column (@JoinColumn).
  • JPA defaults @ManyToOne to FetchType.EAGER — always override to FetchType.LAZY.
  • The inverse @OneToMany (mappedBy) is optional and does not create any DB column.
  • Keep bidirectional relationships in sync with helper methods on the parent.
  • N+1 queries occur when accessing a LAZY @ManyToOne in a loop — fix with JOIN FETCH or @EntityGraph.
  • @BatchSize reduces N+1 from N round trips to N/batchSize round trips.
Java — bidirectional @ManyToOne / @OneToMany
// Order (child) → Customer (parent): many orders, one customer
@Entity
@Table(name = "orders")
public class Order {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // FetchType.EAGER is the JPA default for @ManyToOne — always override!
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    private OrderStatus status;
    private BigDecimal total;
}

// Customer (parent) — owns no FK; inverse side is optional
@Entity
@Table(name = "customers")
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;

    // Optional inverse navigation — does NOT change DB schema
    @OneToMany(mappedBy = "customer",  // "customer" = field name in Order
               fetch = FetchType.LAZY,
               cascade = CascadeType.ALL,
               orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();
}
4

Many-to-Many Mapping

@ManyToMany uses a join table; prefer extracting the join table as an explicit entity (with extra columns like createdAt) to avoid limitations of the implicit join-table approach.

  • @ManyToMany implicitly manages a join table but cannot hold extra columns
  • Hibernate deletes and re-inserts all join-table rows on List mutations — use Set instead
  • Extract the join table as an explicit @Entity to add metadata columns and control SQL
  • @EmbeddedId with @MapsId maps the composite PK to the two foreign-key columns
  • With an explicit join entity, Student and Course become @OneToMany(mappedBy=...)
  • The owning side of a bidirectional @ManyToMany is the side with @JoinTable
JPA — implicit @ManyToMany
@Entity
public class Student {
    @Id @GeneratedValue Long id;
    String name;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id @GeneratedValue Long id;
    String title;

    // Bidirectional — mappedBy refers to the owning side field
    @ManyToMany(mappedBy = "courses")
    List<Student> students = new ArrayList<>();
}
5

@JoinColumn & @JoinTable

@JoinColumn customises the FK column name on the owning side; @JoinTable specifies the join table name and both FK column names for a @ManyToMany relationship.

  • @JoinColumn controls the FK column name, nullability, and constraint name on the owning side
  • @JoinTable names the join table and both FK columns for @ManyToMany relationships
  • The owning side (has @JoinColumn / @JoinTable) is the side whose changes are persisted
  • The inverse side (mappedBy) is a mirror — mutations here are ignored by Hibernate
  • Always declare @ManyToMany with FetchType.LAZY and use Set to prevent bag fetch exceptions
  • referencedColumnName in @JoinColumn lets you FK to a non-PK unique column
Java — @JoinColumn on @ManyToOne with custom FK name
@Entity
public class OrderItem {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(
        name = "order_id",          // FK column name in order_items table
        nullable = false,
        foreignKey = @ForeignKey(name = "fk_order_item_order")  // named FK constraint
    )
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", referencedColumnName = "id")
    private Product product;
}
6

Cascade Types

CascadeType.ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH propagate operations from parent to child; avoid CascadeType.REMOVE on large collections — use bulk queries instead.

  • Six cascade types: PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL (shorthand)
  • CascadeType.REMOVE loads all children into memory before deleting — avoid on large collections
  • orphanRemoval removes a child when it's dereferenced from the parent's collection
  • CascadeType.ALL is safe for private, owned children (OrderItem) but dangerous for shared entities (Tag)
  • Use PERSIST + MERGE for most parent-child relationships; add orphanRemoval separately if needed
  • For large collection deletes, always prefer a bulk JPQL/SQL DELETE over cascaded removes
JPA — PERSIST + MERGE cascade with orphanRemoval
@Entity
public class Order {
    @Id @GeneratedValue Long id;

    // PERSIST + MERGE: propagate save/update; NOT remove
    @OneToMany(mappedBy = "order",
               cascade = {CascadeType.PERSIST, CascadeType.MERGE},
               orphanRemoval = true)
    List<OrderItem> items = new ArrayList<>();
}

// em.persist(order) → also persists all items in the list
Order order = new Order();
order.getItems().add(new OrderItem(order, "SKU-1", 2));
em.persist(order);   // items saved automatically

// Remove a single item from the collection
order.getItems().removeIf(i -> i.getSku().equals("SKU-1"));
// orphanRemoval=true → Hibernate fires DELETE for that item on flush
7

FetchType — EAGER vs LAZY

LAZY loads related entities on first access (default for collections); EAGER loads immediately with a JOIN (default for @ManyToOne, @OneToOne); always prefer LAZY to avoid unintended queries.

  • LAZY (default for collections) defers loading until the proxy method is accessed; EAGER (default for @ManyToOne) loads immediately with a JOIN.
  • Always override the @ManyToOne EAGER default to LAZY — let JOIN FETCH or @EntityGraph control loading explicitly per query.
  • LazyInitializationException occurs when a LAZY proxy is accessed after the Hibernate Session is closed; fix with @Transactional, JOIN FETCH, or DTO projections.
  • Never put EAGER on a @OneToMany or @ManyToMany — it embeds an N+1 problem directly into your mapping.
  • Prefer DTO projections (SELECT new MyDto(...)) for read-only use cases — they bypass the proxy mechanism entirely.
  • spring.jpa.open-in-view=true (Spring Boot default) keeps the session open during the HTTP request — convenient but hides lazy loading in the view layer; set it to false in production.
Java — JPA / Hibernate
@Entity
public class Order {
    @Id Long id;
    String reference;

    // ✅ LAZY (explicitly declared) — no query until accessed
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // ❌ EAGER on a collection — causes N+1 on every Order query
    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    private List<OrderItem> items;  // Never do this on a collection!
}

// LAZY proxy demonstration
Order order = orderRepo.findById(1L).get();  // SELECT order — customer NOT loaded yet
String ref = order.getReference();            // OK — no association access
String name = order.getCustomer().getName();  // SELECT customer — proxy fires HERE
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/hibernate