Spring Transaction Management and Configuration

  1. Transaction Overview

1.1 What is a Transaction?

A transaction refers to a sequence of operations combined into a single operation.

1.2 Role of Transactions

  1. When individual operations in a database operation sequence fail, it provides a way to restore the database state to a normal state (A), ensuring data consistency even in abnormal states (C) (either the state before the operation or the state after the operation).
  2. When multiple users access the database concurrently, it isolates them to prevent interference between concurrent operations (I).
  • Transaction Characteristics (ACID)
  • Atomicity: A transaction is an indivisible whole, where all operations either execute completely or not at all.
  • Consistency: The integrity of data must remain consistent before and after the transaction.
  • Isolation: When multiple users access the database concurrently, each user's transaction should not be interfered with by other transactions, ensuring isolation between concurrent transactions.
  • Durability: Once a transaction is committed, the changes to the database are permanent, and no impact should occur even if the database fails.

1.3 Transaction Isolation Levels

  • Dirty Read: Reading uncommitted information
  • Cause: Read uncommitted

Solution: Table-level read lock

  • Non-Repeatable Read: Data changes during reading
  • Solution: Repeatable read (row-level write lock)
  • Phantom Read: Changes in data entries during reading
  • Solution: Serializable (table-level write lock)
  1. Transaction Management

2.1 Spring Transaction Core Objects

  • J2EE development uses a layered design approach. For simple business layer calls to data layer operations, it doesn't matter whether the transaction is started in the business layer or data layer. When the business includes multiple data layer calls, the transaction should be started in the business layer, combining and assigning multiple operations in the data layer to the same transaction.
  • Spring provides a complete transaction solution for the business layer
  • PlatformTransactionManager
  • TransactionDefinition
  • TransactionStatus

2.2 PlatformTransactionManager

  • Implementation class of platform transaction manager
  • DataSourceTransactionManager for Spring JDBC or MyBatis
  • HibernateTransactionManager for Hibernate 3.0 and above
  • JpaTransactionManager for JPA
  • JdoTransactionManager for JDO
  • JtaTransactionManager for JTA
  • JPA (Java Persistence API) is one of the Java EE standards, providing persistence standards for POJOs and standardizing the unified API for persistence development. Development compliant with JPA can run on different JPA frameworks.
  • JDO (Java Data Object) is a Java object persistence specification used to store objects in a certain database and provide a standardized API. Compared to JDBC, which only operates on relational databases, JDO can be extended to relational databases, files, XML, object databases (ODBMS), etc., offering better portability.
  • JTA (Java Transaction API) is one of the Java EE standards, allowing applications to perform distributed transaction processing. Compared to JDBC, which is limited to a single database connection, a JTA transaction can have multiple participants, such as JDBC connections and JDO, which can participate in a JTA transaction.

This interface defines bassic transaction operations

  • Get transaction:
TransactionStatus getTransaction(TransactionDefinition definition)

  • Commit transaction:
void commit(TransactionStatus status) 

  • Rollback transaction:
void rollback(TransactionStatus status)

2.3 TransactionDefinition

This interface defines basic information about a transaction

  • Get transaction definition name
String getName()

  • Get read/write property of the transaction
boolean isReadOnly()

  • Get transaction isolation level
int getIsolationLevel()

  • Get transaction timeout
int getTimeout()

  • Get transaction propagation behavior
int getPropagationBehavior()

2.4 TransactionStatus

This interface defines the state information of a transaction at a specific point in time and corresponding state operations

2.5 Transaction Control Methods

  • Programmatic
  • Declarative (XML)
  • Declarative (Annotation)

2.6 Case Study

2.6.1 Case Description

Bank transfer business description

In bank transfer operations, there is a fund transfer from account A to account B. The data layer only provides basic operations for a single data entry and does not design busines operations between multiple accounts.

2.6.2 Case Environment (Based on Spring and MyBatis Integration)

  • Business layer interface provides transfer operation
/**
* Transfer operation
* @param outName     Outgoing account username
* @param inName      Incoming account username
* @param money       Transfer amount
*/
public void transfer(String outName,String inName,Double money);

  • Business layer implementation provides transfer operation
public void transfer(String outName,String inName,Double money){
    accountDao.inMoney(outName,money);
    accountDao.outMoney(inName,money);
}

  • Data layer provides corresponding deposit and withdrawal operations
