What is a transaction?

When we develop enterprise applications, one operation of the business person is actually a combination of multiple steps of reading and writing to the database. In the sequential execution of data operations, exceptions may occur at any step, which will lead to failure of subsequent operations. At this time, the business logic is not correctly completed, and the data successfully operated before is not reliable. To ensure the correct execution of the service, there are usually implementation methods:

  1. Record the location of the failure. After the problem is fixed, continue to execute the business logic from the location of the last failure
  2. If the operation fails, all the operations are rolled back to the original state. After the fault is rectified, the original service logic is executed again

Transactions are implementations of approach 2 above. Transaction, generally refers to the things to be done or done, is an operation of the business personnel mentioned above (for example, in the e-commerce system, an operation to create an order contains two basic operations: creating an order and deducting the inventory of goods. If the order is created successfully and the inventory reduction fails, then there will be an oversold problem, so the most basic thing is to include transactions for both operations to ensure that they either succeed or fail.

There are many such scenarios in the actual development process, so today let’s learn how to use transaction management in Spring Boot!

Quick start

In Spring Boot, when we use spring-boot-starter-JDBC or spring-boot-starter-data-jPA dependencies, The framework will automatically default injection DataSourceTransactionManager or JpaTransactionManager respectively. So we don’t need any extra configuration to use the @Transactional annotation for transactions.

We use the previous implementation of “using Spring Data JPA to access MySQL” example as a basic project to learn the use of transactions. In the sample project, we introduced Spring-data-jPA and created the User entity and UserRepository, a data access object for the User. A unit test case using UserRepository for reading and writing data is implemented in the unit test class as follows:

@Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Autowired private UserRepository userRepository; @test public void Test () throws Exception {// Create 10 records userRepository.save(new User("AAA", 10)); userRepository.save(new User("BBB", 20)); userRepository.save(new User("CCC", 30)); userRepository.save(new User("DDD", 40)); userRepository.save(new User("EEE", 50)); userRepository.save(new User("FFF", 60)); userRepository.save(new User("GGG", 70)); userRepository.save(new User("HHH", 80)); userRepository.save(new User("III", 90)); userRepository.save(new User("JJJ", 100)); // Omit some subsequent validation operations}}Copy the code

As you can see, in this unit test case, 10 consecutive User entities are created into the database using the UserRepository object. Let’s create some artificial exceptions to see what happens.

Set the maximum age of the User to 50 by @max (50), so that an exception can be raised when the age attribute of the User entity exceeds 50 at creation time.

@Entity @Data @NoArgsConstructor public class User { @Id @GeneratedValue private Long id; private String name; @Max(50) private Integer age; public User(String name, Integer age) { this.name = name; this.age = age; }}Copy the code

Executing the test case, you can see that the console throws the following exception about the age field error:

The 2020-07-09 11:55:29. ERROR 581-24424 [main] O.H.I.E xceptionMapperStandardImpl: HHH000346: Error during managed flush [Validation failed for classes [com.didispace.chapter310.User] during persist time for groups  [javax.validation.groups.Default, [interpolatedMessage=' interpolatedMessage= 50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'} ]]Copy the code

SQL > alter TABLE User;

As you can see, halfway through the test case, the test case is interrupted by an anomaly, and the first five data inserts are correct and the next five data inserts are not successful. If all 10 data inserts need to succeed or fail, then you can use transactions. It’s very simple, We simply add the @Transactional annotation to the test function.

@test@transactional public void Test () throws Exception {// omits Test content}Copy the code

When the test case is executed again, you can see that the rollback back transaction for test context is printed in the console.

The 2020-07-09 12:48:23. 24889-831 the INFO [main] O.S.T.C.T ransaction. TransactionContext: Began transaction (1) for test context [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@5289 31cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4b85edeb]; The rollback [true] 2020-07-09 12:48:24. 011 INFO 24889 - [the main] O.S.T.C.T ransaction. TransactionContext: Rolled back transaction for test: [DefaultTestContext@f6efaab testClass = Chapter310ApplicationTests, testInstance = com.didispace.chapter310.Chapter310ApplicationTests@60816371, testMethod = test@Chapter310ApplicationTests, testException = javax.validation.ConstraintViolationException: Validation failed for classes [com.didispace.chapter310.User] during persist time for groups [javax.validation.groups.Default, [interpolatedMessage=' interpolatedMessage= 50', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'} ], mergedContextConfiguration = [WebMergedContextConfiguration@3c19aaa5 testClass = Chapter310ApplicationTests, locations = '{}', classes = '{class com.didispace.chapter310.Chapter310Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@34cd072c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@5289 31cf, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2353b3e6, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7ce6a65d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]Copy the code

In the database, there is no AAA to EEE User data in the User table. The automatic rollback is successfully implemented.

This section shows how to use the @transactional annotation to declare that a function needs to be transaction-managed. Normally, to ensure data independence between tests, our unit tests use the @rollback annotation so that each unit test can Rollback at the end. When developing business logic, we usually use @Transactional in service layer interfaces to configure transaction management for individual business logic. For example:

public interface UserService {

    @Transactional
    User update(String name, String password);

}Copy the code

The transaction,

In the example above, we used the default transaction configuration, which can meet some basic transaction requirements, but when our project is larger and more complex (for example, multiple data sources, etc.), we need to specify a different transaction manager when declaring transactions. The transaction management configuration for different Data sources can be found in Spring Data JPA multi-data Source Configuration. When declaring transaction, only through the value attribute specifies the transaction manager configuration can, for example: @ Transactional (value = “transactionManagerPrimary”).

In addition to specifying different transaction managers, you can also control the isolation level and propagation behavior of a transaction, as detailed below:

Isolation level

The isolation level refers to the degree of isolation between several concurrent transactions. The main scenarios relevant to our development time include: dirty reads, repeated reads, and phantom reads.

Could we see the org. Springframework. Transaction. The annotation, the Isolation enumeration class defines five said Isolation level value:

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}Copy the code
  • DEFAULT: This is the default value and indicates that the default isolation level of the underlying database is used. For most databases, this value is usually:READ_COMMITTED.
  • READ_UNCOMMITTED: This isolation level indicates that one transaction can read data modified by another transaction but not yet committed. This level does not protect against dirty and unrepeatable reads, so it is rarely used.
  • READ_COMMITTED: This isolation level indicates that a transaction can only read data already committed by another transaction. This level protects against dirty reads and is recommended in most cases.
  • REPEATABLE_READ: Isolation level Indicates that a transaction can execute a query multiple times throughout the process and return the same records each time. Even if new data is added between multiple queries that satisfy the query, the new records are ignored. This level prevents dirty and unrepeatable reads.
  • SERIALIZABLE: All transactions are executed one by one so that interference between transactions is completely impossible. That is, this level prevents dirty reads, unrepeatable reads, and phantom reads. But this severely affects the performance of the program. This level is also not typically used.

Specify method: set by using the Isolation property, for example:

@Transactional(isolation = Isolation.DEFAULT)Copy the code

Propagation behavior

The propagation behavior of a transaction means that there are several options to specify the execution behavior of a transactional method if a transaction context already exists before the current transaction is started.

Could we see the org. Springframework. Transaction. The annotation. The Propagation enumeration class defines six said transmission behavior of enumerated values:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}Copy the code
  • REQUIRED: If a transaction exists, join the transaction. If there is no transaction currently, a new transaction is created.
  • SUPPORTS: If a transaction exists, join the transaction. If there is no transaction currently, it continues in a non-transactional manner.
  • MANDATORY: If a transaction exists, join the transaction. If there is no transaction currently, an exception is thrown.
  • REQUIRES_NEW: Creates a new transaction and suspends the current transaction if one exists.
  • NOT_SUPPORTED: Runs nontransactionally and suspends the current transaction if one exists.
  • NEVER: Runs nontransactionally and throws an exception if a transaction currently exists.
  • NESTED: Creates a transaction to run as a nested transaction of the current transaction if one exists; If there is no transaction, the value is equivalent toREQUIRED.

To specify the propagation property, for example:

@Transactional(propagation = Propagation.REQUIRED)Copy the code

Code sample

For an example of this article, see the chapter3-10 directory in the repository below:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

If you think this article is good, welcome Star support, your attention is my motivation!

Spring Boot 2.x Basic tutorial: Transaction management introduction. Welcome to pay attention to my official account: Program ape DD, for exclusive learning resources and daily dry goods push. If you are interested in my other topics, direct them to my personal blog: didispace.com.