springboot+mybatis+mysql transaction rollback

1. Introduction

reference resources:

mysql essentially uses database rollback, and the table engine needs InnoDB

The versions used are as follows

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<!-- Packaging configuration -->
	<packaging>jar</packaging>
	<properties>
		<java.version>1.8</java.version>
		<mysql-driver.version>8.0.28</mysql-driver.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- Hot deployment -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>

		<!-- thymeleaf,for html,jsp... -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<!-- json -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.79</version>
		</dependency>

		<!-- mybatis -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.2</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- Database connection pool druid -->
		<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.10</version>
		</dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

		<!--
			redis
			springboot 2.x:Default use lettuce Realized
			springboot 1.x:Default use Jedis Realized

			springboot 2.x change to the use of sth. jedis,The following code
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-starter-data-redis</artifactId>
				<exclusions>
					exclude lettuce package
					<exclusion>
						<groupId>io.lettuce</groupId>
						<artifactId>lettuce-core</artifactId>
					</exclusion>
				</exclusions>
			</dependency>
			add to jedis client
			<dependency>
				<groupId>redis.clients</groupId>
				<artifactId>jedis</artifactId>
			</dependency>
		-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<!-- redis use pool -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
<!--			<version>2.11.1</version>-->
		</dependency>
    </dependencies>

	<build>
		<plugins>
			<!--
				application SpringBoot plug-in unit
				use spring-boot-devtools Application of module,
				When classpath When the file in is changed, it will restart automatically!
			-->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<!-- If the version number is not added, it will be reported in red -->
				<version>2.6.3</version>
			</plugin>

			<!-- mybatis-generator.start -->
			<plugin>
				<groupId>org.mybatis.generator</groupId>
				<artifactId>mybatis-generator-maven-plugin</artifactId>
				<version>1.3.5</version>
				<dependencies>
					<dependency>
						<groupId> mysql</groupId>
						<artifactId>mysql-connector-java</artifactId>
						<version>${mysql-driver.version}</version>
						<scope>runtime</scope>
					</dependency>
					<!--
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.3.5</version>
                    </dependency>
                    -->
				</dependencies>
				<!--
                <executions>
                    <execution>
                        <id>Generate MyBatis Artifacts</id>
                        <phase>package</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                -->
				<configuration>
					<!--Allow moving generated files -->
					<verbose>true</verbose>
					<!-- Overwrite -->
					<!-- Special attention should be paid here,If you do not add this setting, it will be created again in the original directory every time you run-->
					<overwrite>true</overwrite>
					<!-- generator Location of the tool configuration file -->
					<configurationFile>
						src/main/resources/mybatis-generator.xml
					</configurationFile>
				</configuration>
			</plugin>
			<!-- mybatis-generator.end -->
		</plugins>
	</build>

</project>

2. By default, transactions will be rolled back when unchecked exceptions occur

2.1 note that exceptions should be thrown in the @ Transactional annotated class / method and its sub methods

2.1.1Service

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        int count = userMapper.insert(user);
        if( count > 0){
            //An unchecked exception is thrown here, which will cause rollback
            throw new RuntimeException("test uncheck Exception causes rollback");
        }
        return count;
    }

    @Transactional
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //The insertUser method does not define the propagation behavior. It will adopt required (default)
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

2.1.2 controller

package com.demo.controllers;

import com.demo.models.po.User;
import com.demo.services.UserService;
import com.demo.utils.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.HashMap;

@RestController
@RequestMapping("/user")
public class UserController {
    //Automatic injection
    @Autowired
    private UserService service;
    
    /**
     * http://localhost:9000/demo/user/addlist
     * @param user
     * @return
     */
    @RequestMapping(value = "/addlist", method = RequestMethod.POST)
    public AjaxResult insertUsers(User user){
        try{
            int count = service.insertUsers(Arrays.asList(user));
            return AjaxResult.success("Insert successful", new HashMap<String, Object>(){
                {
                    put("count", count);
                    put("user", user);
                }
            });
        }catch(Exception e){
            return AjaxResult.error(e.getMessage());
        }
    }
}

2.1.3 testing

package com.demo.controllers;

import com.alibaba.fastjson.JSON;
import com.demo.models.po.User;
import com.example.demo.DemoApplication;
import org.junit.Assert;
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.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.http.Cookie;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.*;

/**
 * When you want to execute, place the mouse over the corresponding method and right-click to select run.
 */
