Quản lý Transaction với Spring và JPA

Spring và Spring Data hỗ trợ việc quản lý transaction khiến cho việc này cực kỳ đơn giản, tất cả những gì chúng ta cần làm là chú thích một class hay một method với @Transactional annotation. Nhưng thật sự bên dưới nó đang làm gì? những method nào nên được chú thích bằng @Transactional? Và tại sao bạn có thể đặt propagation ở các mức độ khác nhau?

Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu các câu hỏi được đặt ra ở trên. Nhưng trước khi tìm hiểu về transaction trong Spring, chúng ta cần nhớ lại kiến thức một chút về transaction, quản lý transaction trong JDBC. Bởi vì transaction trong Spring cũng chỉ dựa trên database và JDBC mà thôi.

Transaction là gì?

Transaction quản lý những thay đổi mà bạn thực hiện trong một hoặc nhiều hệ thống, nó có thể database, message brokers, hoặc bất kỳ loại hệ thống phần mềm nào khác. Mục tiêu chính của giao dịch là cung cấp các đặc điểm ACID để đảm bảo tính nhất quán và hợp lệ của dữ liệu của bạn.

ACID transactions

Vốn dĩ một transaction được đặc trưng bởi 4 yếu tố (thường được gọi là ACID):

  • Atomicity quy định rằng tất cả các hoạt động của transaction hoặc là thực thi thành công hết hoặc là không có bất cứ hành động nào được thực khi có bất kỳ một hoạt động thực thi không thành công.
  • Consistency nghĩa là tất cả các ràng buộc toàn vẹn dữ liệu(constraints, key, data types, Trigger, Check) phải được thực thi thành công cho mọi transaction phát sinh xuống database, nhầm đảm bảo tính đúng đắn của dữ liệu.
  • Isolation đảm bảo các transaction xảy ra xen kẽ sẽ không làm ảnh hưởng đến tính nhất quán của dữ liệu. Các thay đổi dữ liệu bên trong mỗi transaction sẽ được cô lập, các transaction khác sẽ không thể nhìn thấy cho đến khi nó được đồng bộ xuống database. 
  • Durability đảm bảo một transaction thực thi thành công thì tất cả những thay đổi trong transaction phải được đồng bộ xuống database kể cả khi hệ thống xảy ra lỗi hoặc bị mất điện. 

Quản lý transaction trong JDBC

Có 3 thao tác chính bạn có thể thực hiện thông qua java.sql.Connection để kiểm soát transaction trên database.
try (Connection con = dataSource.getConnection()) {
    con.setAutoCommit(false);
 
    // do something ...
     
    con.commit();
} catch (SQLException e) {
    con.rollback();
}

Bạn phải:

  • Bắt đầu transaction bằng cách lấy một Connection và tắt tính năng auto-commit. Điều này cho phép chúng ta kiểm soát transaction. Nếu không, từng câu lệnh SQL sẽ được thực hiện trong các transaction riêng biệt.
  • Commit một transaction bằng cách gọi phương thức commit () trên Connection interfaxe. Điều này khiến database thực hiện tất cả các kiểm tra tính nhất quán cần thiết và lưu các dữ liệu được thay đổi bởi transaction.
  • Rollback (Khôi phục) tất cả các hoạt động trên một transaction bằng cách gọi hàm rollback(). Chúng ta thường thực hiện thao tác này nếu một câu lệnh SQL bị lỗi hoặc nếu bạn phát hiện ra lỗi trong logic nghiệp vụ của mình.

Như bạn có thể thấy, quản lý một transaction không phải quá khó, nhưng để triển khai chúng trong một ứng dụng lớn thì không dễ như vậy, điều đầu tiên chúng ta có thể thấy đó là việc lặp lại lặp lại code quá nhiều khi start transaction, commit và rollback. Chưa kể trong ứng dụng lớn chứa nhiều logic thì có lẽ sẽ rối tung mù khi mà chi chích những dòng code commit, rollback như thế kia. Đó là lý do tại sao Spring cung cấp cơ chế hỗ trợ quản lý transaction, giúp chúng ta chú tâm hơn vào business thay vì phải xử lý transaction cách thủ công như trên.

Quản lý transaction trong Spring

Như đã thảo luận ở trên, chúng ta có thể thấy rằng việc quản lý transaction trong JDBC khiến chúng ta phải lặp đi lặp đi các công việc như start, commit, rollback transaction. Spring cung cấp cơ chế hỗ trợ quản lý transaction tự động start, commit, hay rollback transaction tự động.

Ngoài ra, nó cũng có thể tích hợp với Hibermate, JPA transaction. Nếu các bạn đang sử dụng Spring Boot, chỉ cần chú thích một class, method hay interface với @Transactional annotation thì chúng sẽ được thực thi bên trong một transaction.

NOTE: Nếu bạn đang sử dụng Spring Boot và có spring-data hay spring-tx dependency thì cơ chế quản lý transaction của Spring sẽ được kích hoạt mặc định.

