Entity Relationships
Intermediate@OneToMany, @ManyToOne, and @ManyToMany map object relationships to foreign keys and join tables. Choosing the right fetch type and understanding the owning side prevents the N+1 problem and LazyInitializationException.
Overview
JPA relationships have an owning side (holds the foreign key column or join table) and an inverse side (mapped with mappedBy). Fetch type controls when related data is loaded: LAZY (default for collections — load on access) vs EAGER (always load with parent — dangerous for collections). The N+1 problem occurs when loading N entities triggers N additional queries for their collections. Use JOIN FETCH in JPQL or @EntityGraph to batch-load associations.
@OneToMany and @ManyToOne
The most common relationship. The @ManyToOne side holds the foreign key and is the owning side. @OneToMany uses mappedBy to point to the field on the owning side. Always use bidirectional helper methods to keep both sides in sync in memory.
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private String id;
@ManyToOne(fetch = FetchType.LAZY) // owning side — holds customer_id FK
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// mappedBy = field name in OrderItem that owns the relationship
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
orphanRemoval = true, fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
// ✅ Helper method — keeps both sides in sync
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);
}
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order; // owning side — holds order_id FK
@Column(nullable = false)
private String productId;
@Column(nullable = false)
private int quantity;
}Fetch Types and the N+1 Problem
LAZY is the safe default — data loads only when accessed. EAGER on a collection causes the full collection to load with every query. The N+1 problem: loading 10 orders fires 1 query for orders + 10 queries for items. Fix with JOIN FETCH or @EntityGraph.
// ❌ N+1 problem — 1 query for orders + N queries for items
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> System.out.println(o.getItems().size())); // N lazy loads
// ✅ Fix 1: JPQL JOIN FETCH in repository
public interface OrderRepository extends JpaRepository<Order, String> {
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :customerId")
List<Order> findByCustomerWithItems(@Param("customerId") String customerId);
}
// ✅ Fix 2: @EntityGraph — select which associations to fetch
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByStatus(OrderStatus status);
// ✅ Fix 3: Batch fetching (in-clause instead of per-row)
// In application.yml:
// spring.jpa.properties.hibernate.default_batch_fetch_size: 50
// Hibernate will load items for 50 orders in one IN(...) query
// ❌ Never use EAGER on collections
@OneToMany(fetch = FetchType.EAGER) // always loads all items — dangerous
private List<OrderItem> items;@ManyToMany and Cascade Types
Many-to-many relationships need a join table. Use an explicit join entity (rather than @ManyToMany directly) when the join table has extra columns. Cascade types control which operations propagate: ALL is convenient but dangerous — never cascade REMOVE from a non-owning side.
// ✅ Explicit join entity — when join table has extra columns
@Entity
@Table(name = "course_enrollments")
public class CourseEnrollment {
@EmbeddedId
private CourseEnrollmentId id = new CourseEnrollmentId();
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("userId")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("courseId")
private Course course;
private LocalDateTime enrolledAt = LocalDateTime.now();
private EnrollmentStatus status = EnrollmentStatus.ACTIVE;
}
@Embeddable
public record CourseEnrollmentId(String userId, String courseId)
implements Serializable {}
// Cascade types explained:
// PERSIST — saving parent auto-saves children
// MERGE — merging parent auto-merges children
// REMOVE — deleting parent deletes children (dangerous on @ManyToMany)
// REFRESH — refreshing parent refreshes children
// DETACH — detaching parent detaches children
// ALL — all of the above
// Safe cascade for @OneToMany owned children:
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // items don't exist without the orderKey Points to Remember
- 1The owning side (has the FK column) controls what gets written to the database — always set it.
- 2Use bidirectional helper methods (addItem/removeItem) to keep both sides in sync in memory.
- 3LAZY is the default for collections and the safe choice — EAGER on collections causes full loads on every query.
- 4N+1 problem: use JOIN FETCH, @EntityGraph, or hibernate.default_batch_fetch_size to batch-load associations.
- 5orphanRemoval = true deletes child entities when removed from the parent collection.
- 6Never use CascadeType.REMOVE on @ManyToMany — deleting one side would cascade-delete shared entities.
Interview Questions
Sign in to ask AriaWhat is the N+1 problem and how do you fix it in Spring Data JPA?
What is the owning side of a JPA relationship and why does it matter?
What is the difference between LAZY and EAGER fetch types?
What causes LazyInitializationException and how do you fix it?
When would you use an explicit join entity instead of @ManyToMany?
Ask Aria about Entity Relationships
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.