Fix org.hibernate.LazyInitializationException: failed to lazily initialize a collection - no Session

intermediate Java2026-03-24| Java 8+, Hibernate 5.x/6.x, Spring Boot 2.x/3.x, JPA, các cơ sở dữ liệu quan hệ (MySQL, PostgreSQL, H2)

Error Message

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session
#java#hibernate#jpa#lazy-loading#spring#session

Lỗi

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session

Bạn fetch một entity trong transaction, session đóng lại, rồi có gì đó cố truy cập vào một relationship được load lazily. Hibernate không thể quay lại database — session đã không còn nữa.

Tình huống điển hình: một service method trả về entity, transaction kết thúc, rồi một controller hoặc JSON serializer đụng vào entity.getOrders(). Là xong.

Nguyên Nhân

JPA/Hibernate mặc định load các collection @OneToMany@ManyToMany theo kiểu lazy. Hibernate bỏ qua việc fetch chúng cho đến khi bạn thực sự truy cập — tiết kiệm bộ nhớ, tránh các query không cần thiết. Vấn đề là: một khi Session (hoặc EntityManager) đóng lại, việc trigger lazy load sẽ ném ra exception này.

Các nguyên nhân phổ biến:

  • Truy cập lazy collection bên ngoài method có @Transactional
  • open-session-in-view bị tắt — đây là cấu hình đúng cho môi trường production
  • Serialize entity sang JSON ở tầng controller sau khi transaction của service đã kết thúc
  • Gọi getter trong một background thread không có session đang hoạt động

Các Cách Sửa

Cách 1: Dùng Fetch Join Trong Query (Khuyến Nghị)

Một query thay vì hai — fetch join báo cho Hibernate load collection cùng lúc với entity cha. Không có thêm round trip, không có exception bất ngờ sau này.

// Trong repository của bạn
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);

Hoặc dùng JPA Criteria API:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
root.fetch("items", JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), orderId));

Load đúng thứ bạn cần, trong một lần, bên trong session. Không có gì có thể sai sau khi transaction đóng vì mọi thứ đã được load xong.

Cách 2: Mở Rộng Phạm Vi Transaction Với @Transactional

Khi lazy access xảy ra trong code service của bạn, hãy giữ session mở đủ lâu để khởi tạo những gì cần thiết.

@Service
public class OrderService {

    @Transactional  // Session vẫn mở trong suốt method
    public OrderDTO getOrderWithItems(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new EntityNotFoundException());
        
        // An toàn: session vẫn đang hoạt động ở đây
        order.getItems().size(); // trigger lazy load bên trong transaction
        
        return toDTO(order); // convert trước khi session đóng
    }
}

Hãy convert sang DTO bên trong transaction — không phải sau đó. Trả về entity thô và để controller đụng vào các lazy field sau này chính là nguyên nhân gây ra lỗi này ngay từ đầu.

Cách 3: Dùng DTO Và Ngừng Expose Entity

Trả entity trực tiếp từ service method thường là nơi vấn đề bắt đầu. Map sang DTO bên trong transaction và bạn sẽ không bao giờ phải lo về lazy loading nữa — session có thể đóng an toàn vì không còn gì để load.

@Transactional(readOnly = true)
public OrderDTO findOrder(Long id) {
    Order order = orderRepository.findByIdWithItems(id) // query với fetch join
        .orElseThrow(EntityNotFoundException::new);
    
    return new OrderDTO(
        order.getId(),
        order.getStatus(),
        order.getItems().stream()
            .map(item -> new ItemDTO(item.getId(), item.getName(), item.getPrice()))
            .toList()
    );
}

Cách 4: Đổi FetchType Sang EAGER (Dùng Thận Trọng)

Bạn có thể bắt Hibernate luôn load collection:

@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<Item> items;

Đơn giản, nhưng có cái giá thật sự. Mỗi lần bạn load một Order, Hibernate cũng load toàn bộ items — kể cả khi bạn không cần. Một entity với 3 quan hệ eager có thể bắn 4 query riêng biệt chỉ để fetch một row. Với danh sách 50 orders, đó là hơn 200 query. Chỉ dùng cách này cho các collection nhỏ mà luôn luôn cần thiết.

Cách 5: Dùng Hibernate.initialize() Cho Các Trường Hợp Đặc Biệt

Đôi khi bạn không thể tái cấu trúc query. Trong những trường hợp đó, hãy ép khởi tạo tường minh trong khi vẫn còn bên trong transaction:

@Transactional
public Order loadOrderWithItems(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    Hibernate.initialize(order.getItems()); // khởi tạo tường minh
    return order;
}

Đây là cách sửa có mục tiêu cụ thể. Nó hoạt động, nhưng hãy ưu tiên dùng fetch join trước — cách đó rõ ràng hơn và giữ được ý định trong query.

Cách 6: Bật open-in-view (Chỉ Dành Cho Development)

Spring Boot mặc định đặt spring.jpa.open-in-view=true. Điều này giữ session mở trong suốt HTTP request, nên lazy load hoạt động ở bất kỳ đâu trong vòng đời request. Nó che giấu vấn đề trong quá trình development, nhưng giữ kết nối database mở lâu hơn cần thiết — gây ra vấn đề thực sự khi tải cao.

# application.properties
spring.jpa.open-in-view=false  # Tắt cái này trong production

Hãy tắt nó đi. Bạn sẽ thấy exception ngay lập tức, buộc bạn phải sửa vấn đề thực sự thay vì che giấu nó.

Kiểm Tra

Chạy kiểm tra nhanh trước khi deploy:

  • Xác nhận spring.jpa.open-in-view=false — nếu không bạn đang test trong điều kiện không thực tế
  • Gọi endpoint đang throw exception và kiểm tra response
  • Quét application logs để tìm các stack trace LazyInitializationException còn sót lại
  • Bật SQL logging để phát hiện các vấn đề N+1 do cách sửa của bạn gây ra:
# application.properties
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE

Một query trên mỗi entity trong log có nghĩa là N+1. Hãy chuyển sang dùng fetch join.

Tóm Tắt Nhanh

  • Collection được truy cập trong vòng lặp → nguy cơ N+1: Dùng fetch join hoặc @EntityGraph
  • Serialization ở controller bị lỗi: Convert sang DTO bên trong method service có @Transactional
  • Background thread bị crash: Mở transaction mới tường minh bằng TransactionTemplate
  • Chỉ trong môi trường test: Thêm @Transactional vào test class để giữ session mở trong quá trình kiểm tra
// EntityGraph thay thế fetch join — giữ repository gọn gàng
@EntityGraph(attributePaths = {"items", "items.product"})
Optional<Order> findById(Long id);

Quy tắc rất đơn giản: đừng bao giờ để một Hibernate entity vượt qua ranh giới transaction với các proxy chưa được khởi tạo. Hãy quyết định bạn cần dữ liệu gì, load tất cả bên trong session, rồi chuyển một DTO thuần túy cho phần còn lại của ứng dụng.

Related Error Notes