Nếu bạn đang sử dụng Spring mà không phải Spring Boot thì các bạn cần phải cấu hình với @EnableTransactionManagement để kích hoạt tính năng quản lý transaction trong Spring.

@Transactional annotation

Dưới đây là một đoạn mã của một service có chứa một method được chú thích với @Transactional annotation.

public interface AuthorService {

    void updateAuthorName(Long id, String name);
}
package com.deft.transactionexample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AuthorServiceImpl implements AuthorService {

    @Autowired
    private AuthorRepository authorRepository;

    @Override
    @Transactional
    public void updateAuthorName(Long id, String name) {

        Author author = authorRepository.findById(id).get();
        author.setName(name);
    }
}

@Transactional annotation sẽ nói với Spring rằng updateAuthorName() method cần được thực thi bên trong một transaction. Khi bạn sử dụng AuthorService ở một nơi nào nó, chẳng hạn như ở các controller class, Spring sẽ tạo ra một proxy object bao bọc AuthorService object và cung cấp các đoạn mã cần thiết để bắt đầu một transaction.

Mặc định, proxy sẽ start một transaction trước khi có một yêu cầu đến method được chú thích với @Transactional annotation. Sau khi method thực thi xong, proxy sẽ commit hoặc rollback transaction nếu có một RuntimeException hoặc Error xảy ra trong quá trình thực thi. Mọi thứ xảy ra ở giữa, chỉ là các đoạn mã code thực thi logic business do chính chúng ta viết. 

@Transactional annottion còn hỗ trợ cho chúng ta tuỳ biến một các hành vi của một transaction thông qua một số thuộc tính quan trọng như propagation, readOnly, rollbackFor, noRollbackFor.

Transaction Propagation

Spring cung cấp 7 tuỳ biến cho Propagation trong @Transactional annotation như sau:

REQUIRED

REQUIRED – Nói với Spring rằng nếu có một transaction đang hoạt động thì nó sẽ sử dụng chung, nếu không có transaction nào đang hoạt động, method được gọi sẽ tạo một transaction mới. Đây là giá trị mặc định của Propagation.

@RestController
@RequestMapping(value = "/author")
public class AuthorController {

    @Autowired
    private AuthorService authorService;

    @PutMapping("/{id}")
    public void updateAuthorName(@PathVariable Long id, @RequestParam String name) {
        authorService.updateAuthorName(id, name);
    }
}

Như trong ví dụ trên, AuthorController gọi đến authorService.updateAuthorName() không bao gồm 1 transaction, vì thế trong AuthorService#updateAuthorName() sẽ tạo ra một transaction mới để thực thi.

Output

Kết quả chúng ta có thể thấy tạo controller, mình đã sử dụng TransactionAspectSupport để lấy thông tin của transaction hiện tại, nhưng kết quả trả về NULL, nghĩa là tại controller không có transaction nào đang hoạt động.

Sau khi đi vào AuthorServiceImpl thì chúng ta có thể thấy nó đã khởi tạo một transaction mới dùng để thao tác với database.

SUPPORTS

SUPPORTS – Chỉ đơn giản là sử dụng lại transaction hiện đang hoạt động. Nếu không thì method được gọi sẽ thực thi mà không được đặt trong một transactional context nào.

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public void updateAuthorNameSupport(Long id, String name) {
    Author author = authorRepository.findById(id).orElse(null);
    author.setName(name);
}

Ví dụ trên chúng ta có thể lấy được một author trong database, tuy nhiên giá trị name mới được thay thế sẽ không được commit xuống database vì những hoạt động này không được đặt trong transactional context nào, vì vậy JPA sẽ không trigger những thay đổi và đồng bộ xuống database.

MANDATORY

MANDATORY yêu cầu phải có một transaction đang hoạt động trước khi gọi, nếu không method được gọi sẽ ném ra một exception.

@Override
@Transactional(propagation = Propagation.MANDATORY)
public void updateAuthorNameMandatory(Long id, String name) {
    Author author = authorRepository.findById(id).orElse(null);
    author.setName(name);
}

Như vậy khi bạn gọi updateAuthorNameMandatory() từ controller sẽ nhận lại một exception vì ở controller không có transaction nào được khởi tạo và hoạt động.

@PutMapping("/{id}")
public void updateAuthorName(@PathVariable Long id, @RequestParam String name) {
    authorService.updateAuthorNameMandatory(id, name);
}

Output

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

NEVER

NEVER sẽ ném một exception nếu method được gọi trong một transaction hoạt động.

NOT_SUPPORTED

Dừng transaction hiện tại và thực thi method mà không thuộc một transaction nào.

REQUIRES_NEW

Để luôn bắt đầu một transaction mới cho method được gọi. Nếu method được gọi với một transaction đang hoạt động, transaction đó sẽ bị tạm ngưng, một transaction mới sẽ được tạo và sử dụng cho method này.

Transaction mới vừa được tạo sẽ thực thi độc lập với transaction bên ngoài, khi transaction này kết thúc dữ liệu sẽ được đồng bộ xuống database. Sau đó transaction bên ngoài sẽ được kích hoạt và hoạt động trở lại.

