Pengurusan Transaksi Terprogram pada Musim Bunga

1. Gambaran keseluruhan

Anotasi @Transactional Spring menyediakan API deklaratif yang bagus untuk menandakan sempadan transaksi.

Di sebalik tabir, satu aspek menjaga pembuatan dan pemeliharaan transaksi seperti yang ditentukan dalam setiap kejadian anotasi @Transactional . Pendekatan ini menjadikannya mudah untuk mencabut logik perniagaan teras kami dari keprihatinan rentas seperti pengurusan transaksi.

Dalam tutorial ini, kita akan melihat bahawa ini bukan pendekatan terbaik. Kami akan meneroka alternatif yang disediakan oleh Spring, seperti TransactionTemplate , dan alasan kami menggunakannya.

2. Masalah di Syurga

Katakan kita mencampurkan dua jenis I / O yang berbeza dalam perkhidmatan mudah:

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

Di sini, kami mempunyai beberapa panggilan pangkalan data di samping panggilan REST API yang mungkin mahal. Pada pandangan pertama, mungkin masuk akal untuk membuat keseluruhan kaedah transaksional, kerana kita mungkin ingin menggunakan satu EntityManager untuk melakukan keseluruhan operasi secara atom.

Walau bagaimanapun, jika API luaran memerlukan masa lebih lama daripada biasa untuk bertindak balas, atas sebab apa pun, kami mungkin kehabisan sambungan pangkalan data!

2.1. Sifat Realiti yang Kasar

Inilah yang berlaku apabila kita memanggil kaedah Pembayaran awal :

  1. Aspek transaksi membuat EntityManager baru dan memulakan transaksi baru - jadi, ia meminjam satu Sambungan dari kumpulan sambungan
  2. Selepas panggilan pangkalan data pertama, ia memanggil API luaran sambil mengekalkan Sambungan yang dipinjam
  3. Akhirnya, ia menggunakan Connection untuk melakukan panggilan pangkalan data yang tinggal

Sekiranya panggilan API bertindak balas dengan perlahan untuk sementara waktu, kaedah ini akan memonopoli Sambungan yang dipinjam sementara menunggu respons .

Bayangkan bahawa dalam tempoh ini, kami mendapat banyak panggilan ke kaedah Pembayaran awal . Kemudian, semua Sambungan mungkin menunggu tindak balas dari panggilan API. Itulah sebabnya kami kehabisan sambungan pangkalan data - kerana perkhidmatan back-end yang lambat!

Mencampurkan pangkalan data I / O dengan jenis I / O lain dalam konteks transaksi adalah bau busuk. Oleh itu, penyelesaian pertama untuk masalah seperti ini adalah memisahkan jenis I / O ini . Sekiranya atas sebab apa pun kita tidak dapat memisahkannya, kita masih boleh menggunakan Spring API untuk menguruskan transaksi secara manual.

3. Menggunakan TransactionTemplate

TransactionTemplate menyediakan sekumpulan API berasaskan panggilan balik untuk menguruskan transaksi secara manual. Untuk menggunakannya, pertama, kita harus memulakannya dengan PlatformTransactionManager.

Sebagai contoh, kita boleh menyediakan templat ini menggunakan suntikan kebergantungan:

// test annotations class ManualTransactionIntegrationTest { @Autowired private PlatformTransactionManager transactionManager; private TransactionTemplate transactionTemplate; @BeforeEach void setUp() { transactionTemplate = new TransactionTemplate(transactionManager); } // omitted }

The PlatformTransactionManager membantu template untuk membuat, melakukan, atau transaksi kembalikan.

Semasa menggunakan Spring Boot, biji jenis PlatformTransactionManager yang sesuai akan didaftarkan secara automatik, jadi kami hanya perlu memasukkannya. Jika tidak, kita harus mendaftar kacang PlatformTransactionManager secara manual .

3.1. Contoh Model Domain

Mulai sekarang, demi demonstrasi, kami akan menggunakan model domain pembayaran yang dipermudahkan. Dalam domain sederhana ini, kami mempunyai entiti Pembayaran untuk merangkumi setiap butiran pembayaran:

@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 } }

Juga, kami akan menjalankan semua ujian di dalam kelas ujian, menggunakan perpustakaan Testcontainers untuk menjalankan instance PostgreSQL sebelum setiap kes ujian:

@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. Transaksi dengan Hasil

The TransactionTemplate tawaran kaedah yang disebut melaksanakan, yang boleh menjalankan mana-mana blok diberikan kod di dalam transaksi dan kemudian kembali beberapa keputusan:

@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(); }

Di sini, kami memasukkan contoh Pembayaran baru ke dalam pangkalan data dan kemudian mengembalikan id yang dihasilkan secara automatik.

Sama seperti pendekatan deklaratif, templat dapat menjamin keberagaman bagi kita. Iaitu, jika salah satu operasi dalam transaksi gagal diselesaikan, itumengembalikan kesemuanya:

@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(); }

Oleh kerana nombor rujukan kedua adalah pendua, pangkalan data menolak operasi berterusan kedua, menyebabkan keseluruhan transaksi kembali. Oleh itu, pangkalan data tidak mengandungi pembayaran selepas transaksi. Anda juga boleh mencetuskan kemunduran secara manual dengan memanggil setRollbackOnly () pada TransactionStatus:

@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. Urus Niaga Tanpa Hasil

Sekiranya kita tidak bermaksud mengembalikan apa-apa dari transaksi, kita boleh menggunakan kelas panggilan balik 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. Konfigurasi Transaksi Tersuai

Up until now, we used the TransactionTemplate with its default configuration. Although this default is more than enough most of the time, it's still possible to change configuration settings.

For example, we can set the transaction isolation level:

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

Similarly, we can change the transaction propagation behavior:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Or we can set a timeout, in seconds, for the transaction:

transactionTemplate.setTimeout(1000);

It's even possible to benefit from optimizations for read-only transactions:

transactionTemplate.setReadOnly(true);

Anyway, once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.

4. Using PlatformTransactionManager

In addition to the TransactionTemplate, we can use an even lower-level API like PlatformTransactionManager to manage transactions manually. Quite interestingly, both @Transactional and TransactionTemplate use this API to manage their transactions internally.

4.1. Configuring Transactions

Before using this API, we should define how our transaction is going to look. For example, we can set a three-second timeout with the repeatable read transaction isolation level:

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

Transaction definitions are similar to TransactionTemplate configurations. However, we can use multiple definitions with just one PlatformTransactionManager.

4.2. Maintaining Transactions

Setelah mengkonfigurasi transaksi kami, kami dapat menguruskan transaksi secara terprogram:

@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. Kesimpulan

Dalam tutorial ini, pertama, kita melihat kapan seseorang harus memilih pengurusan transaksi terprogram daripada pendekatan deklaratif. Kemudian, dengan memperkenalkan dua API yang berbeza, kami belajar bagaimana membuat, melakukan, atau memutar balik transaksi yang diberikan secara manual.

Seperti biasa, kod sampel boleh didapati di GitHub.