@RunWith(SpringRunner.class)
//you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
//classes points to the startup class
@SpringBootTest(classes= DemoApplication.class)
public class UserControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mvc;
    private MockHttpSession session;

    /**
     * Simulate login before starting
     */
    @Before
    public void setupMockMvc(){
        //Initialize MockMvc object
        mvc = MockMvcBuilders.webAppContextSetup(wac).build();
        session = new MockHttpSession();
        //The interceptor will determine whether the user logs in, so a user is injected here
        session.setAttribute("user", "root");
    }

    /**
     * Note: the url here does not need to add / demo (the secondary directory of the configuration file server. Servlet. Context path = / Demo)
     * @throws Exception
     */
    @Test
    public void insertUser() throws Exception {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
        params.add("id", "4");
        params.add("name", "test4");
        params.add("birthday", "2020-1-4");
        params.add("address", "Test City 4");

        String uri = "/user/addlist";
        String result = mvc.perform(MockMvcRequestBuilders.post(uri)
                //Impersonate cookie s
                .cookie( new Cookie("token", "123456") )
                //Simulation session
                .session(session)
                //Mainstream browsers such as Google comply with normal specifications and do not need to set character coding, but considering non mainstream browsers, they are still retained
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .params(params)

//                .contentType(MediaType.APPLICATION_JSON)
//                / / pass content as Null (JSON)????
          )
          .andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
          .andDo(MockMvcResultHandlers.print())
          //Returns the requested string information
          .andReturn().getResponse().getContentAsString();


        Assert.assertEquals("Call succeeded","demo8-1",result);
    }

}

Result: rollback

2.2 the exception thrown is not in the class / method of @ Transactional annotation and its sub methods

If @ Transactional is in the Service class and an exception is thrown in the controller (whether unchecked or not), it will not be rolled back

2.2.1 controller

package com.demo.controllers;

import com.demo.models.po.User;
import com.demo.services.UserService;
import com.demo.utils.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.HashMap;

@RestController
@RequestMapping("/user")
public class UserController {
    //Automatic injection
    @Autowired
    private UserService service;
    
    /**
     * http://localhost:9000/demo/user/addlist
     * @param user
     * @return
     */
    @RequestMapping(value = "/addlist", method = RequestMethod.POST)
    public AjaxResult insertUsers(User user){
        try{
            int count = service.insertUsers(Arrays.asList(user));
            if( count>0 ){
                //If an exception is not thrown in the annotation method and its sub methods, it will not cause rollback
                throw new RuntimeException("test uncheck Exceptions do not cause rollback");
            }
            return AjaxResult.success("Insert successful", new HashMap<String, Object>(){
                {
                    put("count", count);
                    put("user", user);
                }
            });
        }catch(Exception e){
            return AjaxResult.error(e.getMessage());
        }
    }
}

2.2.2Service

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        int count = userMapper.insert(user);
        return count;
    }

    @Transactional
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //1. The insertuser method does not define the propagation behavior. It will adopt required (default)
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: no rollback

3. By default, if a checked exception is encountered, it will not be rolled back

3.1Service

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        int count = userMapper.insert(user);
        if( count > 0){
            //check exceptions will not cause rollback
            throw new Exception("test check Exceptions do not cause rollback");
        }
        return count;
    }

    @Transactional
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //The insertUser method does not define the propagation behavior. It will adopt required (default)
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

3.2 controller

package com.demo.controllers;

import com.demo.models.po.User;
import com.demo.services.UserService;
import com.demo.utils.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.HashMap;

@RestController
@RequestMapping("/user")
public class UserController {
    //Automatic injection
    @Autowired
    private UserService service;

    /**
     * http://localhost:9000/demo/user/addlist
     * @param user
     * @return
     */
    @RequestMapping(value = "/addlist", method = RequestMethod.POST)
    public AjaxResult insertUsers(User user){
        try{
            int count = service.insertUsers(Arrays.asList(user));
            return AjaxResult.success("Insert successful", new HashMap<String, Object>(){
                {
                    put("count", count);
                    put("user", user);
                }
            });
        }catch(Exception e){
            return AjaxResult.error(e.getMessage());
        }
    }
}

Result: no rollback

3.3 solution 1: set rollback point

Set the rollback point in @ Transactional's class / method and its sub methods, and roll back to the rollback point after catching the checked exception

Service:

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        //Insert id=5 that will not be rolled back
        User noRollUser = new User();
        noRollUser.setId(5);
        noRollUser.setName("Test 5");
        userMapper.insert(noRollUser);
        //Set rollback point
        Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
        try{
            int count = userMapper.insert(user);
            if( count > 0){
//            throw new RuntimeException("test uncheck exception causes rollback");
                throw new Exception("test check Exceptions do not cause rollback");
            }
            return count;
        }catch(Exception e){
            //After catching the exception, roll back to savePoint
            TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
            throw e;
        }
    }

    @Transactional
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //The insertUser method does not define the propagation behavior. It will adopt required (default)
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: only 4 but no 5 were rolled back

3.4 if it is an unchecked exception, it will also go to the rollback point, not all

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        //Insert id=5 that won't roll
        User noRollUser = new User();
        noRollUser.setId(5);
        noRollUser.setName("Test 5");
        userMapper.insert(noRollUser);
        //Set rollback point
        Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
        try{
            int count = userMapper.insert(user);
            if( count > 0){
                throw new RuntimeException("test uncheck Exception causes rollback");
//                throw new Exception("test check exception will not cause rollback");
            }
            return count;
        }catch(Exception e){
            //After catching the exception, roll back to savePoint
            TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
            throw e;
        }
    }

    @Transactional
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //The insertUser method does not define the propagation behavior. It will adopt required (default)
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: only 4 but no 5 were rolled back

 

