Relationships — Cheat Sheet
Hibernate & JPA · 7 topics. Download the PDF or the Instagram carousel and share it.
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.
// 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 UserProfileOne-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).
// 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;
}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.
// 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<>();
}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
@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<>();
}@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
@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;
}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
@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 flushFetchType — 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.
@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