diff --git a/jta/pom.xml b/jta/pom.xml new file mode 100644 index 0000000000..3eb0e294d5 --- /dev/null +++ b/jta/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + com.baeldung + jta-demo + 1.0-SNAPSHOT + jar + + JEE JTA demo + + + org.springframework.boot + spring-boot-starter-parent + 2.0.4.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-jta-bitronix + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.hsqldb + hsqldb + 2.4.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java b/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java new file mode 100644 index 0000000000..f7b1cbbb4d --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java @@ -0,0 +1,60 @@ +package com.baeldung.jtademo; + +import com.baeldung.jtademo.services.TellerService; +import org.hsqldb.jdbc.JDBCDataSourceFactory; +import org.hsqldb.jdbc.pool.JDBCXADataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.jta.bitronix.BitronixXADataSourceWrapper; +import org.springframework.boot.jta.bitronix.PoolingDataSourceBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.util.Assert; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; + +@EnableAutoConfiguration +@EnableTransactionManagement +public class JtaDemoApplication { + + @Bean("dataSourceAccount") + public DataSource dataSource() throws Exception { + return createHsqlXADatasource("jdbc:hsqldb:mem:accountDb"); + } + + @Bean("dataSourceAudit") + public DataSource dataSourceAudit() throws Exception { + return createHsqlXADatasource("jdbc:hsqldb:mem:auditDb"); + } + + private DataSource createHsqlXADatasource(String connectionUrl) throws Exception { + JDBCXADataSource dataSource = new JDBCXADataSource(); + dataSource.setUrl(connectionUrl); + dataSource.setUser("sa"); + BitronixXADataSourceWrapper wrapper = new BitronixXADataSourceWrapper(); + return wrapper.wrapDataSource(dataSource); + } + + @Bean("jdbcTemplateAccount") + public JdbcTemplate jdbcTemplate(@Qualifier("dataSourceAccount") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean("jdbcTemplateAudit") + public JdbcTemplate jdbcTemplateAudit(@Qualifier("dataSourceAudit") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java b/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java new file mode 100644 index 0000000000..cc1474ce81 --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java @@ -0,0 +1,27 @@ +package com.baeldung.jtademo.dto; + +import java.math.BigDecimal; + +public class TransferLog { + private String fromAccountId; + private String toAccountId; + private BigDecimal amount; + + public TransferLog(String fromAccountId, String toAccountId, BigDecimal amount) { + this.fromAccountId = fromAccountId; + this.toAccountId = toAccountId; + this.amount = amount; + } + + public String getFromAccountId() { + return fromAccountId; + } + + public String getToAccountId() { + return toAccountId; + } + + public BigDecimal getAmount() { + return amount; + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java b/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java new file mode 100644 index 0000000000..098a6f9e1f --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java @@ -0,0 +1,25 @@ +package com.baeldung.jtademo.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.math.BigDecimal; + +@Service +public class AuditService { + + final JdbcTemplate jdbcTemplate; + + @Autowired + public AuditService(@Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional + public void log(String fromAccount, String toAccount, BigDecimal amount) { + jdbcTemplate.update("insert into AUDIT_LOG(FROM_ACCOUNT, TO_ACCOUNT, AMOUNT) values ?,?,?", fromAccount, toAccount, amount); + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/services/BankAccountManualTxService.java b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountManualTxService.java new file mode 100644 index 0000000000..acbef33427 --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountManualTxService.java @@ -0,0 +1,27 @@ +package com.baeldung.jtademo.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.transaction.UserTransaction; +import java.math.BigDecimal; + +@Component +public class BankAccountManualTxService { + @Resource + UserTransaction userTransaction; + + @Autowired + @Qualifier("jdbcTemplateAccount") + JdbcTemplate jdbcTemplate; + + public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) throws Exception { + userTransaction.begin(); + jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE-? where ID=?", amount, fromAccountId); + jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE+? where ID=?", amount, toAccountId); + userTransaction.commit(); + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java new file mode 100644 index 0000000000..bb59437add --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java @@ -0,0 +1,27 @@ +package com.baeldung.jtademo.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.math.BigDecimal; + +@Service +public class BankAccountService { + + final JdbcTemplate jdbcTemplate; + + @Autowired + public BankAccountService(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional + public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) { + jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE-? where ID=?", amount, fromAccountId); + jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE+? where ID=?", amount, toAccountId); + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java b/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java new file mode 100644 index 0000000000..8bef892ca2 --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java @@ -0,0 +1,32 @@ +package com.baeldung.jtademo.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.math.BigDecimal; + +@Service +public class TellerService { + private final BankAccountService bankAccountService; + private final AuditService auditService; + + @Autowired + public TellerService(BankAccountService bankAccountService, AuditService auditService) { + this.bankAccountService = bankAccountService; + this.auditService = auditService; + } + + @Transactional + public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) { + bankAccountService.transfer(fromAccontId, toAccountId, amount); + auditService.log(fromAccontId, toAccountId, amount); + } + + @Transactional + public void executeTransferFail(String fromAccontId, String toAccountId, BigDecimal amount) { + bankAccountService.transfer(fromAccontId, toAccountId, amount); + auditService.log(fromAccontId, toAccountId, amount); + throw new RuntimeException("Something wrong, rollback!"); + } +} diff --git a/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java b/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java new file mode 100644 index 0000000000..21370cf034 --- /dev/null +++ b/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java @@ -0,0 +1,59 @@ +package com.baeldung.jtademo.services; + +import com.baeldung.jtademo.dto.TransferLog; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; + +@Component +public class TestHelper { + final JdbcTemplate jdbcTemplateAccount; + + final JdbcTemplate jdbcTemplateAudit; + + @Autowired + public TestHelper(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplateAccount, @Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplateAudit) { + this.jdbcTemplateAccount = jdbcTemplateAccount; + this.jdbcTemplateAudit = jdbcTemplateAudit; + } + + public void runAccountDbInit() throws SQLException { + runScript("account.sql", jdbcTemplateAccount.getDataSource()); + } + + public void runAuditDbInit() throws SQLException { + runScript("audit.sql", jdbcTemplateAudit.getDataSource()); + } + + private void runScript(String scriptName, DataSource dataSouorce) throws SQLException { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + Resource script = resourceLoader.getResource(scriptName); + try (Connection con = dataSouorce.getConnection()) { + ScriptUtils.executeSqlScript(con, script); + } + } + + public TransferLog lastTransferLog() { + return jdbcTemplateAudit.query("select FROM_ACCOUNT,TO_ACCOUNT,AMOUNT from AUDIT_LOG order by ID desc", (ResultSetExtractor) (rs) -> { + if(!rs.next()) return null; + return new TransferLog(rs.getString(1), rs.getString(2), BigDecimal.valueOf(rs.getDouble(3))); + }); + } + + public BigDecimal balanceOf(String accountId) { + return jdbcTemplateAccount.query("select BALANCE from ACCOUNT where ID=?", new Object[] { accountId }, (ResultSetExtractor) (rs) -> { + rs.next(); + return new BigDecimal(rs.getDouble(1)); + }); + } +} diff --git a/jta/src/main/resources/account.sql b/jta/src/main/resources/account.sql new file mode 100644 index 0000000000..af14f89b01 --- /dev/null +++ b/jta/src/main/resources/account.sql @@ -0,0 +1,9 @@ +DROP SCHEMA PUBLIC CASCADE; + +create table ACCOUNT ( +ID char(8) PRIMARY KEY, +BALANCE NUMERIC(28,10) +); + +insert into ACCOUNT(ID, BALANCE) values ('a0000001', 1000); +insert into ACCOUNT(ID, BALANCE) values ('a0000002', 2000); \ No newline at end of file diff --git a/jta/src/main/resources/application.properties b/jta/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jta/src/main/resources/audit.sql b/jta/src/main/resources/audit.sql new file mode 100644 index 0000000000..aa5845f402 --- /dev/null +++ b/jta/src/main/resources/audit.sql @@ -0,0 +1,8 @@ +DROP SCHEMA PUBLIC CASCADE; + +create table AUDIT_LOG ( +ID INTEGER IDENTITY PRIMARY KEY, +FROM_ACCOUNT varchar(8), +TO_ACCOUNT varchar(8), +AMOUNT numeric(28,10) +); \ No newline at end of file diff --git a/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java b/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java new file mode 100644 index 0000000000..68ce5531a1 --- /dev/null +++ b/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java @@ -0,0 +1,70 @@ +package com.baeldung.jtademo; + +import com.baeldung.jtademo.dto.TransferLog; +import com.baeldung.jtademo.services.BankAccountManualTxService; +import com.baeldung.jtademo.services.TellerService; +import com.baeldung.jtademo.services.TestHelper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = JtaDemoApplication.class) +public class JtaDemoUnitTest { + @Autowired + TestHelper testHelper; + + @Autowired + TellerService tellerService; + + @Autowired + BankAccountManualTxService bankAccountManualTxService; + + @Before + public void beforeTest() throws Exception { + testHelper.runAuditDbInit(); + testHelper.runAccountDbInit(); + } + + @Test + public void whenNoException_thenAllCommitted() throws Exception { + tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500)); + + BigDecimal result = testHelper.balanceOf("a0000001"); + assertThat(testHelper.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(500)); + assertThat(testHelper.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2500)); + + TransferLog lastTransferLog = testHelper.lastTransferLog(); + assertThat(lastTransferLog).isNotNull(); + assertThat(lastTransferLog.getFromAccountId()).isEqualTo("a0000001"); + assertThat(lastTransferLog.getToAccountId()).isEqualTo("a0000002"); + assertThat(lastTransferLog.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(500)); + } + + @Test + public void whenException_thenAllRolledBack() throws Exception { + assertThatThrownBy(() -> { + tellerService.executeTransferFail("a0000002", "a0000001", BigDecimal.valueOf(100)); + }).hasMessage("Something wrong, rollback!"); + + assertThat(testHelper.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000)); + assertThat(testHelper.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000)); + + assertThat(testHelper.lastTransferLog()).isNull(); + } + + @Test + public void givenBMT_whenNoException_thenAllCommitted() throws Exception { + bankAccountManualTxService.transfer("a0000001", "a0000002", BigDecimal.valueOf(100)); + + assertThat(testHelper.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(900)); + assertThat(testHelper.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2100)); + } +} diff --git a/pom.xml b/pom.xml index 7ff1aa7d47..b95a874537 100644 --- a/pom.xml +++ b/pom.xml @@ -593,6 +593,7 @@ spring-reactive-kotlin jnosql spring-boot-angular-ecommerce + jta