วิธีแก้ปัญหา N+1 ใน Spring Data JPA: Fetch Join, EntityGraph และ Hibernate 7.4

คู่มือฉบับสมบูรณ์สำหรับการตรวจจับและแก้ไขปัญหา N+1 ใน Spring Data JPA ครอบคลุม Fetch join, @EntityGraph, batch fetching และกลยุทธ์ประสิทธิภาพการคิวรี

การแก้ปัญหา N+1 ด้วย Spring Data JPA โดยใช้ fetch join และ EntityGraph

ปัญหา N+1 ถือเป็นหนึ่งในกับดักด้านประสิทธิภาพที่พบบ่อยที่สุดใน JPA คิวรีธรรมดาสำหรับดึงคำสั่งซื้อ 100 รายการอาจกระตุ้นให้เกิดคิวรี SQL ถึง 101 ครั้ง: หนึ่งครั้งสำหรับคำสั่งซื้อ และอีกหนึ่งครั้งสำหรับลูกค้าที่เกี่ยวข้องแต่ละราย การเพิ่มจำนวนคิวรีอย่างเงียบๆ นี้ทำให้ประสิทธิภาพลดลงและสร้างภาระให้ฐานข้อมูล

ผลกระทบจริงของ N+1

endpoint ที่คืนค่าบทความ 50 รายการพร้อมผู้เขียนอาจกระโดดจาก 10ms เป็น 500ms เนื่องจาก N+1 การตรวจจับแต่เนิ่นๆ ช่วยป้องกันปัญหาวิกฤตในระบบโปรดักชัน

ทำความเข้าใจปัญหา N+1 ใน JPA

ปัญหา N+1 เกิดขึ้นเมื่อ JPA โหลดคอลเลกชันของเอนทิตี จากนั้นเรียกใช้คิวรีเพิ่มเติมสำหรับแต่ละเอนทิตีเพื่อโหลดความสัมพันธ์ของมัน พฤติกรรมนี้มาจากการ lazy loading ที่เป็นค่าเริ่มต้นของความสัมพันธ์ @OneToMany และ @ManyToMany

ลองพิจารณาโมเดลคลาสสิกที่มีคำสั่งซื้อและลูกค้า แต่ละคำสั่งซื้อเป็นของลูกค้าหนึ่งราย และความสัมพันธ์นี้ใช้ lazy loading เป็นค่าเริ่มต้น

Order.javajava
@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
}
Customer.javajava
@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 จะเรียกใช้คิวรีเพิ่มเติมสำหรับแต่ละคำสั่งซื้อ

OrderService.java - Problematic codejava
@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 เผยให้เห็นรูปแบบที่ทำลายล้างนี้

sql
-- 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 เรียกใช้

yaml
# 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 ให้สรุปที่มีค่าเมื่อสิ้นสุดแต่ละธุรกรรม

text
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 โดยดึงข้อมูลที่จำเป็นทั้งหมดในครั้งเดียว

OrderRepository.javajava
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 ให้เป็นคิวรีที่ปรับให้เหมาะสมเพียงครั้งเดียว

sql
-- 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

ตอนนี้บริการใช้เมธอดที่ปรับให้เหมาะสมโดยไม่ต้องแก้ไขโค้ดทางธุรกิจ

OrderService.java - Optimized codejava
@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 แบบกำหนดเอง สำหรับภาพรวมที่กว้างขึ้นเกี่ยวกับแนวปฏิบัติที่ดีที่สุดของ JPA โปรดดู โมดูลพื้นฐาน Spring Data JPA

OrderRepository.javajava
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

Order.javajava
@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 อนุญาตให้โหลดความสัมพันธ์ที่ซ้อนกันได้ ตัวอย่างข้างต้นโหลดคำสั่งซื้อ รายการของพวกเขา และผลิตภัณฑ์ของแต่ละรายการในคิวรีเดียว

Hibernate 7 แนะนำ ไวยากรณ์ @NamedEntityGraph แบบข้อความที่เรียบง่ายขึ้น ซึ่งลดความยาวของ annotation:

Order.java - Hibernate 7 text-based syntaxjava
@Entity
@Table(name = "orders")
@org.hibernate.annotations.NamedEntityGraph(
    name = "Order.full",
    graph = "customer, items(product)"
)
public class Order {
    // Fields unchanged
}

การกำหนดบรรทัดเดียวนี้แทนที่ annotation @NamedAttributeNode และ @NamedSubgraph ที่ยาว

