Học viên: Vũ Bá Phúc
Lớp: Java Fullstack 15
Email: phucnhan20022022@gmail.com
Số điện thoại: 0968616076
Nguồn tham khảo : https://www.baeldung.com

1. Giới Thiệu

Chú thích @Transactional của Spring cung cấp một API khai báo rõ ràng để đánh dấu ranh giới của giao dịch.

Đằng sau đó, một khía cạnh sẽ quản lý việc tạo và duy trì các giao dịch khi được định nghĩa trong mỗi lần sử dụng chú thích @Transactional. Phương pháp này giúp cho việc tách logic cốt lõi của doanh nghiệp ra khỏi những mối quan tâm chung như quản lý giao dịch.

Trong hướng dẫn này, chúng ta sẽ thấy rằng đây không phải lúc nào cũng là phương pháp tốt nhất. Chúng ta sẽ khám phá những lựa chọn chương trình khác mà Spring cung cấp, như TransactionTemplate, và lý do sử dụng chúng.

2. Trouble in Paradise

Giả sử chúng ta đang trộn hai loại I/O khác nhau trong một dịch vụ đơn giản:

@Transactional
public void initialPayment(PaymentRequest request) {
    savePaymentRequest(request); // DB
    callThePaymentProviderApi(request); // API
    updatePaymentState(request); // DB
    saveHistoryForAuditing(request); // DB
}

Ở đây chúng ta có một số lệnh gọi cơ sở dữ liệu cùng với lệnh gọi REST API có thể tốn kém. Đầu tiên, có vẻ như việc làm cho toàn bộ phương thức thành giao dịch sẽ hợp lý vì chúng ta có thể sử dụng một EntityManager để thực hiện toàn bộ hoạt động một cách chi tiết.

Tuy nhiên nếu API bên ngoài đó mất nhiều thời gian hơn bình thường để phản hồi vì bất kì lý do gì, chúng ta có thể sớm hết tài nguyên kết nối đến cơ sở dữ liệu.

2.1. The Harsh Nature of Reality

Sau đây là những gì xảy ra khi chúng ta gọi phương thức initialPayment:

  1. Khía cạnh giao dịch tạo ra một EntityManager mới và bắt đầu một giao dịch mới, vì vậy nó mượn một kết nối từ bể kết nối.
  2. Sau lần gọi cơ sở dữ liệu đầu tiên, nó gọi tới API bên ngoài trong khi vẫn giữ kết nối đã mượn.
  3. Cuối cùng, nó sử dụng kết nối đó để thực hiện các cuộc gọi cơ sở dữ liệu còn lại.

Nếu cuộc gọi API phản hồi rất chậm trong một thời gian, phương thức này sẽ giữ kết nối đã mượn trong khi đang chờ phản hồi.

Giả sử trong giai đoạn này chúng ta nhận được một loạt các cuộc gọi đến phương thức initialPayment. Trong trường hợp đó, tất cả các Connection có thể đợi phản hồi từ cuộc gọi API. Đó là lý do tại sao chúng ta có thể hết kết nối cơ sở dữ liệu - do một service phía sau bị chậm.

Trộn I/O cơ sở dữ liệu với các loại I/O khác trong bối cảnh transactionalkhông phải là ý tưởng tuyệt vời. Vì vậy, giải pháp đầu tiên cho những vấn đề này là tách các loại I/O này hoàn toàn. Nếu vì bất kỳ lý do gì mà chúng ta không thể tách chúng, chúng ta vẫn có thể sử dụng các API của Spring để quản lý transaction thủ công.

3. Using TransactionTemplate

TransactionTemplate cung cấp một tập hợp API dựa trên lệnh callback để quản lý các giao dịch theo cách thủ công. Để sử dụng nó, trước tiên chúng ta nên khởi tạo nó với một PlatformTransactionManager.
Chúng ta có thể thiết lập mẫu này bằng cách sử dụng dependency injection:

// test annotations
class ManualTransactionIntegrationTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // omitted
}

PlatformTransactionManager giúp template tạo, commit hoặc rollback các giao dịch.

Khi sử dụng Spring Boot, một bean thích hợp kiểu PlatformTransactionManager sẽ được đăng ký tự động, do đó chúng ta chỉ cần inject nó vào một cách đơn giản. Nếu không, chúng ta cần đăng ký một bean PlatformTransactionManager theo cách thủ công.

3.1. Sample Domain Model

Từ đây trở đi, để minh họa, chúng ta sẽ sử dụng một mô hình lĩnh vực thanh toán đơn giản hóa.

Trong lĩnh vực đơn giản này, chúng ta có một thực thể Thanh toán để đóng gói chi tiết của mỗi khoản thanh toán:

@Entity
public class Payment {

    @Id
    @GeneratedValue
    private Long id;

    private Long amount;

    @Column(unique = true)
    private String referenceNumber;

    @Enumerated(EnumType.STRING)
    private State state;

    // getters and setters

    public enum State {
        STARTED, FAILED, SUCCESSFUL
    }
}

Cũng như vậy, chúng ta sẽ chạy tất cả các bài test trong một lớp test, sử dụng thư viện Testcontainers để chạy một phiên bản PostgreSQL trước mỗi trường hợp test:

@DataJpaTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {

    @Autowired 
    private PlatformTransactionManager transactionManager;

    @Autowired 
    private EntityManager entityManager;

    @Container
    private static PostgreSQLContainer<?> pg = initPostgres();

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    public void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // tests

    private static PostgreSQLContainer<?> initPostgres() {
        PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:11.1")
                .withDatabaseName("baeldung")
                .withUsername("test")
                .withPassword("test");
        pg.setPortBindings(singletonList("54320:5432"));

        return pg;
    }
}