<update id="inMoney">
    update account set money = money + #{money} where name = #{name}
</update>
<update id="outMoney">
    update account set money = money - #{money} where name = #{name}
</update>

2.6.3 Programmatic Transaction

public void transfer(String outName,String inName,Double money){
    // Create transaction manager
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    // Set the data source used by the data layer for the transaction manager
    dstm.setDataSource(dataSource);
    // Create transaction definition object
    TransactionDefinition td = new DefaultTransactionDefinition();
    // Create transaction status object to control transaction execution
    TransactionStatus ts = dstm.getTransaction(td);
    accountDao.inMoney(outName,money);
    int i = 1/0;    // Simulate an error during business layer transaction
    accountDao.outMoney(inName,money);
    // Commit transaction
    dstm.commit(ts);
}

2.7 Using AOP to Control Transactions

Extract the transaction handling functionality of the business layer and make it into an AOP notification, using around notification to dynamically weave it during runtime

public Object tx(ProceedingJoinPoint pjp) throws Throwable {
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    dstm.setDataSource(dataSource);
    TransactionDefinition td = new DefaultTransactionDefinition();
    TransactionStatus ts = dstm.getTransaction(td);
    Object ret = pjp.proceed(pjp.getArgs());
    dstm.commit(ts);
    return ret;
}

Configure the AOP notification class and inject dataSource

<bean id="txAdvice" class="com.itheima.aop.TxAdvice">
    <property name="dataSource" ref="dataSource"/>
</bean>

Use around notification to weave the notification class into the original business object execution process

<aop:config>
    <aop:pointcut id="pt" expression="execution(* *..transfer(..))"/>
    <aop:aspect ref="txAdvice">
        <aop:around method="tx" pointcut-ref="pt"/>
    </aop:aspect>
</aop:config>

2.8 Declarative Transaction (XML)

AOP configuration for transaction special cases?

public Object tx(ProceedingJoinPoint pjp) throws Throwable {
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    dstm.setDataSource(dataSource);
    TransactionDefinition td = new DefaultTransactionDefinition();
    TransactionStatus ts = dstm.getTransaction(td);
    Object ret = pjp.proceed(pjp.getArgs());
    dstm.commit(ts);
    return ret;
}

<bean id="txAdvice" class="com.itheima.aop.TxAdvice">
    <property name="dataSource" ref="dataSource"/>
</bean>

Use tx namespace configuration for transaction-specific notification class

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="*" read-only="false" />
        <tx:method name="get*" read-only="true" />
        <tx:method name="find*" read-only="true" />
    </tx:attributes>
</tx:advice>

Use aop:advisor in AOP configuration to reference the transaction-specific notification class

<aop:config>
    <aop:pointcut id="pt" expression="execution(* *..*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>

2.8.1 aop:advice vs aop:advisor

  • aop:advice configuration notification class can be a regular Java object, without implementing interfaces or inheritance relationships
  • aop:advisor configuration notification class must implement notification interfaces
  • MethodBeforeAdvice
  • AfterReturningAdvice
  • ThrowsAdvice
  • ...

2.8.2 tx configuration -- tx:advice

  • Name: tx:advice
  • Type: Tag
  • Belongs to: beans tag
  • Purpose: specifically for declaring transaction notifications
  • Format:
<beans>
    <tx:advice id="txAdvice" transaction-manager="txManager">
    </tx:advice>
</beans>

  • Basic attributes:
  • id: used to specify the advisor ID when configuring AOP
  • transaction-manager: specifies the transaction manager bean

2.8.3 tx configuration -- tx:attributes

  • Name: tx:attributes
  • Type: Tag
  • Belongs to: tx:advice tag
  • Purpose: define notification properties
  • Format:
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    </tx:attributes>
</tx:advice>

  • Basic attributes:
  • None

2.8.4 tx configuration -- tx:method

  • Name: tx:method
  • Type: Tag
  • Belongs to: tx:attribute tag
  • Purpose: set specific transaction properties
  • Format:
<tx:attributes>
    <tx:method name="*" read-only="false" />
    <tx:method name="get*" read-only="true" />
</tx:attributes>

  • Explanation:

Usually, transaction properties are configured with multiple ones, including one read-write full transaction property and one read-only query transaction property

tx:method Attributes