วิธีแก้ที่ 3: Batch Fetching สำหรับคอลเลกชัน

Batch fetching นำเสนอทางเลือกแทน fetch join สำหรับคอลเลกชัน @OneToMany แทนที่จะโหลดแต่ละคอลเลกชันทีละรายการ Hibernate จะจัดกลุ่มคิวรีเป็นชุด

Order.javajava
@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

sql
-- 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)

การกำหนดค่าขนาดแบทช์ทั่วโลกใช้กับคอลเลกชันทั้งหมดในแอปพลิเคชัน

yaml
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        # Global default batch size
        default_batch_fetch_size: 25
Batch กับ Fetch Join

Batch fetching เหมาะสำหรับกรณีที่ fetch join สร้างผลคูณคาร์ทีเซียนที่ใหญ่เกินไป สำหรับคำสั่งซื้อที่มี 10 รายการและ 5 การชำระเงิน fetch join คืนค่า 50 แถว Batch fetching เรียกใช้คิวรีแยก 2 รายการที่มีประสิทธิภาพมากกว่า

การแบ่งหน้าด้วย Fetch Join ใน Hibernate 7.4

ข้อจำกัดที่มีมานานของ fetch join คือความไม่เข้ากันกับการแบ่งหน้า การรวม setMaxResults() กับ collection fetch join บังคับให้ Hibernate โหลดแถวที่ตรงกันทั้งหมด จากนั้นใช้ limit ในหน่วยความจำ คำเตือน firstResult/maxResults specified with collection fetch; applying in memory บ่งชี้ปัญหาประสิทธิภาพนี้

Hibernate 7.4 แก้ไขปัญหานี้ การแบ่งหน้าด้วย collection fetch join ตอนนี้ทำงานที่ระดับฐานข้อมูลโดยใช้ nested query Hibernate จะกำหนดชุดจำกัดของ identifier ของเอนทิตีหลักก่อน จากนั้นดึงคอลเลกชันที่เกี่ยวข้องเฉพาะสำหรับแถวหลักเหล่านั้น

OrderRepository.java - Safe pagination with Hibernate 7.4java
public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o FROM Order o JOIN FETCH o.items")
    Page<Order> findAllWithItems(Pageable pageable);

    @Query("SELECT o FROM Order o JOIN FETCH o.customer JOIN FETCH o.items")
    Slice<Order> findAllWithCustomerAndItems(Pageable pageable);
}

SQL ที่สร้างขึ้นตอนนี้ใช้ subquery สำหรับการแบ่งหน้าที่ระดับฐานข้อมูล:

sql
-- Hibernate 7.4: database-level pagination with fetch join
SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.id IN (
    SELECT id FROM orders
    ORDER BY created_at DESC
    LIMIT 10 OFFSET 0
)

การปรับปรุงนี้ลดการใช้หน่วยความจำอย่างมากสำหรับ endpoint ที่มีการแบ่งหน้าที่แสดงความสัมพันธ์แม่-ลูก Spring Boot 4.1 รวม Hibernate 7.4.5 ทำให้การเพิ่มประสิทธิภาพนี้พร้อมใช้งานโดยค่าเริ่มต้น

สำหรับแอปพลิเคชันที่ยังใช้เวอร์ชัน Hibernate ก่อนหน้า รูปแบบ two-phase fetching ยังคงเป็นวิธีการที่ปลอดภัยที่สุด: ดึง ID หลักก่อนด้วยการแบ่งหน้า จากนั้นโหลดความสัมพันธ์สำหรับ ID เฉพาะเหล่านั้น

เปรียบเทียบกลยุทธ์การโหลด

แต่ละกลยุทธ์มีข้อได้เปรียบขึ้นอยู่กับบริบทการใช้งาน ตารางต่อไปนี้สรุปกรณีการใช้งานที่เหมาะสมที่สุด

กลยุทธ์กรณีการใช้งานข้อดีข้อเสีย
Fetch Joinความสัมพันธ์ @ManyToOneคิวรี SQL เดียวผลคูณคาร์ทีเซียนกับคอลเลกชัน
@EntityGraphการโหลดแบบประกาศใช้ซ้ำได้ อ่านง่ายยืดหยุ่นน้อยกว่า JPQL
Batch Fetchingคอลเลกชัน @OneToManyหลีกเลี่ยงผลคูณคาร์ทีเซียนคิวรีหลายรายการ
Subselectคอลเลกชันที่เข้าถึงไม่บ่อยโหลดเฉพาะเมื่อจำเป็นคิวรีย่อยที่สัมพันธ์กัน

