Cách khắc phục javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available

intermediate Java2026-04-11| Java 8+, Spring Boot 2.x/3.x, Hibernate 5/6, Spring Data JPA

Error Message

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread
#java#spring-boot#jpa#hibernate#transaction

Lỗi

Có lẽ bạn đã từng gặp lỗi này: bạn kích hoạt một cập nhật cơ sở dữ liệu, và thay vì nhận được thông báo thành công, console của bạn bùng nổ với lỗi javax.persistence.TransactionRequiredException. Điều này xảy ra vì JPA rất nghiêm ngặt về tính toàn vẹn dữ liệu. Nó từ chối thực thi các thao tác "ghi"—như cập nhật hoặc xóa—trừ khi nó có một ngữ cảnh giao dịch (transaction context) được đảm bảo để bao bọc chúng.

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread

Hãy coi transaction như một tấm lưới an toàn. Nếu không có nó, Hibernate sẽ không biết khi nào nên commit các thay đổi của bạn hoặc khi nào nên rollback nếu có sự cố xảy ra. Nếu bạn cố gắng sửa đổi dữ liệu bên ngoài tấm lưới này, JPA sẽ kéo phanh khẩn cấp.

Tại sao lỗi này xảy ra?

EntityManager trong Spring là một proxy dùng chung. Khi bạn gọi một phương thức repository, proxy này sẽ tìm kiếm một transaction đang hoạt động gắn với thread hiện tại. Nếu không tìm thấy và bạn đang cố gắng thay đổi dữ liệu, nó sẽ ném ra ngoại lệ này. Thủ phạm thường rơi vào một trong bốn kịch bản sau:

  • Thiếu Annotation: Bạn quên đặt @Transactional trên phương thức service của mình.
  • Bẫy Tự Gọi (Self-Invocation): Bạn gọi một phương thức có transaction từ một phương thức khác trong cùng một class.
  • Truy vấn Thủ công: Bạn đang sử dụng truy vấn @Modifying tùy chỉnh nhưng không xác định ranh giới transaction.
  • Ranh giới Thread: Bạn đã bắt đầu một thread mới (thông qua @Async hoặc CompletableFuture), và transaction đã không đi theo bạn sang đó.

Các bước khắc phục

1. Bao bọc Logic Service của bạn

Cách khắc phục đơn giản nhất là đặt @Transactional ở tầng service. Điều này yêu cầu Spring bắt đầu một transaction ngay khi phương thức bắt đầu và commit ngay khi phương thức kết thúc thành công.

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void deactivateUser(Long userId) {
        // Spring hiện đảm bảo một transaction đang hoạt động tại đây
        userRepository.updateStatus(userId, "INACTIVE");
    }
}

2. Xử lý các truy vấn @Modifying tùy chỉnh

Khi bạn viết một truy vấn UPDATE hoặc DELETE tùy chỉnh trong một JpaRepository, Spring Data JPA không tự động giả định nó là một phần của một transaction. Bạn phải chỉ định rõ ràng.

Cách làm sai:

public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Query("UPDATE User u SET u.lastLogin = NOW() WHERE u.id = :id")
    void updateLoginTimestamp(Long id);
}

Cách làm đúng:

Mặc dù bạn có thể thêm @Transactional vào repository, nhưng thường tốt hơn là giữ nó ở tầng Service để quản lý các logic nghiệp vụ phức tạp. Nếu repository được sử dụng độc lập, hãy thêm nó như sau:

public interface UserRepository extends JpaRepository<User, Long> {
    @Transactional
    @Modifying
    @Query("UPDATE User u SET u.lastLogin = NOW() WHERE u.id = :id")
    void updateLoginTimestamp(Long id);
}

3. Phá vỡ vòng lặp Tự Gọi (Self-Invocation)

Đây là "kẻ giết người thầm lặng" của các Spring transaction. Spring sử dụng các AOP proxy để quản lý transaction. Nếu Method A gọi Method B trong cùng một class, bạn đang bỏ qua proxy đó. Nó giống như việc có một nhân viên bảo vệ ở cửa trước, nhưng bạn lại sử dụng hành lang nội bộ để đi đến kho tiền—nhân viên bảo vệ không bao giờ nhìn thấy bạn.

@Service
public class OrderService {

    public void processOrder(Long id) {
        // Lời gọi này bỏ qua Proxy! @Transactional bên dưới bị ngó lơ.
        saveOrder(id);
    }

    @Transactional
    public void saveOrder(Long id) {
        repository.updateStatus(id, "PROCESSED");
    }
}

Cách khắc phục: Luôn chú thích phương thức điểm đầu vào (processOrder) hoặc chuyển logic transaction sang một class service riêng biệt để proxy có thể thực hiện nhiệm vụ của nó.

4. Transaction trong các Thread chạy ngầm

Transaction gắn liền với thread. Nếu bạn sử dụng @Async, thread mới sẽ không thấy transaction từ phía người gọi. Bạn phải đảm bảo bản thân phương thức @Async được đánh dấu @Transactional.

@Async
@Transactional
public void processInBackground(Long entityId) {
    // Cách này hoạt động vì thread mới bắt đầu transaction của riêng nó
    repository.deleteOldRecords(entityId);
}

Cách kiểm tra kết quả

Đừng chỉ hy vọng nó hoạt động—hãy kiểm tra nó. Bạn có thể theo dõi hành vi của transaction bằng cách thêm dòng này vào file application.properties của bạn:

logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG

Khi bạn chạy mã của mình, hãy tìm các bản ghi log thông báo Creating new transactionInitiating transaction commit. Nếu bạn không thấy những dòng này xung quanh lệnh gọi cơ sở dữ liệu của mình, transaction của bạn không hoạt động, ngay cả khi lỗi đã biến mất (điều này có thể xảy ra với một số cấu hình Hibernate nhất định, dẫn đến mất dữ liệu trong im lặng).

Mẹo chuyên nghiệp để phòng tránh

  • Mặc định là Read-Only: Sử dụng @Transactional(readOnly = true) ở cấp độ class và ghi đè nó bằng một @Transactional tiêu chuẩn trên các phương thức thực sự thay đổi dữ liệu. Điều này cải thiện hiệu suất bằng cách bỏ qua quá trình dirty-checking của Hibernate.
  • Ranh giới ở tầng Service: Hãy giữ các transaction ở tầng Service. Đặt chúng ở Controller là quá muộn, và đặt chúng ở Repository thường quá vụn vặt.
  • Theo dõi Log: Nếu bạn thấy TransactionRequiredException, điều đầu tiên bạn nên kiểm tra là liệu bạn có đang gọi một phương thức trong cùng một class hay không. 90% trường hợp, tự gọi (self-invocation) chính là thủ phạm.

Related Error Notes