AuthorServiceAImpl {
    @Override
    @Transactional
       public void updateAuthorNameRequireNew(Long id, String name) {
       Author author = authorRepository.findById(id).orElse(null);
       updateAnotherAuthor(2L, "new name");
       author.setName(name);
       throw new RuntimeException("no way");
   }
}

AuthorServiceBImpl {
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateAnotherAuthor(Long id, String name) {
       Author author = authorRepository.findById(id).orElse(null);
       author.setName(name);
    
    }
}

Như ví dụ trên, updateAnotherAuthor() được thực thi trong một transaction mới độc lập với transaction bên ngoài nên các thay đổi bên trong nó sẽ được đồng bộ xuống database bất kể updateAuthorNameRequireNew() xảy ra exception. Vì updateAuthorNameRequireNew() xảy ra exception nên các thay đổi dữ liệu sẽ không được đồng bộ xuống database.

NESTED

Method được gọi sẽ tạo một transaction mới nếu không có transaction nào đang hoạt động. Nếu method được gọi với một transaction đang hoạt động Spring sẽ tạo một savepoint và rollback tại đây nếu có Exception xảy ra. 

Read-Only transaction

Thuộc tính readOnly trong @Transactional annotation gây sự nhầm lẫn cho nhiều người đặc biệt là khi làm việc với JPA, trong Java docs của nó mô tả như sau:

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.

Hiểu đơn giản nghĩa là chúng ta không thể chắc chắn sẽ có các hoạt động INSERT hay UPDATE dữ liệu xảy ra trong một transacion được chú thích với readOnly. Hành vi này phụ thuộc vào nhà cung cấp các JPA implemetation (Như Hibernate, EclipseLink, etc) trong khi JPA không thể quản lý việc này từ nhà cung cấp.

Cũng cần hiểu rằng thuộc tính readOnly chỉ có liên quan trong một transaction. Nếu một hoạt động xảy ra bên ngoài ngữ cảnh của transaction, readOnly sẽ bị bỏ qua. Từ non-transactional context – một transaction sẽ không được tạo và thuộc tính readOnly sẽ bị bỏ qua.

Tuy nhiên, kể từ Spring 5.1 thuộc tính readOnly trong Hibernate sẽ giúp chúng ta tránh khởi các kiểm tra trên các thực thể cần truy xuất, từ đó có thể tối ưu hiệu xuất.

@Override
@Transactional(readOnly = true)
public Author getAuthorById(Long id) {
    Author author = authorRepository.findById(id).orElse(null);
    return author;
}

Handling Exceptions

Như đã thảo luận ở phần trước, Spring proxy sẽ tự động rollback transaction nếu có một RuntimeException xảy ra. Bạn có thể tùy biến bằng cách sử dụng thuộc tính rollbackFornoRollbackFor của @Transactional annotation.

Chúng ta có thể phỏng đoán ý nghĩa của chúng từ tên như thuộc tính rollbackFor cho phép bạn cung cấp một mảng các Exception class mà transaction sẽ bị rollback nếu chúng xảy ra. Và noRollbackFor được dùng để chỉ định một mảng các Exception class mà transaction sẽ không rollback khi chúng xảy ra.

Trong ví dụ sau, mình muốn rollback transaction cho tất cả các sub-class của Exception ngoại trừ EntityNotFoundException

@Override
@Transactional
        (rollbackFor = Exception.class,
                noRollbackFor = EntityNotFoundException.class)
public void updateAuthorWithRollbackCustom(Long id, String name) {
    Author author = authorRepository.findById(id).orElse(null);
    author.setName(name);
}

Kết bài

Spring và Spring Data JPA cung cấp cơ chế quản lý transaction giúp chúng ta dễ dàng hơn trong việc xử lý transaction. Bạn chỉ cần chú thích @Transactional annotation trên các interface, class, method và Spring sẽ bao bao ngoài chúng một proxy giúp chúng ta thực thi các thao tác tự động như start, commt hay rollback transaction.

Ngoài ra bạn có thể sử dụng readOnly flag để tối ưu hóa hiệu xuất cho các method chỉ thực thi các hoạt động đọc dữ liệu từ database.

Mặc định thì transaction chỉ được rollback khi có RuntimeException xảy ra. Bạn có thể tùy biến điều này với thuộc tính rollbackFor trong @Transactional annotation. 

Nếu muốn transaction rollback cho tất cả các lỗi xảy ra thì có thể sử dụng @Transactional (rollbackFor = Throwable.class)

Sau cùng để có thể hiểu và thực hành lý thuyết thông qua bài viết trên, các bạn có thể tham khảo mã nguồn được mình công khai trên gitlab: transaction-example

Nguồn tham khảo

https://thorben-janssen.com/transactions-spring-data-jpa/

https://www.baeldung.com/transaction-configuration-with-jpa-and-spring

5 5 votes
Article Rating
Subscribe
Notify of
guest
11 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
11
0
Would love your thoughts, please comment.x
()
x