01-The implementation of transactions in Spring: programmatic transactions and declarative transactions

Abnormal bank account transfer

Requirement: To transfer 10,000 from act-001 account to act-002 account, it is required that one of the balances of the two accounts is successfully reduced and the other is added successfully, that is, the two update statements executed must succeed or fail at the same time

Implementation steps

Step 1: Introduce the dependencies required for the project

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.powernode</groupId>
    <artifactId>spring6-013-tx-bank</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <!--Warehouse-->
    <repositories>
        <!--Repository of spring milestone version-->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <!--Dependencies-->
    <dependencies>
        <!--spring context, association introduces AOP-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--spring jdbc (JdbcTemplate of Spring framework), the association introduces transaction-related dependencies-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--mysql driver-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!--Druid connection pool-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>
       <!--@Resource annotation-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

Step 2: Prepare the t_act table and insert two account records act-001 and act-002 into the table

Step 3: Write the entity class corresponding to the t_act table

@Date
public class Account {<!-- -->
    private String actno;
    private Double balance;
}

Step 4: Write the AccountDao interface in Dao (persistence layer) and its implementation class AccountDaoImpl which is specifically responsible for t_actCRUD operations on tables, without any business logic code

public interface AccountDao {<!-- -->
    // Check the balance based on the account number
    Account selectByActno(String actno);
    //Update account information
    int update(Account act);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {<!-- -->
    @Resource(name = "jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Override
    public Account selectByActno(String actno) {<!-- -->
        // Query account information based on the account number, and encapsulate the query results into the corresponding entity class
        String sql = "select actno, balance from t_act where actno = ?";
        Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
        return account;
    }

    @Override
    public int update(Account act) {<!-- -->
        //Update the balance of the account based on the account number
        String sql = "update t_act set balance = ? where actno = ?";
        int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
        return count;
    }
}

Step 5: Write AccountService in Service (Business Layer) and its implementation class AccountServiceImpl, which is responsible for the business logic processing of accounts. , such as transaction control related code

public interface AccountService {<!-- -->
    //Transfer method
    void transfer(String fromActno, String toActno, double money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {<!-- -->
    @Resource(name = "accountDao")
    private AccountDao accountDao;
    // Because all transfer operations need to be completed in this method, the transaction needs to be controlled
    @Override
    public void transfer(String fromActno, String toActno, double money) {<!-- -->
        // Check whether the account balance is sufficient
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {<!-- -->
            throw new RuntimeException("Account balance insufficient");
        }
        //If the balance is sufficient, start the transfer
        Account toAct = accountDao.selectByActno(toActno);
        // Modify the balance of the two objects in the memory first
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        
        //Update the balance of the database account
        int count = accountDao.update(fromAct);
        count + = accountDao.update(toAct);
        if (count != 2) {<!-- -->
            throw new RuntimeException("Transfer failed, please contact the bank");
        }
    }
}

Step 6: Write spring configuration file

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--Component scanning-->
    <context:component-scan base-package="com.powernode.bank"/>
    <!--Configuring data source-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
    <!--Configure jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

Step 7: Write a test program to simulate the presentation/control layer to handle user needs, and call the corresponding business layer in the background to complete the business

public class BankTest {<!-- -->
    @Test
    public void testTransfer(){<!-- -->
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        try {<!-- -->
            accountService.transfer("act-001", "act-002", 10000);
            System.out.println("Transfer successful");
        } catch (Exception e) {<!-- -->
            e.printStackTrace();
        }
    }
}

Step 8: Simulate transfer exception. If an exception occurs during the operation of updating the balances of two accounts, the balance of the former will be reduced but the balance of the latter will not be added

@Service("accountService")
public class AccountServiceImpl implements AccountService {<!-- -->
    @Resource(name = "accountDao")
    private AccountDao accountDao;

    @Override
    public void transfer(String fromActno, String toActno, double money) {<!-- -->
        // Check whether the account balance is sufficient
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {<!-- -->
            throw new RuntimeException("Account balance insufficient");
        }
        // The balance is sufficient, start the transfer
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountDao.update(fromAct);
        
        // Simulate exception
        String s = null;
        s.toString();

        count + = accountDao.update(toAct);
        if (count != 2) {<!-- -->
            throw new RuntimeException("Transfer failed, please contact the bank");
        }
    }
}

Implementation of Spring transactions

Programmatic Transactions

Programmatic transactions (understanding), handwrite the code to control transactions in the business method

@Override
public void transfer(String fromActno, String toActno, double money) {<!-- -->
    //The first step is to open the transaction

    //The second step executes the core business logic

    // The third step is to submit the transaction if there are no exceptions in the execution of the core business process.
    
    //The fourth step is to roll back the transaction if there is an exception in the execution of the core business process.
}

Declarative transactions

Declarative transaction (commonly used), based on annotation method or XML configuration method to achieve transaction control

The bottom layer of Spring transaction management is based on AOP, so Spring has developed a set of APIs specifically for transactions PlatformTransactionManager (the core interface of the transaction manager) and has two implementation classes

Implementation class Description
DataSourceTransactionManager Support transaction management such as JdbcTemplate, MyBatis, Hibernate
JtaTransactionManager Support distributed transaction management

Step 1: Introduce the tx namespace and its constraint file into the spring configuration file, and associate it with the spring-jdbc dependency Transaction-related dependencies

Step 2: Configure Transaction Manager in the spring configuration file. If you use JdbcTemplate in Spring 6, you must use DataSourceTransactionManager transaction management Server to manage transactions

  • Since the bottom layer of the transaction manager DataSourceTransactionManager is still Connection connection connection object to open and close transactions, so the transaction manager needs to configure the data source

Step 3: Enable the Transaction Annotation Driver in the spring configuration file to tell the Spring framework to use annotations to control transactions

  • The transaction manager is our aspect class, and the method identified by the @Transactional annotation is the connection point (the location where the aspect can be woven)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--Component scanning-->
    <context:component-scan base-package="com.powernode.bank"/>
    <!--Configuring data source-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    <!--Configure jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
     <!--Configuration transaction manager-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--Turn on the transaction annotation driver to tell the Spring framework to use annotations to control transactions-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

Step 4: Add the @Transactional annotation to the business class or business method in the Service layer, indicating that the current class/method has enabled transactions, and batch DML operations in the business method can be guaranteed to be done at the same time Success or Failure

  • Add annotations to the business class: indicating that all methods in the class enable transactions
  • Add annotations to the business method: indicating that only the current method starts the transaction
@Service("accountService")
@Transactional// Start transaction
public class AccountServiceImpl implements AccountService {<!-- -->
    @Resource(name = "accountDao")
    private AccountDao accountDao;
    @Override
    public void transfer(String fromActno, String toActno, double money) {<!-- -->
        // Check whether the account balance is sufficient
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {<!-- -->
            throw new RuntimeException("Account balance insufficient");
        }
        // The balance is sufficient, start the transfer
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountDao.update(fromAct);

        // Simulate exception
        String s = null;
        s.toString();

        count + = accountDao.update(toAct);
        if (count != 2) {<!-- -->
            throw new RuntimeException("Transfer failed, please contact the bank");
        }
    }
}