3.2. Transactions With Results

*TransactionTemplate * cung cấp một phương thức là execute, có thể chạy bất kỳ khối mã cụ thể nào bên trong một giao dịch và sau đó trả về một số kết quả:

@Test
void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
    Long id = transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);

        return payment.getId();
    });

    Payment payment = entityManager.find(Payment.class, id);
    assertThat(payment).isNotNull();
}

Ở đây, chúng ta đang lưu trữ một Payment mới vào cơ sở dữ liệu và sau đó trả về id được tạo tự động của nó.

Tương tự với cách tiếp cận khai báo, template có thể đảm bảo tính nguyên tử cho chúng ta.

Nếu một trong các hoạt động bên trong một giao dịch không hoàn thành, nó sẽ khôi phục tất cả chúng:

@Test
void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
    try {
        transactionTemplate.execute(status -> {
            Payment first = new Payment();
            first.setAmount(1000L);
            first.setReferenceNumber("Ref-1");
            first.setState(Payment.State.SUCCESSFUL);

            Payment second = new Payment();
            second.setAmount(2000L);
            second.setReferenceNumber("Ref-1"); // same reference number
            second.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(first); // ok
            entityManager.persist(second); // fails

            return "Ref-1";
        });
    } catch (Exception ignored) {}

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

referenceNumber thứ hai là một bản sao, cơ sở dữ liệu từ chối hoạt động persist thứ hai, gây ra việc toàn bộ giao dịch bị rollback. Do đó, cơ sở dữ liệu không chứa bất kỳ thanh toán nào sau giao dịch.

Bạn cũng có thể gây ra rollback bằng cách gọi setRollbackOnly() trên TransactionStatus một cách thủ công:

@Test
void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
    transactionTemplate.execute(status -> {
        Payment payment = new Payment();
        payment.setAmount(1000L);
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        status.setRollbackOnly();

        return payment.getId();
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}

3.3. Transactions Without Results

Nếu chúng ta không có ý định trả về bất cứ điều gì từ giao dịch, chúng ta có thể sử dụng lớp callback TransactionCallbackWithoutResult:

@Test
void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            Payment payment = new Payment();
            payment.setReferenceNumber("Ref-1");
            payment.setState(Payment.State.SUCCESSFUL);

            entityManager.persist(payment);
        }
    });

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

3.4. Custom Transaction Configurations

Từ trước đến nay, chúng ta đã sử dụng TransactionTemplate với cấu hình mặc định của nó. Mặc dù cấu hình mặc định này đôi khi đã đủ cho phần lớn các trường hợp, nhưng vẫn có thể thay đổi các thiết lập cấu hình.

Hãy cài đặt cấu hình cấp độ cô lập của giao dịch (transaction isolation level):

transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);

Tương tự, chúng ta có thể thay đổi propagation trong transaction

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Hoặc chúng ta có thể đặt một thời gian giới hạn cho giao dịch, tính bằng giây:

transactionTemplate.setTimeout(1000);

Thậm chí có thể tận dụng được những tối ưu hóa cho các giao dịch chỉ đọc (read-only transactions):

transactionTemplate.setReadOnly(true);

Khi chúng ta tạo một TransactionTemplate với một cấu hình, tất cả các giao dịch sẽ sử dụng cấu hình đó để thực thi. Do đó, nếu chúng ta cần nhiều cấu hình khác nhau, chúng ta nên tạo nhiều phiên bản mẫu.

4. Using PlatformTransactionManager

Tương tự, ngoài TransactionTemplate chúng ta có thể sử dụng một API cấp thấp hơn như PlatformTransactionManager để quản lý các giao dịch một cách thủ công. Khá thú vị, khi cả @TransactionalTransactionTemplate đều sử dụng API này để quản lý các giao dịch của họ một cách nội bộ.

4.1. Configuring Transactions

Trước khi sử dụng API này, chúng ta cần xác định cách thức thực hiện giao dịch của chúng ta.

Hãy thiết lập một thời gian chờ ba giây với mức độ cô lập giao dịch đọc có thể lặp lại (repeatable read):

DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);

Định nghĩa giao dịch tương tự như cấu hình TransactionTemplate. Tuy nhiên, chúng ta có thể sử dụng nhiều định nghĩa với chỉ một PlatformTransactionManager.

4.2. Maintaining Transactions

Sau khi cấu hình giao dịch của chúng ta, chúng ta có thể quản lý giao dịch theo cách lập trình:

@Test
void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {
 
    // transaction definition

    TransactionStatus status = transactionManager.getTransaction(definition);
    try {
        Payment payment = new Payment();
        payment.setReferenceNumber("Ref-1");
        payment.setState(Payment.State.SUCCESSFUL);

        entityManager.persist(payment);
        transactionManager.commit(status);
    } catch (Exception ex) {
        transactionManager.rollback(status);
    }

    assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}

5. Kết Luận

Trong bài viết này, chúng ta đã thấy khi nào chúng ta nên chọn quản lý giao dịch bằng cách lập trình thay vì sử dụng phương pháp khai báo.

Sau đó, bằng cách giới thiệu hai API khác nhau, chúng ta đã học cách tạo, xác nhận hoặc hủy bỏ bất kỳ giao dịch nào.