กลยุทธ์ subselect โหลดทั้งคอลเลกชันเมื่อเข้าถึงองค์ประกอบใดๆ เป็นครั้งแรก

Order.javajava
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items = new ArrayList<>();
sql
-- 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 หลีกเลี่ยงการโหลดเอนทิตีและความสัมพันธ์ของพวกเขาโดยสิ้นเชิง รูปแบบนี้มีความเกี่ยวข้องเป็นพิเศษสำหรับ endpoint ที่เน้นการอ่าน ตามที่กล่าวถึงใน โมดูลคิวรี JPA

OrderSummaryDto.javajava
public record OrderSummaryDto(
    Long orderId,
    String orderNumber,
    String customerName,
    String customerEmail
) {}
OrderRepository.javajava
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 ที่เหมาะสมที่สุดโดยไม่มีค่าใช้จ่ายของการแมปเอนทิตี

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 ใน Spring Boot 4.x ให้การจัดการ EntityGraph ที่ปรับปรุงแล้วและการเพิ่มประสิทธิภาพคิวรี การทำความเข้าใจ transaction propagation กลายเป็นสิ่งจำเป็นเมื่อรวมกลยุทธ์ fetch เหล่านี้กับ business logic ที่ซับซ้อน

OrderRepository.javajava
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
    );
}

การโหลดแบบมีเงื่อนไขอนุญาตให้ใช้กลยุทธ์ที่แตกต่างกันตามบริบท

OrderService.javajava
@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 ที่เรียกใช้ สำหรับกลยุทธ์การทดสอบที่ครอบคลุม โปรดดู คู่มือ Testcontainers สำหรับ Spring Boot

OrderRepositoryPerformanceTest.javajava
@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() รับประกันว่าการเพิ่มประสิทธิภาพยังคงอยู่ระหว่างวิวัฒนาการของโค้ด

แหล่งอ้างอิง

  • Hibernate 7.4 What's New - การแบ่งหน้าด้วย fetch join ตอนนี้ปลอดภัยที่ระดับฐานข้อมูล
  • Spring Boot 4.1 Release Notes - รวม Hibernate 7.4.5.Final
  • Hibernate ORM Documentation - เอกสารอ้างอิงอย่างเป็นทางการสำหรับกลยุทธ์ fetch และ EntityGraph

รายการตรวจสอบการป้องกัน N+1 สำหรับ Spring Data JPA

ปัญหา N+1 เป็นความท้าทายด้านประสิทธิภาพที่สำคัญใน Spring Data JPA แต่มีวิธีแก้ที่มีประสิทธิภาพหลายวิธี Fetch join และ @EntityGraph แก้ปัญหาส่วนใหญ่โดยการโหลดความสัมพันธ์ในคิวรีเดียว Batch fetching เป็นทางเลือกสำหรับคอลเลกชันขนาดใหญ่ที่ fetch join สร้างผลคูณคาร์ทีเซียน ด้วย Hibernate 7.4 การแบ่งหน้าด้วย fetch join ในที่สุดก็ทำงานที่ระดับฐานข้อมูลได้

สิ่งที่ควรนำไปใช้ในระบบโปรดักชัน:

  • เปิดใช้งานบันทึก SQL ในการพัฒนาเพื่อตรวจจับคิวรีหลายรายการ
  • ใช้ JOIN FETCH สำหรับความสัมพันธ์ @ManyToOne ที่เข้าถึงบ่อย
  • ประยุกต์ใช้ @EntityGraph สำหรับการโหลดแบบประกาศที่ใช้ซ้ำได้
  • กำหนดค่า @BatchSize สำหรับคอลเลกชัน @OneToMany ขนาดใหญ่
  • ใช้ DTO projection สำหรับการดำเนินการแบบอ่านอย่างเดียว
  • เขียนการทดสอบเพื่อตรวจสอบจำนวนคิวรีที่เรียกใช้
  • ปิดใช้งานบันทึก SQL และสถิติในระบบโปรดักชัน
  • ทำโปรไฟล์ endpoint สำคัญเป็นประจำด้วยเมตริกของ Hibernate
  • อัปเกรดเป็น Spring Boot 4.1+ เพื่อใช้ประโยชน์จากการแก้ไขการแบ่งหน้าของ Hibernate 7.4
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Spring Boot เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 2 กันยายน 2569

แท็ก

#spring data jpa
#n+1 problem
#fetch join
#entitygraph
#performance

แชร์

บทความที่เกี่ยวข้อง