Handling simple transactions in Spring Boot

Speaking of transactions, our first impact should be an important concept of database management systems.

Transaction is a concept in a database management system (DBMS) that is used to manage a set of operations on the database. These operations are either all executed successfully or all are rolled back (undone).

A transaction usually consists of a series of database operations, such as insert, update, delete, etc. These operations are treated as a logical unit, and either all of them are executed successfully or none of them are executed. Transactions have the following four properties, often referred to as ACID properties:

  • Atomicity: A transaction is regarded as an indivisible atomic operation, and either all of them are executed successfully or all of them are rolled back. If any operation in the transaction fails, the entire transaction will be rolled back to the initial state, and the database will not be affected by the partial operation.
  • Consistency: The integrity constraints of the database are not violated before and after the transaction is executed. This means that transactions must ensure that the database transitions from one consistent state to another consistent state.
  • Isolation: Multiple transactions executed concurrently should be isolated from each other, and the operations of each transaction should be independent of the operations of other transactions. Isolation ensures that each transaction will not interfere with each other when executed concurrently, avoiding data inconsistency.
  • Durability: Once a transaction is submitted successfully, the modifications made will be permanently saved in the database. Even if the system fails or is restarted, the modified data will not be lost.

The use of transactions can ensure the consistency and reliability of database operations. Especially in the environment of concurrent access to the database, the isolation of transactions can avoid data conflicts and concurrency problems.

So what if we implement simple transaction processing in Spring Boot?

Then follow me to simply implement a transaction through Spring Boot:
Introduce related dependencies:

 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

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

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

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

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

Configure the database in the relevant configuration file of Spring Boot:

spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true & amp;characterEncoding=utf-8 & amp;useSSL=true & amp;serverTimezone=UTC
spring.datasource.primary.username=root
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create
@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;
    }

    public Long getId() {<!-- -->
        return id;
    }

    public void setId(Long id) {<!-- -->
        this.id = id;
    }

    public String getName() {<!-- -->
        return name;
    }

    public void setName(String name) {<!-- -->
        this.name = name;
    }

    public Integer getAge() {<!-- -->
        return age;
    }

    public void setAge(Integer age) {<!-- -->
        this.age = age;
    }

    public User() {<!-- -->
    }
}

Create an interface to host SQL:

public interface UserRepository extends JpaRepository<User, Long> {<!-- -->

    User findByName(String name);

    User findByNameAndAge(String name, Integer age);

    @Query("from User u where u.name=:name")
    User findUser(@Param("name") String name);

}

Finally create a test class:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {<!-- -->

    @Autowired
    private UserRepository userRepository;

    @Test
    //@Transactional //This annotation will not be executed first. Add it when executed again and see the result.
    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));

        //Test findAll, query all records
        Assert.assertEquals(10, userRepository.findAll().size());

        //Test findByName, query the User whose name is FFF
        Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());

        //Test findUser, query the User whose name is FFF
        Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());

        //Test findByNameAndAge, query the User whose name is FFF and whose age is 60
        Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());

        //Test to delete the User named AAA
        userRepository.delete(userRepository.findByName("AAA"));

        //Test findAll, query all records, and verify whether the above deletion is successful.
        Assert.assertEquals(9, userRepository.findAll().size());

    }

}

Because we set the maximum value for User’s age to 50 through the @Max annotation in the entity class, we can trigger an exception when the age attribute of the created User entity exceeds 50, that is, I In the above test class, data whose execution age is greater than 50 years old will be terminated:

HHH000346: Error during managed flush [Validation failed for classes [com.miaow.demo.User] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{<!-- -->interpolatedMessage='The maximum cannot exceed 50', propertyPath=age, rootBeanClass=class com.miaow.demo.User, messageTemplate='{<!-- -->javax.validation.constraints. Max.message}'}
]]

If we check the database table, we will find that only data containing all data under the age of 50 exists, and other data has failed to be added.