3.5 solution 2: @ Transactional's rollback for

Throw an exception directly. The exception cannot be caught here. Otherwise, the rollback for will be invalidated

Service

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
         int count = userMapper.insert(user);
         if( count > 0){
            throw new Exception("test check Exceptions do not cause rollback");
         }
         return count;
    }

    @Transactional(
            //Isolation level:
            //A transaction can only read data that has been committed by another transaction. This level prevents dirty reads
            isolation = Isolation.READ_COMMITTED,
            //Communication behavior:
            // REQUIRED: if a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created.
            propagation = Propagation.REQUIRED,
            //Specify exception class
            rollbackFor = Exception.class  //{RuntimeException.class, Exception.class}
    )
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //1. The insertuser method does not define the propagation behavior. It will adopt REQUIRED,
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: rollback occurs

3.6 solution 3: catch exceptions and roll back manually

3.6.1 note that it should be captured and processed in @ Transactional's class / method and its sub methods. It is invalid outside

3.6.2 when an exception is caught and no longer thrown, the rollback will not be rolled back even if the rollback for is specified

Note that both checked and unchecked exceptions will not be rolled back as long as they are caught and no longer thrown

Service

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        try{
            int count = userMapper.insert(user);
            if( count > 0){
                //throw new RuntimeException("test uncheck exception causes rollback");
                throw new Exception("test check Exceptions do not cause rollback");
            }
            return count;
        }catch(Exception e){
            //To handle rollback, or even unchecked is invalid
            return 0;
        }
    }

    @Transactional(
            //Isolation level:
            //A transaction can only read data that has been committed by another transaction. This level prevents dirty reads
            isolation = Isolation.READ_COMMITTED,
            //Communication behavior:
            // REQUIRED: if a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created.
            propagation = Propagation.REQUIRED,
            //Specify exception class
            rollbackFor = Exception.class
    )
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //1. The insertuser method does not define the propagation behavior. It will adopt REQUIRED,
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: no rollback

 

3.6.3 correct usage: manually roll back after catching exceptions

package com.demo.services;

import com.demo.models.mapper.AppadminMapper;
import com.demo.models.mapper.UserMapper;
import com.demo.models.po.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;

@Service
public class UserService {

    //Automatic injection
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public int insertUser(User user) throws Exception {
        try{
            int count = userMapper.insert(user);
            if( count > 0){
                //throw new RuntimeException("test uncheck exception causes rollback");
                throw new Exception("test check Exceptions do not cause rollback");
            }
            return count;
        }catch(Exception e){
            //Only when used in combination with @ Transactional(rollbackFor = Exception.class) can it be rolled back
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return 0;
        }
    }

    @Transactional(
            //Isolation level:
            //A transaction can only read data that has been committed by another transaction. This level prevents dirty reads
            isolation = Isolation.READ_COMMITTED,
            //Communication behavior:
            // REQUIRED: if a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created.
            propagation = Propagation.REQUIRED,
            //Specify exception class
            rollbackFor = Exception.class //{RuntimeException.class, Exception.class}
    )
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //1. The insertuser method does not define the propagation behavior. It will adopt REQUIRED,
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            count += insertUser(user);
        }
        return count;
    }
}

Result: rollback occurs

4. Transaction attribute setting

@Transactional(
            //Isolation level:
            //A transaction can only read data that has been committed by another transaction. This level prevents dirty reads
            isolation = Isolation.READ_COMMITTED,
            //Communication behavior:
            // REQUIRED: if a transaction currently exists, join the transaction; If there is no current transaction, a new transaction is created.
            propagation = Propagation.REQUIRED
            // SUPPORTS: if a transaction currently exists, join the transaction; If there are currently no transactions, continue to run in a non transactional manner.
//            propagation = Propagation.SUPPORTS
    )
    public int insertUsers(List<User> userList) throws Exception {
        int count = 0;
        for(User user : userList){
            //Calling the sub method insertUser will use the annotation @ Transactional to define the propagation behavior
            //1. The insertuser method does not define the propagation behavior. It will adopt REQUIRED,
            //That is, the current transaction is followed, so it will use the same transaction as the insertUsers method
            //2.SUPPORTS: the insertUser method does not define the propagation behavior, so SUPPORTS is adopted
            count += insertUser(user);
        }
        return count;
    }

 

 

5. Summary

Transaction rollback must be in the @ Transactional class / method and its sub methods. It is invalid outside, so general exception capture can be placed outside

In order to roll back unchecked and checked exceptions at the same time, within the scope of @ Transactional, you can use:

1. Catch exception + set rollback point

2. Catch exception + rollback for + manual rollback setRollbackOnly

 

Keywords: Java MySQL Mybatis Spring Boot

Added by cdickson on Mon, 21 Feb 2022 11:01:31 +0200