2.9 Transaction Propagation Behavior

  • Transaction Manager
  • Transaction Coordinator
  • Transaction propagation behavior describes how the transaction coordinator handles the transaction carried by the transaction manager

2.10 Transaction Propagation Behavior

2.11 Transaction Propagation Application

  • Scenario A: Order Generation Business
  • Sub-business S1: Record logs to database table X
  • Sub-business S2: Save order data to database table Y
  • Sub-business S3: ...
  • If S2 or S3 or ... transaction submission fails, should S1 roll back? How to control?
  • (S1 needs a new transaction)
  • Scenario B: Order Generation Business
  • Background 1: Order number generation depends on a dedicated table M in the database for controlling order number generation
  • Background 2: Each time an order number is obtained, the record in table M increments by 1
  • Sub-business S1: Obtain order number from table M
  • Sub-business S2: Save order data, order number comes from table M
  • Sub-business S3: ...
  • If S2 or S3 or ... transaction submission fails, should S1 roll back? How to control?
  • (S1 needs a new transaction)

2.12 Declarative Transaction (Annotation)

2.12.1 @Transactional

  • Name: @Transactional
  • Type: Method annotation, class annotation, interface annotation
  • Location: above method definition, class definition, interface definition
  • Purpose: set current class/interface methods or specific methods to open transactions and specify related transaction properties
  • Example:
@Transactional(
    readOnly = false,
    timeout = -1,
    isolation = Isolation.DEFAULT,
    rollbackFor = {ArithmeticException.class, IOException.class},
    noRollbackFor = {},
    propagation = Propagation.REQUIRES_NEW
)

2.12.2 tx:annotation-driven

  • Name: tx:annotation-driven
  • Type: Tag
  • Belongs to: beans tag
  • Purpose: enable transaction annotation driver and specify the corresponding transaction manager
  • Example:
<tx:annotation-driven transaction-manager="txManager"/>

2.13 Declarative Transaction (Pure Annotation Drive)

  • Name: @EnableTransactionManagement
  • Type: Class annotation
  • Location: above Spring annotation configuration class
  • Purpose: enable annotation drive, equivalent to the annotation drive in XML format
  • Example:
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JDBCConfig.class,MyBatisConfig.class,TransactionManagerConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}

public class TransactionManagerConfig {
    @Bean
    public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

  1. Template Objects

3.1 Spring Module Objects

  • TransactionTemplate
  • JdbcTemplate
  • RedisTemplate
  • RabbitTemplate
  • JmsTemplate
  • HibernateTemplate
  • RestTemplate

3.2 JdbcTemplate (Understand)

Provides standard SQL statement operation APIs

public void save(Account account) {
    String sql = "insert into account(name,money)values(?,?)";
    jdbcTemplate.update(sql,account.getName(),account.getMoney());
}

3.3 NamedParameterJdbcTemplate (Understand)

Provides standard named SQL statement operation APIs

public void save(Account account) {
    String sql = "insert into account(name,money)values(:name,:money)";
    Map pm = new HashMap();
    pm.put("name",account.getName());
    pm.put("money",account.getMoney());
    jdbcTemplate.update(sql,pm);
}

3.4 RedisTemplate

RedisTemplate object structure

public void changeMoney(Integer id, Double money) {
    redisTemplate.opsForValue().set("account:id:"+id,money);
}
public Double findMondyById(Integer id) {
    Object money = redisTemplate.opsForValue().get("account:id:" + id);
    return new Double(money.toString());
}

  1. Transaction Underlying Principle Analysis

4.1 Strategy Pattern Application

The strategy pattern uses different strategy objects to implement different behaviors, and the change of strategy objects leads to changes in behavior.

The strategy pattern uses different strategy objects to implement different behaviors, and the change of strategy objects leads to changes in behavior.

November 2022 added Bilibili San Geng Cao Tang Spring-04 notes as follows:

Spring-04

