| name | jpa-patterns |
| description | JPA/Hibernate patterns and common pitfalls (N+1, lazy loading, transactions, queries). Use when user has JPA performance issues, LazyInitializationException, or asks about entity relationships and fetching strategies. |
JPA Patterns Skill
Best practices and common pitfalls for JPA/Hibernate in Spring applications.
When to Use
- User mentions "N+1 problem" / "too many queries"
- LazyInitializationException errors
- Questions about fetch strategies (EAGER vs LAZY)
- Transaction management issues
- Entity relationship design
- Query optimization
Quick Reference: Common Problems
| Problem | Symptom | Solution |
|---|
| N+1 queries | Many SELECT statements | JOIN FETCH, @EntityGraph |
| LazyInitializationException | Error outside transaction | Open Session in View, DTO projection, JOIN FETCH |
| Slow queries | Performance issues | Pagination, projections, indexes |
| Dirty checking overhead | Slow updates | Read-only transactions, DTOs |
| Lost updates | Concurrent modifications | Optimistic locking (@Version) |
N+1 Problem
The #1 JPA performance killer
The Problem
@Entity
public class Author {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
System.out.println(author.getBooks().size());
}
Solution 1: JOIN FETCH (JPQL)
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
}
List<Author> authors = authorRepository.findAllWithBooks();
Solution 2: @EntityGraph
public interface AuthorRepository extends JpaRepository<Author, Long> {
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
@EntityGraph(value = "Author.withBooks")
List<Author> findAllWithBooks();
}
@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
}
Solution 3: Batch Fetching
@Entity
public class Author {
@OneToMany(mappedBy = "author")
@BatchSize(size = 25)
private List<Book> books;
}
spring.jpa.properties.hibernate.default_batch_fetch_size=25
Detecting N+1
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
Lazy Loading
FetchType Basics
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
}
Best Practice: Default to LAZY
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
LazyInitializationException
@Service
public class OrderService {
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
}
Order order = orderService.getOrder(1L);
order.getItems().size();
Solutions for LazyInitializationException
Solution 1: JOIN FETCH in query
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
Solution 2: @Transactional on service method
@Service
public class OrderService {
@Transactional(readOnly = true)
public OrderDTO getOrderWithItems(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
int itemCount = order.getItems().size();
return new OrderDTO(order, itemCount);
}
}
Solution 3: DTO Projection (recommended)
public interface OrderSummary {
Long getId();
String getStatus();
int getItemCount();
}
@Query("SELECT o.id as id, o.status as status, SIZE(o.items) as itemCount " +
"FROM Order o WHERE o.id = :id")
Optional<OrderSummary> findOrderSummary(@Param("id") Long id);
Solution 4: Open Session in View (not recommended)
spring:
jpa:
open-in-view: true
Transactions
Basic Transaction Management
@Service
public class OrderService {
@Transactional(readOnly = true)
public Order findById(Long id) {
return orderRepository.findById(id).orElseThrow();
}
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
return orderRepository.save(order);
}
@Transactional(rollbackFor = Exception.class)
public void processPayment(Long orderId) throws PaymentException {
}
}
Transaction Propagation
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
paymentService.processPayment(order);
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Order order) {
}
@Transactional(propagation = Propagation.MANDATORY)
public void updatePaymentStatus(Order order) {
}
}
Common Transaction Mistakes
@Service
public class OrderService {
public void processOrder(Long id) {
updateOrder(id);
}
@Transactional
public void updateOrder(Long id) {
}
}
@Service
public class OrderService {
@Autowired
private OrderService self;
public void processOrder(Long id) {
self.updateOrder(id);
}
@Transactional
public void updateOrder(Long id) {
}
}
Entity Relationships
OneToMany / ManyToOne
@Entity
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
public void removeBook(Book book) {
books.remove(book);
book.setAuthor(null);
}
}
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
}
ManyToMany
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public void addCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
equals() and hashCode() for Entities
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId
@Column(unique = true, nullable = false)
private String isbn;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book book)) return false;
return isbn != null && isbn.equals(book.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn);
}
}
Query Optimization
Pagination
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
@Query("SELECT o FROM Order o WHERE o.status = :status")
Page<Order> findByStatusSorted(
@Param("status") OrderStatus status,
Pageable pageable
);
}
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Order> orders = orderRepository.findByStatus(OrderStatus.PENDING, pageable);
DTO Projections
public interface OrderSummary {
Long getId();
String getCustomerName();
BigDecimal getTotal();
}
@Query("SELECT o.id as id, o.customer.name as customerName, o.total as total " +
"FROM Order o WHERE o.status = :status")
List<OrderSummary> findOrderSummaries(@Param("status") OrderStatus status);
public record OrderDTO(Long id, String customerName, BigDecimal total) {}
@Query("SELECT new com.example.dto.OrderDTO(o.id, o.customer.name, o.total) " +
"FROM Order o WHERE o.status = :status")
List<OrderDTO> findOrderDTOs(@Param("status") OrderStatus status);
Bulk Operations
public interface OrderRepository extends JpaRepository<Order, Long> {
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.createdAt < :date")
int updateOldOrdersStatus(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
@Modifying
@Query("DELETE FROM Order o WHERE o.status = :status AND o.createdAt < :date")
int deleteOldOrders(
@Param("status") OrderStatus status,
@Param("date") LocalDateTime date
);
}
@Transactional
public void archiveOldOrders() {
LocalDateTime threshold = LocalDateTime.now().minusYears(1);
int updated = orderRepository.updateOldOrdersStatus(
OrderStatus.ARCHIVED,
threshold
);
log.info("Archived {} orders", updated);
}
Optimistic Locking
Prevent Lost Updates
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
private OrderStatus status;
private BigDecimal total;
}
Handling OptimisticLockException
@Service
public class OrderService {
@Transactional
public Order updateOrder(Long id, UpdateOrderRequest request) {
try {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
} catch (OptimisticLockException e) {
throw new ConcurrentModificationException(
"Order was modified by another user. Please refresh and try again."
);
}
}
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrderWithRetry(Long id, UpdateOrderRequest request) {
Order order = orderRepository.findById(id).orElseThrow();
order.setStatus(request.getStatus());
return orderRepository.save(order);
}
}
Common Mistakes
1. Cascade Misuse
@Entity
public class Book {
@ManyToOne(cascade = CascadeType.ALL)
private Author author;
}
@Entity
public class Author {
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books;
}
2. Missing Index
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findByCustomerEmail(@Param("email") String email);
@Entity
@Table(indexes = @Index(name = "idx_order_customer_email", columnList = "customerEmail"))
public class Order {
private String customerEmail;
}
3. toString() with Lazy Fields
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
@Override
public String toString() {
return "Author{id=" + id + ", books=" + books + "}";
}
}
@Override
public String toString() {
return "Author{id=" + id + ", name='" + name + "'}";
}
Performance Checklist
When reviewing JPA code, check:
Related Skills
spring-boot-patterns - Spring Boot controller/service patterns
java-code-review - General code review checklist
clean-code - Code quality principles