āļ§āļīāļāļĩāđāļāđāļāļąāļāļŦāļē N+1 āđāļ Spring Data JPA āļāļĩ 2026: Fetch Join āđāļĨāļ° EntityGraph
āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļĢāļ§āļāļāļąāļāđāļĨāļ°āđāļāđāđāļāļāļąāļāļŦāļē N+1 āđāļ Spring Data JPA āļāļĢāļāļāļāļĨāļļāļĄ Fetch join, @EntityGraph, batch fetching āđāļĨāļ°āļāļĨāļĒāļļāļāļāđāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļēāļĢāļāļīāļ§āļĢāļĩ

āļāļąāļāļŦāļē N+1 āļāļ·āļāđāļāđāļāļŦāļāļķāđāļāđāļāļāļąāļāļāļąāļāļāđāļēāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļĩāđāļāļāļāđāļāļĒāļāļĩāđāļŠāļļāļāđāļ JPA āļāļīāļ§āļĢāļĩāļāļĢāļĢāļĄāļāļēāļŠāļģāļŦāļĢāļąāļāļāļķāļāļāļģāļŠāļąāđāļāļāļ·āđāļ 100 āļĢāļēāļĒāļāļēāļĢāļāļēāļāļāļĢāļ°āļāļļāđāļāđāļŦāđāđāļāļīāļāļāļīāļ§āļĢāļĩ SQL āļāļķāļ 101 āļāļĢāļąāđāļ: āļŦāļāļķāđāļāļāļĢāļąāđāļāļŠāļģāļŦāļĢāļąāļāļāļģāļŠāļąāđāļāļāļ·āđāļ āđāļĨāļ°āļāļĩāļāļŦāļāļķāđāļāļāļĢāļąāđāļāļŠāļģāļŦāļĢāļąāļāļĨāļđāļāļāđāļēāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļāđāļāđāļĨāļ°āļĢāļēāļĒ āļāļēāļĢāđāļāļīāđāļĄāļāļģāļāļ§āļāļāļīāļ§āļĢāļĩāļāļĒāđāļēāļāđāļāļĩāļĒāļāđ āļāļĩāđāļāļģāđāļŦāđāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļĨāļāļĨāļāđāļĨāļ°āļŠāļĢāđāļēāļāļ āļēāļĢāļ°āđāļŦāđāļāļēāļāļāđāļāļĄāļđāļĨ
endpoint āļāļĩāđāļāļ·āļāļāđāļēāļāļāļāļ§āļēāļĄ 50 āļĢāļēāļĒāļāļēāļĢāļāļĢāđāļāļĄāļāļđāđāđāļāļĩāļĒāļāļāļēāļāļāļĢāļ°āđāļāļāļāļēāļ 10ms āđāļāđāļ 500ms āđāļāļ·āđāļāļāļāļēāļ N+1 āļāļēāļĢāļāļĢāļ§āļāļāļąāļāđāļāđāđāļāļīāđāļāđ āļāđāļ§āļĒāļāđāļāļāļāļąāļāļāļąāļāļŦāļēāļ§āļīāļāļĪāļāđāļāļĢāļ°āļāļāđāļāļĢāļāļąāļāļāļąāļ
āļāļģāļāļ§āļēāļĄāđāļāđāļēāđāļāļāļąāļāļŦāļē N+1 āđāļ JPA
āļāļąāļāļŦāļē N+1 āđāļāļīāļāļāļķāđāļāđāļĄāļ·āđāļ JPA āđāļŦāļĨāļāļāļāļĨāđāļĨāļāļāļąāļāļāļāļāđāļāļāļāļīāļāļĩ āļāļēāļāļāļąāđāļāđāļĢāļĩāļĒāļāđāļāđāļāļīāļ§āļĢāļĩāđāļāļīāđāļĄāđāļāļīāļĄāļŠāļģāļŦāļĢāļąāļāđāļāđāļĨāļ°āđāļāļāļāļīāļāļĩāđāļāļ·āđāļāđāļŦāļĨāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāļāļāļāļĄāļąāļ āļāļĪāļāļīāļāļĢāļĢāļĄāļāļĩāđāļĄāļēāļāļēāļāļāļēāļĢ lazy loading āļāļĩāđāđāļāđāļāļāđāļēāđāļĢāļīāđāļĄāļāđāļāļāļāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđ @OneToMany āđāļĨāļ° @ManyToMany
āļĨāļāļāļāļīāļāļēāļĢāļāļēāđāļĄāđāļāļĨāļāļĨāļēāļŠāļŠāļīāļāļāļĩāđāļĄāļĩāļāļģāļŠāļąāđāļāļāļ·āđāļāđāļĨāļ°āļĨāļđāļāļāđāļē āđāļāđāļĨāļ°āļāļģāļŠāļąāđāļāļāļ·āđāļāđāļāđāļāļāļāļāļĨāļđāļāļāđāļēāļŦāļāļķāđāļāļĢāļēāļĒ āđāļĨāļ°āļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāļāļĩāđāđāļāđ lazy loading āđāļāđāļāļāđāļēāđāļĢāļīāđāļĄāļāđāļ
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private LocalDateTime createdAt;
// ManyToOne relationship is lazy by default since JPA 2.0
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// OneToMany relationship lazy by default
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
// Getters and setters omitted
}@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters omitted
}āđāļĄāļ·āđāļāļāļīāļ§āļĢāļĩāļāļķāļāļāļģāļŠāļąāđāļāļāļ·āđāļāđāļĨāđāļ§āđāļāđāļēāļāļķāļāļāļ·āđāļāļĨāļđāļāļāđāļē Hibernate āļāļ°āđāļĢāļĩāļĒāļāđāļāđāļāļīāļ§āļĢāļĩāđāļāļīāđāļĄāđāļāļīāļĄāļŠāļģāļŦāļĢāļąāļāđāļāđāļĨāļ°āļāļģāļŠāļąāđāļāļāļ·āđāļ
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public List<OrderDto> getAllOrders() {
// 1 query: SELECT * FROM orders
List<Order> orders = orderRepository.findAll();
// For each order, accessing customer triggers a query
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getOrderNumber(),
// N queries: SELECT * FROM customers WHERE id = ?
order.getCustomer().getName()
))
.toList();
}
}āļŠāļģāļŦāļĢāļąāļāļāļģāļŠāļąāđāļāļāļ·āđāļ 100 āļĢāļēāļĒāļāļēāļĢ āđāļāđāļāļāļĩāđāđāļĢāļĩāļĒāļāđāļāđāļāļīāļ§āļĢāļĩ SQL 101 āļāļĢāļąāđāļ āļāļąāļāļāļķāļāļāļāļ Hibernate āđāļāļĒāđāļŦāđāđāļŦāđāļāļĢāļđāļāđāļāļāļāļĩāđāļāļģāļĨāļēāļĒāļĨāđāļēāļāļāļĩāđ
-- Query 1: fetch orders
SELECT o.id, o.order_number, o.created_at, o.customer_id FROM orders o
-- Queries 2-101: fetch each customer
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 1
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 2
SELECT c.id, c.name, c.email FROM customers c WHERE c.id = 3
-- ... 97 more identical queriesāļāļēāļĢāļāļĢāļ§āļāļāļąāļāļāļąāļāļŦāļē N+1 āļāđāļ§āļĒāļāļąāļāļāļķāļāļāļāļ Hibernate
āļāļąāđāļāļāļāļāđāļĢāļāđāļāļĩāđāļĒāļ§āļāđāļāļāļāļąāļāļāļēāļĢāđāļāļīāļāđāļāđāļāļēāļāļāļąāļāļāļķāļ SQL āđāļāļ·āđāļāļĢāļ°āļāļļāļāļīāļ§āļĢāļĩāļāļĩāđāļĄāļĩāļāļąāļāļŦāļē āļāļēāļĢāļāļģāļŦāļāļāļāđāļēāļāđāļāđāļāļāļĩāđāđāļŠāļāļāļāļļāļāļāļīāļ§āļĢāļĩāļāļĩāđ Hibernate āđāļĢāļĩāļĒāļāđāļāđ
# application.yml
spring:
jpa:
show-sql: true
properties:
hibernate:
# Format SQL for better readability
format_sql: true
# Display session statistics (queries, time)
generate_statistics: true
logging:
level:
# Detailed SQL query logging
org.hibernate.SQL: DEBUG
# Display prepared statement parameters
org.hibernate.orm.jdbc.bind: TRACEāļŠāļāļīāļāļīāļāļāļ Hibernate āđāļŦāđāļŠāļĢāļļāļāļāļĩāđāļĄāļĩāļāđāļēāđāļĄāļ·āđāļāļŠāļīāđāļāļŠāļļāļāđāļāđāļĨāļ°āļāļļāļĢāļāļĢāļĢāļĄ
Session Metrics {
23421 nanoseconds spent acquiring 1 JDBC connection;
0 nanoseconds spent releasing 0 JDBC connections;
1254789 nanoseconds spent preparing 101 JDBC statements;
15478963 nanoseconds spent executing 101 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
}āļāļģāļāļ§āļ 101 āļāļģāļŠāļąāđāļ JDBC āļŠāļģāļŦāļĢāļąāļāļĢāļēāļĒāļāļēāļĢāļāļģāļŠāļąāđāļāļāļ·āđāļāļāļĢāļĢāļĄāļāļēāđāļāđāļāļŠāļąāļāļāļēāļāļāļĩāđāļāļąāļāđāļāļāļāļāļāļāļąāļāļŦāļē N+1
āļāļąāļāļāļķāļ SQL āđāļĨāļ°āļŠāļāļīāļāļīāļŠāđāļāļāļĨāļāļĢāļ°āļāļāļāđāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ āļāļąāļ§āđāļĨāļ·āļāļāđāļŦāļĨāđāļēāļāļĩāđāļāļ§āļĢāļāļīāļāđāļāđāļāļēāļāđāļāļĢāļ°āļāļāđāļāļĢāļāļąāļāļāļąāļāđāļĨāļ°āļŠāļāļ§āļāđāļ§āđāļŠāļģāļŦāļĢāļąāļāļŠāļ āļēāļāđāļ§āļāļĨāđāļāļĄāļāļēāļĢāļāļąāļāļāļēāđāļĨāļ°āļāļēāļĢāļāļāļŠāļāļ
āļ§āļīāļāļĩāđāļāđāļāļĩāđ 1: Fetch Join āļāđāļ§āļĒ JPQL
Fetch join āđāļŦāļĨāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāđāļāļāļīāļ§āļĢāļĩ SQL āđāļāļĩāļĒāļ§āļāđāļēāļ join āđāļāļ§āļāļēāļāļāļĩāđāļāļąāļāđāļāļāļāļĩāđāđāļāđāļāļąāļāļŦāļē N+1 āđāļāļĒāļāļķāļāļāđāļāļĄāļđāļĨāļāļĩāđāļāļģāđāļāđāļāļāļąāđāļāļŦāļĄāļāđāļāļāļĢāļąāđāļāđāļāļĩāļĒāļ§
public interface OrderRepository extends JpaRepository<Order, Long> {
// Explicit fetch join to load customers
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();
// Multiple fetch join for several associations
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.customer c " +
"JOIN FETCH o.items i")
List<Order> findAllWithCustomerAndItems();
// Fetch join with WHERE condition
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.customer c " +
"WHERE o.createdAt > :since")
List<Order> findRecentOrdersWithCustomer(
@Param("since") LocalDateTime since
);
}Fetch join āđāļāļĨāļĩāđāļĒāļāļāļīāļ§āļĢāļĩ N+1 āđāļŦāđāđāļāđāļāļāļīāļ§āļĢāļĩāļāļĩāđāļāļĢāļąāļāđāļŦāđāđāļŦāļĄāļēāļ°āļŠāļĄāđāļāļĩāļĒāļāļāļĢāļąāđāļāđāļāļĩāļĒāļ§
-- Single query with join
SELECT o.id, o.order_number, o.created_at, o.customer_id,
c.id, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.idāļāļāļāļāļĩāđāļāļĢāļīāļāļēāļĢāđāļāđāđāļĄāļāļāļāļāļĩāđāļāļĢāļąāļāđāļŦāđāđāļŦāļĄāļēāļ°āļŠāļĄāđāļāļĒāđāļĄāđāļāđāļāļāđāļāđāđāļāđāļāđāļāļāļēāļāļāļļāļĢāļāļīāļ
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public List<OrderDto> getAllOrders() {
// Single query with join
List<Order> orders = orderRepository.findAllWithCustomer();
// No additional queries
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getOrderNumber(),
order.getCustomer().getName() // Already loaded
))
.toList();
}
}āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ Spring Boot āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļ§āļīāļāļĩāđāļāđāļāļĩāđ 2: @EntityGraph āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļ§āļāļāļļāļĄāđāļāļāļāļĢāļ°āļāļēāļĻ
āļāļģāļāļāļīāļāļēāļĒāļāļĢāļ°āļāļāļ @EntityGraph āļāļģāđāļŠāļāļāļāļēāļāđāļĨāļ·āļāļāđāļāļāļāļĢāļ°āļāļēāļĻāđāļāļ fetch join āđāļāļĒāļāļģāļŦāļāļāļ§āđāļēāļāļ°āđāļŦāļĨāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāđāļāđāļāļĒāđāļĄāđāļāđāļāļāđāļāļĩāļĒāļ JPQL āđāļāļāļāļģāļŦāļāļāđāļāļ
public interface OrderRepository extends JpaRepository<Order, Long> {
// Inline EntityGraph with attributePaths
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();
// EntityGraph with multiple attributes
@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findByCreatedAtAfter(LocalDateTime since);
// Named EntityGraph referencing entity definition
@EntityGraph(value = "Order.withCustomerAndItems")
List<Order> findByCustomerId(Long customerId);
// Combination with custom query
@EntityGraph(attributePaths = {"customer"})
@Query("SELECT o FROM Order o WHERE o.orderNumber LIKE :prefix%")
List<Order> findByOrderNumberPrefix(@Param("prefix") String prefix);
}EntityGraph āļāļĩāđāļĄāļĩāļāļ·āđāļāļāļđāļāļāļģāļŦāļāļāđāļāļĒāļāļĢāļāļāļāđāļāļāļāļīāļāļĩāđāļāļ·āđāļāļāļģāļāļĨāļąāļāļĄāļēāđāļāđāđāļāļŦāļĨāļēāļĒ repository
@Entity
@Table(name = "orders")
@NamedEntityGraph(
name = "Order.withCustomer",
attributeNodes = @NamedAttributeNode("customer")
)
@NamedEntityGraph(
name = "Order.withCustomerAndItems",
attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode("items")
}
)
@NamedEntityGraph(
name = "Order.full",
attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode(value = "items", subgraph = "items-product")
},
subgraphs = @NamedSubgraph(
name = "items-product",
attributeNodes = @NamedAttributeNode("product")
)
)
public class Order {
// Fields unchanged
}Subgraph āļāļāļļāļāļēāļāđāļŦāđāđāļŦāļĨāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāļāļĩāđāļāđāļāļāļāļąāļāđāļāđ āļāļąāļ§āļāļĒāđāļēāļāļāđāļēāļāļāđāļāđāļŦāļĨāļāļāļģāļŠāļąāđāļāļāļ·āđāļ āļĢāļēāļĒāļāļēāļĢāļāļāļāļāļ§āļāđāļāļē āđāļĨāļ°āļāļĨāļīāļāļ āļąāļāļāđāļāļāļāđāļāđāļĨāļ°āļĢāļēāļĒāļāļēāļĢāđāļāļāļīāļ§āļĢāļĩāđāļāļĩāļĒāļ§
āļ§āļīāļāļĩāđāļāđāļāļĩāđ 3: Batch Fetching āļŠāļģāļŦāļĢāļąāļāļāļāļĨāđāļĨāļāļāļąāļ
Batch fetching āļāļģāđāļŠāļāļāļāļēāļāđāļĨāļ·āļāļāđāļāļ fetch join āļŠāļģāļŦāļĢāļąāļāļāļāļĨāđāļĨāļāļāļąāļ @OneToMany āđāļāļāļāļĩāđāļāļ°āđāļŦāļĨāļāđāļāđāļĨāļ°āļāļāļĨāđāļĨāļāļāļąāļāļāļĩāļĨāļ°āļĢāļēāļĒāļāļēāļĢ Hibernate āļāļ°āļāļąāļāļāļĨāļļāđāļĄāļāļīāļ§āļĢāļĩāđāļāđāļāļāļļāļ
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// Batch fetching on the collection
@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items = new ArrayList<>();
}āļāđāļ§āļĒ @BatchSize(size = 25) Hibernate āļāļ°āđāļŦāļĨāļāļĢāļēāļĒāļāļēāļĢāļŠāļģāļŦāļĢāļąāļāļāļģāļŠāļąāđāļāļāļ·āđāļ 25 āļĢāļēāļĒāļāļēāļĢāđāļāļāļĢāļēāļ§āđāļāļĩāļĒāļ§āđāļāļāļāļĩāđāļāļ°āđāļāđāļ 1 āļĢāļēāļĒāļāļēāļĢ āļŠāļģāļŦāļĢāļąāļ 100 āļāļģāļŠāļąāđāļāļāļ·āđāļ āļāļģāļāļ§āļāļāļīāļ§āļĢāļĩāļĨāļāļĨāļāļāļēāļ 101 āđāļāđāļ 5
-- Without batch fetching: 100 queries
SELECT * FROM order_items WHERE order_id = 1
SELECT * FROM order_items WHERE order_id = 2
-- ... 98 more queries
-- With @BatchSize(size = 25): 4 queries
SELECT * FROM order_items WHERE order_id IN (1, 2, 3, ..., 25)
SELECT * FROM order_items WHERE order_id IN (26, 27, 28, ..., 50)
SELECT * FROM order_items WHERE order_id IN (51, 52, 53, ..., 75)
SELECT * FROM order_items WHERE order_id IN (76, 77, 78, ..., 100)āļāļēāļĢāļāļģāļŦāļāļāļāđāļēāļāļāļēāļāđāļāļāļāđāļāļąāđāļ§āđāļĨāļāđāļāđāļāļąāļāļāļāļĨāđāļĨāļāļāļąāļāļāļąāđāļāļŦāļĄāļāđāļāđāļāļāļāļĨāļīāđāļāļāļąāļ
# application.yml
spring:
jpa:
properties:
hibernate:
# Global default batch size
default_batch_fetch_size: 25Batch fetching āđāļŦāļĄāļēāļ°āļŠāļģāļŦāļĢāļąāļāļāļĢāļāļĩāļāļĩāđ fetch join āļŠāļĢāđāļēāļāļāļĨāļāļđāļāļāļēāļĢāđāļāļĩāđāļāļĩāļĒāļāļāļĩāđāđāļŦāļāđāđāļāļīāļāđāļ āļŠāļģāļŦāļĢāļąāļāļāļģāļŠāļąāđāļāļāļ·āđāļāļāļĩāđāļĄāļĩ 10 āļĢāļēāļĒāļāļēāļĢāđāļĨāļ° 5 āļāļēāļĢāļāļģāļĢāļ°āđāļāļīāļ fetch join āļāļ·āļāļāđāļē 50 āđāļāļ§ Batch fetching āđāļĢāļĩāļĒāļāđāļāđāļāļīāļ§āļĢāļĩāđāļĒāļ 2 āļĢāļēāļĒāļāļēāļĢāļāļĩāđāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļĄāļēāļāļāļ§āđāļē
āđāļāļĢāļĩāļĒāļāđāļāļĩāļĒāļāļāļĨāļĒāļļāļāļāđāļāļēāļĢāđāļŦāļĨāļ
āđāļāđāļĨāļ°āļāļĨāļĒāļļāļāļāđāļĄāļĩāļāđāļāđāļāđāđāļāļĢāļĩāļĒāļāļāļķāđāļāļāļĒāļđāđāļāļąāļāļāļĢāļīāļāļāļāļēāļĢāđāļāđāļāļēāļ āļāļēāļĢāļēāļāļāđāļāđāļāļāļĩāđāļŠāļĢāļļāļāļāļĢāļāļĩāļāļēāļĢāđāļāđāļāļēāļāļāļĩāđāđāļŦāļĄāļēāļ°āļŠāļĄāļāļĩāđāļŠāļļāļ
| āļāļĨāļĒāļļāļāļāđ | āļāļĢāļāļĩāļāļēāļĢāđāļāđāļāļēāļ | āļāđāļāļāļĩ | āļāđāļāđāļŠāļĩāļĒ |
|----------|----------|------------|---------------|
| Fetch Join | āļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđ @ManyToOne | āļāļīāļ§āļĢāļĩ SQL āđāļāļĩāļĒāļ§ | āļāļĨāļāļđāļāļāļēāļĢāđāļāļĩāđāļāļĩāļĒāļāļāļąāļāļāļāļĨāđāļĨāļāļāļąāļ |
| @EntityGraph | āļāļēāļĢāđāļŦāļĨāļāđāļāļāļāļĢāļ°āļāļēāļĻ | āđāļāđāļāđāļģāđāļāđ āļāđāļēāļāļāđāļēāļĒ | āļĒāļ·āļāļŦāļĒāļļāđāļāļāđāļāļĒāļāļ§āđāļē JPQL |
| Batch Fetching | āļāļāļĨāđāļĨāļāļāļąāļ @OneToMany | āļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāļĨāļāļđāļāļāļēāļĢāđāļāļĩāđāļāļĩāļĒāļ | āļāļīāļ§āļĢāļĩāļŦāļĨāļēāļĒāļĢāļēāļĒāļāļēāļĢ |
| Subselect | āļāļāļĨāđāļĨāļāļāļąāļāļāļĩāđāđāļāđāļēāļāļķāļāđāļĄāđāļāđāļāļĒ | āđāļŦāļĨāļāđāļāļāļēāļ°āđāļĄāļ·āđāļāļāļģāđāļāđāļ | āļāļīāļ§āļĢāļĩāļĒāđāļāļĒāļāļĩāđāļŠāļąāļĄāļāļąāļāļāđāļāļąāļ |
āļāļĨāļĒāļļāļāļāđ subselect āđāļŦāļĨāļāļāļąāđāļāļāļāļĨāđāļĨāļāļāļąāļāđāļĄāļ·āđāļāđāļāđāļēāļāļķāļāļāļāļāđāļāļĢāļ°āļāļāļāđāļāđ āđāļāđāļāļāļĢāļąāđāļāđāļĢāļ
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items = new ArrayList<>();-- Generated subselect query
SELECT * FROM order_items
WHERE order_id IN (SELECT id FROM orders WHERE created_at > ?)āļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļ N+1 āļāđāļ§āļĒ DTO Projections
DTO projection āļāļģāđāļŠāļāļāđāļāļ§āļāļēāļāļāļĩāđāļĢāļļāļāđāļĢāļāđāļāđāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ āđāļāļĒāļāļēāļĢāđāļĨāļ·āļāļāđāļāļāļēāļ°āļāļāļĨāļąāļĄāļāđāļāļĩāđāļāļģāđāļāđāļ projection āļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāļēāļĢāđāļŦāļĨāļāđāļāļāļāļīāļāļĩāđāļĨāļ°āļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāļāļāļāļāļ§āļāđāļāļēāđāļāļĒāļŠāļīāđāļāđāļāļīāļ
public record OrderSummaryDto(
Long orderId,
String orderNumber,
String customerName,
String customerEmail
) {}public interface OrderRepository extends JpaRepository<Order, Long> {
// DTO projection with constructor
@Query("SELECT new com.example.dto.OrderSummaryDto(" +
"o.id, o.orderNumber, c.name, c.email) " +
"FROM Order o JOIN o.customer c")
List<OrderSummaryDto> findAllOrderSummaries();
// DTO projection with condition
@Query("SELECT new com.example.dto.OrderSummaryDto(" +
"o.id, o.orderNumber, c.name, c.email) " +
"FROM Order o JOIN o.customer c " +
"WHERE o.createdAt > :since")
List<OrderSummaryDto> findRecentOrderSummaries(
@Param("since") LocalDateTime since
);
}āđāļāļ§āļāļēāļāļāļĩāđāļŠāļĢāđāļēāļāļāļīāļ§āļĢāļĩ SQL āļāļĩāđāđāļŦāļĄāļēāļ°āļŠāļĄāļāļĩāđāļŠāļļāļāđāļāļĒāđāļĄāđāļĄāļĩāļāđāļēāđāļāđāļāđāļēāļĒāļāļāļāļāļēāļĢāđāļĄāļāđāļāļāļāļīāļāļĩ
SELECT o.id, o.order_number, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > ?āļāļēāļĢāļāļģāļŦāļāļāļāđāļēāļāļąāđāļāļŠāļđāļāļāđāļ§āļĒ Spring Data JPA
Spring Data JPA 3.x āđāļāļ°āļāļģāļāļēāļĢāļāļĢāļąāļāļāļĢāļļāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļāļēāļĢ EntityGraph āđāļĨāļ°āļāļīāļ§āļĢāļĩāļāļĩāđāļāļĢāļąāļāđāļŦāđāđāļŦāļĄāļēāļ°āļŠāļĄ
public interface OrderRepository extends JpaRepository<Order, Long> {
// Dynamic EntityGraph with Specification
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll(Specification<Order> spec);
// Pagination with EntityGraph
@EntityGraph(attributePaths = {"customer"})
Page<Order> findByCustomerNameContaining(
String name,
Pageable pageable
);
// Slice for efficient pagination
@EntityGraph(attributePaths = {"customer"})
Slice<Order> findByCreatedAtBefore(
LocalDateTime date,
Pageable pageable
);
}āļāļēāļĢāđāļŦāļĨāļāđāļāļāļĄāļĩāđāļāļ·āđāļāļāđāļāļāļāļļāļāļēāļāđāļŦāđāđāļāđāļāļĨāļĒāļļāļāļāđāļāļĩāđāđāļāļāļāđāļēāļāļāļąāļāļāļēāļĄāļāļĢāļīāļāļ
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final EntityManager entityManager;
public List<Order> getOrdersWithGraph(String graphName) {
// Dynamic EntityGraph retrieval
EntityGraph<?> graph = entityManager
.getEntityGraph(graphName);
return entityManager
.createQuery("SELECT o FROM Order o", Order.class)
.setHint("jakarta.persistence.loadgraph", graph)
.getResultList();
}
public List<Order> getOrdersForListing() {
// Minimal graph for listing
return getOrdersWithGraph("Order.withCustomer");
}
public List<Order> getOrdersForDetail() {
// Full graph for detail view
return getOrdersWithGraph("Order.full");
}
}āđāļĢāļīāđāļĄāļāļķāļāļāđāļāļĄāđāļĨāļĒ!
āļāļāļŠāļāļāļāļ§āļēāļĄāļĢāļđāđāļāļāļāļāļļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāļŠāļąāļĄāļ āļēāļĐāļāđāđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļēāļĢāļāļāļŠāļāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļāļ·āđāļāļāļĢāļ§āļāļāļąāļ N+1
āļāļēāļĢāļāļāļŠāļāļāļāļąāļāđāļāļĄāļąāļāļīāļĒāļ·āļāļĒāļąāļāļ§āđāļēāđāļĄāđāļĄāļĩāļāļąāļāļŦāļē N+1 āđāļāļĒāļāļēāļĢāļāļąāļāļāļīāļ§āļĢāļĩ SQL āļāļĩāđāđāļĢāļĩāļĒāļāđāļāđ
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class OrderRepositoryPerformanceTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private EntityManager entityManager;
@PersistenceContext
private EntityManager em;
private Statistics statistics;
@BeforeEach
void setUp() {
// Enable Hibernate statistics
Session session = entityManager.unwrap(Session.class);
SessionFactory factory = session.getSessionFactory();
statistics = factory.getStatistics();
statistics.setStatisticsEnabled(true);
statistics.clear();
}
@Test
void findAllWithCustomer_shouldExecuteSingleQuery() {
// Given: 50 orders in database
createTestOrders(50);
entityManager.clear();
statistics.clear();
// When: fetch with fetch join
List<Order> orders = orderRepository.findAllWithCustomer();
// Then: single query executed
assertThat(orders).hasSize(50);
assertThat(statistics.getQueryExecutionCount())
.as("Should execute only 1 query with fetch join")
.isEqualTo(1);
}
@Test
void findAll_withoutOptimization_triggersNPlus1() {
// Given: 50 orders in database
createTestOrders(50);
entityManager.clear();
statistics.clear();
// When: fetch without optimization
List<Order> orders = orderRepository.findAll();
// Access customers to trigger lazy loading
orders.forEach(o -> o.getCustomer().getName());
// Then: N+1 queries (51 instead of 1)
assertThat(statistics.getQueryExecutionCount())
.as("Should detect N+1 problem")
.isGreaterThan(1);
}
private void createTestOrders(int count) {
for (int i = 0; i < count; i++) {
Customer customer = new Customer();
customer.setName("Customer " + i);
customer.setEmail("customer" + i + "@test.com");
entityManager.persist(customer);
Order order = new Order();
order.setOrderNumber("ORD-" + i);
order.setCustomer(customer);
order.setCreatedAt(LocalDateTime.now());
entityManager.persist(order);
}
entityManager.flush();
}
}āļāļēāļĢāļĒāļ·āļāļĒāļąāļāļāļ getQueryExecutionCount() āļĢāļąāļāļāļĢāļ°āļāļąāļāļ§āđāļēāļāļēāļĢāđāļāļīāđāļĄāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļĒāļąāļāļāļāļāļĒāļđāđāļĢāļ°āļŦāļ§āđāļēāļāļ§āļīāļ§āļąāļāļāļēāļāļēāļĢāļāļāļāđāļāđāļ
āļāļāļŠāļĢāļļāļ
āļāļąāļāļŦāļē N+1 āđāļāđāļāļāļ§āļēāļĄāļāđāļēāļāļēāļĒāļāđāļēāļāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļĩāđāļŠāļģāļāļąāļāđāļ Spring Data JPA āđāļāđāļĄāļĩāļ§āļīāļāļĩāđāļāđāļāļĩāđāļĄāļĩāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļŦāļĨāļēāļĒāļ§āļīāļāļĩ Fetch join āđāļĨāļ° @EntityGraph āđāļāđāļāļąāļāļŦāļēāļŠāđāļ§āļāđāļŦāļāđāđāļāļĒāļāļēāļĢāđāļŦāļĨāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđāđāļāļāļīāļ§āļĢāļĩāđāļāļĩāļĒāļ§ Batch fetching āđāļāđāļāļāļēāļāđāļĨāļ·āļāļāļŠāļģāļŦāļĢāļąāļāļāļāļĨāđāļĨāļāļāļąāļāļāļāļēāļāđāļŦāļāđāļāļĩāđ fetch join āļŠāļĢāđāļēāļāļāļĨāļāļđāļāļāļēāļĢāđāļāļĩāđāļāļĩāļĒāļ
āļĢāļēāļĒāļāļēāļĢāļāļĢāļ§āļāļŠāļāļāļāļēāļĢāļāđāļāļāļāļąāļ N+1:
- â āđāļāļīāļāđāļāđāļāļēāļāļāļąāļāļāļķāļ SQL āđāļāļāļēāļĢāļāļąāļāļāļēāđāļāļ·āđāļāļāļĢāļ§āļāļāļąāļāļāļīāļ§āļĢāļĩāļŦāļĨāļēāļĒāļĢāļēāļĒāļāļēāļĢ
- â
āđāļāđ
JOIN FETCHāļŠāļģāļŦāļĢāļąāļāļāļ§āļēāļĄāļŠāļąāļĄāļāļąāļāļāđ@ManyToOneāļāļĩāđāđāļāđāļēāļāļķāļāļāđāļāļĒ - â
āļāļĢāļ°āļĒāļļāļāļāđāđāļāđ
@EntityGraphāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāđāļŦāļĨāļāđāļāļāļāļĢāļ°āļāļēāļĻāļāļĩāđāđāļāđāļāđāļģāđāļāđ - â
āļāļģāļŦāļāļāļāđāļē
@BatchSizeāļŠāļģāļŦāļĢāļąāļāļāļāļĨāđāļĨāļāļāļąāļ@OneToManyāļāļāļēāļāđāļŦāļāđ - â āđāļāđ DTO projection āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļģāđāļāļīāļāļāļēāļĢāđāļāļāļāđāļēāļāļāļĒāđāļēāļāđāļāļĩāļĒāļ§
- â āđāļāļĩāļĒāļāļāļēāļĢāļāļāļŠāļāļāđāļāļ·āđāļāļāļĢāļ§āļāļŠāļāļāļāļģāļāļ§āļāļāļīāļ§āļĢāļĩāļāļĩāđāđāļĢāļĩāļĒāļāđāļāđ
- â āļāļīāļāđāļāđāļāļēāļāļāļąāļāļāļķāļ SQL āđāļĨāļ°āļŠāļāļīāļāļīāđāļāļĢāļ°āļāļāđāļāļĢāļāļąāļāļāļąāļ
- â āļāļģāđāļāļĢāđāļāļĨāđ endpoint āļŠāļģāļāļąāļāđāļāđāļāļāļĢāļ°āļāļģāļāđāļ§āļĒāđāļĄāļāļĢāļīāļāļāļāļ Hibernate
āđāļāđāļ
āđāļāļĢāđ
āļāļāļāļ§āļēāļĄāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļ

āļŠāļąāļĄāļ āļēāļĐāļāđ Spring GraphQL: Resolver, DataLoader āđāļĨāļ°āļ§āļīāļāļĩāđāļāđāļāļąāļāļŦāļē N+1
āđāļāļĢāļĩāļĒāļĄāļāļąāļ§āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ Spring GraphQL āļāđāļ§āļĒāļāļđāđāļĄāļ·āļāļāļĩāđāļāļĢāļāļāđāļ§āļāļāļĩāđ Resolver, DataLoader, āļāļēāļĢāļāļąāļāļāļēāļĢāļāļąāļāļŦāļē N+1, mutation āđāļĨāļ°āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļāļŠāļģāļŦāļĢāļąāļāļāļģāļāļēāļĄāļāļēāļāđāļāļāļāļīāļ

Spring Boot 3.4 Virtual Threads: āļāļģāļāļēāļĄāļŠāļąāļĄāļ āļēāļĐāļāđāđāļĨāļ° Benchmark āļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ
āđāļāļĩāđāļĒāļ§āļāļēāļ Java 21 Virtual Threads āļāđāļ§āļĒ Spring Boot 3.4: 15 āļāļģāļāļēāļĄāļŠāļąāļĄāļ āļēāļĐāļāđ Benchmark āļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ āđāļĨāļ°āļĢāļđāļāđāļāļāļāļēāļĢāļĒāđāļēāļĒāļĢāļ°āļāļāļŠāļģāļŦāļĢāļąāļāļŠāļāļāļŠāļąāļĄāļ āļēāļĐāļāđāđāļāļāļāļīāļ

Spring Boot Logging āđāļāļāļĩ 2026: āļĨāđāļāļāđāļāļāļĄāļĩāđāļāļĢāļāļŠāļĢāđāļēāļāļŠāļģāļŦāļĢāļąāļāđāļāļĢāļāļąāļāļāļąāļāļāđāļ§āļĒ Logback āđāļĨāļ° JSON
āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļŠāļģāļŦāļĢāļąāļ structured logging āđāļ Spring Boot āļāļēāļĢāļāļąāđāļāļāđāļē Logback JSON, MDC āļŠāļģāļŦāļĢāļąāļ tracing āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļāđāļāđāļāļĢāļāļąāļāļāļąāļ āđāļĨāļ°āļāļēāļĢāļĢāļ§āļĄāļāļąāļ ELK Stack