  1. Spring integrated with JUnit

① Import dependencies

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<!-- Spring integration with JUnit dependency -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>


② Write test class

Add the following annotations to the test class:

@RunWith(SpringJUnit4ClassRunner.class) annotation, specifying that the test runs in the Spring environment

@ContextConfiguration annotation, specifying the configuration file or configuration class needed to create the Spring container

@RunWith(SpringJUnit4ClassRunner.class)//Let the test run in the Spring test environment
@ContextConfiguration(locations = "classpath:configuration file 1.xml")//Set the Spring configuration file or configuration class
//@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {}


This avoids creating a spring container to getbean and then running the test code

③ Inject objects for testing

Inject the objects to be tested in the test class, define test methods, and use the objects to be tested in them.

@RunWith(SpringJUnit4ClassRunner.class)//Let the test run in the Spring test environment
@ContextConfiguration(locations = "classpath:configuration file 1.xml")//Set the Spring configuration file or configuration class
//@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {

    // Inject the object you want to test
    @Autowired
    private UserService userService;

    // Define test method
    @Test
    public void testUserService() {
        userService.findById(10);
    }

}


  1. Spring integrated with Mybatis

We need to use an integration package mybatis-spring to integrate Mybatis into Spring

Official documentation: http://mybatis.org/spring/zh/index.html

① Import dependencies

    <!-- spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>

    <!-- mybatis integration with Spring integration package -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.4</version>
    </dependency>

    <!-- mybatis dependency -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.4</version>
    </dependency>
    <!-- mysql driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

    <!-- druid data source -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.16</version>
    </dependency>



② Inject integration-related objects into the container

    <!-- Read properties file -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!-- Create connection pool and inject into container -->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
    </bean>   
<!-- Spring integrates with Mybatis and controls the creation and acquisition of SqlSessionFactory objects -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sessionFactory">
        <!-- Configure connection pool -->
        <property name="dataSource" ref="dataSource"></property>
        <!-- Configure the path of MyBatis configuration file -->
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    </bean>

    <!-- Mapper scan configuration, mapper objects scanned will be injected into the Spring container -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
        <property name="basePackage" value="com.sangeng.dao"></property>
    </bean>


MyBatis configuration file mybatis-config.xml is as follows:

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    <typeAliases>
        <package name="com.sangeng.domain"></package>
    </typeAliases>
</configuration>


③ Get Mapper objects from the container for use

    @Autowired
    private UserDao userDao;


  1. Spring Declarative Transactions

3.1 Transaction Review

3.1.1 Concept of Transaction

Guarantee a group of database operations, either all succeed or all fail

3.1.2 Four Characteristics

  • Isolation

Multiple transactions should be isolated from each other, not interfering with each other

  • Atomicity

Refers to a transaction as an indivisible whole, similar to an indivisible atom

  • Consistency

Ensure the state of this group of data is consistent before and after the transaction. Either all are successful or all are failed.

  • Durability

Once a transaction is committed, the modified data in this group of operations is really changed. Even if the database fails afterwards, it should not have any impact.

3.2 Implement Declarative Transactions

If we control transactions ourselves, we need to add transaction control-related code based on the core code. In actual development, this kind of transaction control operation is also very common. Therefore, Spring provides a declarative transaction method for us to control transactions.

Just simply add an annotation (or XML configuration) to achieve transaction control, without transaction control, just remove the corresponding annotation. (This avoids writing the previous JDBC original connection.setAutoCommit(false); con.rollback(); etc.)

There are two ways:

  • 3.2.1 Implement with annotation
  • 3.2.2 Implement with XML

3.2.0 Case Environment Preparation

① Data Initialization

CREATE DATABASE /*!32312 IF NOT EXISTS*/`spring_db` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `spring_db`;
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) DEFAULT NULL,
  `money` DOUBLE DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT  INTO `account`(`id`,`name`,`money`) VALUES (1,'San Geng',100),(2,'Cao Tang',100);

② Spring integrated with MyBatis

③ Create Service and Dao

public interface AccountService {
    /**
     * Transfer
     * @param outId Transfer out account ID
     * @param inId Transfer in account ID
     * @param money Transfer amount
     */
    public void transfer(Integer outId,Integer inId,Double money);
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccoutDao accoutDao;

    public void transfer(Integer outId, Integer inId, Double money) {
        // Add
        accoutDao.updateMoney(inId,money);
        // Subtract
        accoutDao.updateMoney(outId,-money);
    }
}

public interface AccoutDao {

    void updateMoney(@Param("id") Integer id,@Param("updateMoney") Double updateMoney);
}

AccoutDao.xml is as follows:

<?xml version="1.0" encoding="UTF-8" ?>