At this time, we need to perform transaction processing, because he inserted the ones before the age of 50, but the ones after the age of 50 are gone. This does not satisfy the atomicity of the transaction. Either do it all or do nothing. We can pass @Transactional to implement, then we took a look at the data in the database table, we found that when @Transactional is not added, it will execute part of it, and the other part will not execute, that is It does not satisfy the atomicity of our transaction. After we added @Transactional, when our execution is blocked or terminated abnormally, we will use the @Rollback annotation to allow our related classes to Get a rollback at the end, that is, this thing must be done, it must be done successfully, or it must not be done.

Transaction isolation level

When it comes to transactions, what we need to mention is the isolation level. The isolation level of a transaction is a mechanism used to control and access data in our database management system. It defines the impact of transactions on other transactions when they access the database at the same time. A rule of influence and visibility.

Our common isolation levels:

  • Read Uncommitted: The lowest isolation level, allowing one transaction to read uncommitted data from another transaction. May cause dirty read (Dirty Read) problem.
  • Read Committed: Ensure that a transaction can only read committed data. Avoids dirty read problems, but may cause non-repeatable read (Non-repeatable Read) problems.
  • Repeatable Read: Ensures that a transaction can obtain consistent results when reading the same data multiple times during execution. Avoids non-repeatable read problems, but may cause phantom read problems.
  • Serializable: The highest isolation level, which avoids concurrency problems by forcing transactions to be executed serially. You can avoid the problems of dirty reads, non-repeatable reads and phantom reads, but it will reduce concurrency performance.
package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction isolation levels for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Isolation {<!-- -->

/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
*/
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

/**
* A constant indicating that dirty reads, non-repeatable reads and phantom reads
* can occur. This level allows a row changed by one transaction to be read by
* another transaction before any changes in that row have been committed
* (a "dirty read"). If any of the changes are rolled back, the second
* transaction will have retrieved an invalid row.
* @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
*/
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

/**
* A constant indicating that dirty reads are prevented; non-repeatable reads
* and phantom reads can occur. This level only prohibits a transaction
* from reading a row with uncommitted changes in it.
* @see java.sql.Connection#TRANSACTION_READ_COMMITTED
*/
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

/**
* A constant indicating that dirty reads and non-repeatable reads are
* prevented; phantom reads can occur. This level prohibits a transaction
* from reading a row with uncommitted changes in it, and it also prohibits
* the situation where one transaction reads a row, a second transaction
* alters the row, and the first transaction rereads the row, getting
* different values the second time (a "non-repeatable read").
* @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
*/
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

/**
* A constant indicating that dirty reads, non-repeatable reads and phantom
* reads are prevented. This level includes the prohibitions in
* {@code ISOLATION_REPEATABLE_READ} and further prohibits the situation
* where one transaction reads all rows that satisfy a {@code WHERE}
* condition, a second transaction inserts a row that satisfies that
* {@code WHERE} condition, and the first transaction rereads for the
* same condition, retrieving the additional "phantom" row in the second read.
* @see java.sql.Connection#TRANSACTION_SERIALIZABLE
*/
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);


private final int value;


Isolation(int value) {<!-- -->
this.value = value;
}

public int value() {<!-- -->
return this.value;
}
}

For this enumeration class, let’s take a look at the 5 isolation level values defined in our Spring Boot Transaction:

public enum Isolation {<!-- -->
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT: This is the default value, indicating 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 means that a transaction can read data modified by another transaction but not yet committed. This level does not prevent dirty reads and non-repeatable reads, so this isolation level is rarely used.
  • READ_COMMITTED: This isolation level means that a transaction can only read data that has been submitted by another transaction. This level prevents dirty reads and is the recommended value in most cases.
  • REPEATABLE_READ: This isolation level means that a transaction can repeatedly execute a query multiple times during the entire process, and the records returned each time are the same. Even if there is new data between multiple queries to satisfy the query, these new records will be ignored. This level prevents dirty reads and non-repeatable reads.
  • SERIALIZABLE: All transactions are executed one by one in order, so that there is no possibility of interference between transactions. In other words, this level can prevent dirty reads, non-repeatable reads and phantom reads. But this will seriously affect the performance of the program. Normally this level is not used.

Communication behavior

The so-called transaction propagation behavior value is that if a transaction context exists before starting to execute the current transaction, then there are several options to specify the execution behavior of a transactional method.

package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction propagation behaviors for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Propagation {<!-- -->

/**
* Support a current transaction, create a new one if none exists.
* Analogous to EJB transaction attribute of the same name.
* <p>This is the default setting of a transaction annotation.
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

/**
* Support a current transaction, execute non-transactionally if none exists.
* Analogous to EJB transaction attribute of the same name.
* <p>Note: For transaction managers with transaction synchronization,
* PROPAGATION_SUPPORTS is slightly different from no transaction at all,
* as it defines a transaction scope that synchronization will apply for.
* As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
* will be shared for the entire specified scope. Note that this depends on
* the actual synchronization configuration of the transaction manager.
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
*/
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

/**
* Support a current transaction, throw an exception if none exists.
* Analogous to EJB transaction attribute of the same name.
*/
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

/**
* Create a new transaction, and suspend the current transaction if one exists.
* Analogous to the EJB transaction attribute of the same name.
* <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {@link org.springframework.transaction.jta.JtaTransactionManager},
* which requires the {@code javax.transaction.TransactionManager} to be
* made available to it (which is server-specific in standard Java EE).
* @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

/**
* Execute non-transactionally, suspend the current transaction if one exists.
* Analogous to EJB transaction attribute of the same name.
* <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {@link org.springframework.transaction.jta.JtaTransactionManager},
* which requires the {@code javax.transaction.TransactionManager} to be
* made available to it (which is server-specific in standard Java EE).
* @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
*/
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

/**
* Execute non-transactionally, throw an exception if a transaction exists.
* Analogous to EJB transaction attribute of the same name.
*/
NEVER(TransactionDefinition.PROPAGATION_NEVER),

/**
* Execute within a nested transaction if a current transaction exists,
* behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
* <p>Note: Actual creation of a nested transaction will only work on specific
* transaction managers. Out of the box, this only applies to the JDBC
* DataSourceTransactionManager when working on a JDBC 3.0 driver.
* Some JTA providers might support nested transactions as well.
* @see org.springframework.jdbc.datasource.DataSourceTransactionManager
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);


private final int value;


Propagation(int value) {<!-- -->
this.value = value;
}

public int value() {<!-- -->
return this.value;
}

}

Transaction propagation behavior refers to the way a transaction behaves when operating between multiple transactions. Common transaction propagation behaviors include:

  • REQUIRED: If there is currently a transaction, join the transaction, if there is no transaction, create a new transaction. This is the default propagation behavior.
  • REQUIRES_NEW: Create a new transaction regardless of whether a transaction currently exists. If a transaction currently exists, suspend it.
  • SUPPORTS: If there is currently a transaction, join the transaction, if there is no transaction, execute in a non-transactional manner.
  • NOT_SUPPORTED: Perform operations in a non-transactional manner, suspending a transaction if it currently exists.
  • MANDATORY: If there is currently a transaction, join the transaction, if there is no transaction, throw an exception.
  • NEVER: Perform operations in a non-transactional manner, and throw an exception if a transaction currently exists.
  • NESTED: If a transaction currently exists, execute in a nested transaction. Nested transactions are part of an outer transaction and can be committed or rolled back independently, but if the outer transaction rolls back, the nested transaction will also roll back.

Transaction propagation behavior can be selected based on specific business requirements to ensure transaction consistency and reliability. Different propagation behaviors can provide flexible control and management between multiple transactions.

Specify method: Set by using the propagation attribute, for example:

@Transactional(propagation = Propagation.REQUIRED)