<mapper namespace="com.sangeng.dao.AccoutDao">


    <update id="updateMoney">
        update  account set money = money + #{updateMoney} where id = #{id}
    </update>
</mapper>


3.2.1 Implement with Annotation

① Configure Transaction Manager and Transaction Annotation Driver

In the Spring configuration file, add the following configuration:

    <!-- Inject the transaction manager into the Spring container, need to configure a connection pool -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- Enable transaction annotation driver, configure the transaction manager used -->
    <tx:annotation-driven transaction-manager="txManager"/>


② Add Annotation

Add the @Transactional annotation on the method or class that needs transaction control to achieve transaction control.

    @Transactional
    public void transfer(Integer outId, Integer inId, Double money) {
        // Add
        accoutDao.updateMoney(inId,money);
//        System.out.println(1/0);
        // Subtract
        accoutDao.updateMoney(outId,-money);
    }


Note: If added to the class, all methods of this class will be under transaction control, and if added to the method, only that method will be under transaction control.

Note that since declarative transactions are implemented through AOP, it is best to add the AOP-related dependencies.

       <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>


3.2.2 XML Implementation

① Configure Transaction Manager
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>


② Configure Transaction Aspect
  
    <!-- Define the transaction management notification class -->
    <tx:advice transaction-manager="txManager" id="txAdvice">
        <tx:attributes>
            <tx:method name="trans*"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pt" expression="execution(* com.sangeng.service..*.*(..))"></aop:pointcut>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
    </aop:config>


Note that since declarative transactions are implemented through AOP, it is best to add the AOP-related dependencies.

       <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>


3.3 Attribute Configuration

3.3.1 Transaction Propagation Behavior Propagation

When transaction method are nested, it is necessary to control whether a new transaction is opened, and the transaction propagation behavior can be used to control it.

Test case:

@Service
public class TestServiceImpl {
    @Autowired
    AccountService accountService;

    @Transactional // Note: outer method has transaction annotation
    public void test(){
        accountService.transfer(1,2,10D);
        accountService.log();
    }
}

public class AccountServiceImpl implements AccountService {
    //... omitted irrelevant code
    @Transactional // Note: inner method also has transaction annotation
    public void log() {
        System.out.println("Printing log");
        int i = 1/0;
    }

}

In cases where there is a nested transaction, like the above example, the following rules apply:

Attribute Value Behavior
**REQUIRED (Must have)** The inner method joins if the outer method has a transaction (essentially using the same JDBC connection). If the outer method does not have a transaction, the inner method creates a new one.
**REQUIRES_NEW (Must have a new transaction)** **Outer** method has a transaction, the inner method creates a new one (new JDBC connection, outer rollback does not affect the inner method). If the outer method does not have a transaction, the inner method also creates a new one.
SUPPORTS (Supports having) If the outer method has a transaction, the inner method joins it. If the outer method does not have a transaction, the inner method also does not.
NOT_SUPPORTED (Supports not having) If the outer method has a transaction, the inner method does not. If the outer method does not have a transaction, the inner method also does not.
MANDATORY (Must have outer transaction) If the outer method has a transaction, the inner method joins it. If the outer method does not have a transaction, the inner method throws an error.
NEVER (No transaction allowed) If the outer method has a transaction, the inner method throws an error. If the outer method does not have a transaction, the inner method also does not.

For example:

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void transfer(Integer outId, Integer inId, Double money) {
        // Add
        accoutDao.updateMoney(inId,money);
        // Subtract
        accoutDao.updateMoney(outId,-money);
    }


3.3.2 Isolation Level Isolation

Isolation.DEFAULT indicates the default isolation level of the database.

Isolation.READ_UNCOMMITTED

Isolation.READ_COMMITTED

Isolation.REPEATABLE_READ

Isolation.SERIALIZABLE

   @Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
    public void transfer(Integer outId, Integer inId, Double money) {
        // Add
        accoutDao.updateMoney(inId,money);
        // Subtract
        accoutDao.updateMoney(outId,-money);
    }


3.3.3 ReadOnly

If the operations (method code) in the transaction are all read operations and do not involve writing data, set readOnly to true. This can improve efficiency.

    @Transactional(readOnly = true)
    public void log() {
        System.out.println("Printing log");
        int i = 1/0;
    }


Tags: Spring transactions java programming database

Posted on Sat, 19 Sep 2026 16:25:04 +0000 by wee_eric