Spring Boot中的事務管理實戰
一 什麼是事務
在開發企業應用時,對於業務人員的一個操作實際是對資料讀寫的多步操作的結合。由於資料操作在順序執行的過程中,任何一步操作都有可能發生異常,異常會導致後續操作無法完成,此時由於業務邏輯並未正確的完成,之前成功操作資料的並不可靠,需要在這種情況下進行回退。
事務的作用就是為了保證使用者的每一個操作都是可靠的,事務中的每一步操作都必須成功執行,只要有發生異常就回退到事務開始未進行操作的狀態。
事務管理是Spring框架中最為常用的功能之一,我們在使用Spring Boot開發應用時,大部分情況下也都需要使用事務。
二 點睛
在Spring Boot中,當我們使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依賴的時候,框架會自動預設分別注入DataSourceTransactionManager或JpaTransactionManager。所以我們不需要任何額外配置就可以用@Transactional註解進行事務的使用。
1 隔離級別
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
-
DEFAULT:這是預設值,表示使用底層資料庫的預設隔離級別。對大部分資料庫而言,通常這值就是:READ_COMMITTED。
-
READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的資料。該級別不能防止髒讀和不可重複讀,因此很少使用該隔離級別。
-
READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的資料。該級別可以防止髒讀,這也是大多數情況下的推薦值。
-
REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重複執行某個查詢,並且每次返回的記錄都相同。即使在多次查詢之間有新增的資料滿足該查詢,這些新增的記錄也會被忽略。該級別可以防止髒讀和不可重複讀。
-
SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。但是這將嚴重影響程式的效能。通常情況下也不會用到該級別。
指定方法:通過使用isolation屬性設定
@Transactional(isolation = Isolation.DEFAULT)
2 傳播行為
所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
-
REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
-
SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
-
MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。
-
REQUIRES_NEW:建立一個新的事務,如果當前存在事務,則把當前事務掛起。
-
NOT_SUPPORTED:以非事務方式執行,如果當前存在事務,則把當前事務掛起。
-
NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。
-
NESTED:如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於REQUIRED。
指定方法:通過使用propagation屬性設定。
@Transactional(propagation = Propagation.REQUIRED)
三 實戰
1 新建依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
2 實體類
package com.didispace.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
//定義長度,構造異常
@Column(nullable = false, length = 5)
private String name;
@Column(nullable = false)
private Integer age;
public User(){}
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;
}
}
3 定義UserRepository
package com.didispace.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
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);
}
4 定義服務
package com.didispace.service;
import com.didispace.domain.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
public interface UserService {
//隔離級別和傳播性的用法
@Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED)
User login(String name, String password);
}
5 啟動類
package com.didispace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.Isolation;
import static org.springframework.transaction.annotation.Isolation.READ_UNCOMMITTED;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
6 application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=create
7 測試類
package com.didispace;
import com.didispace.domain.User;
import com.didispace.domain.UserRepository;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void test() throws Exception {
// 建立10條記錄
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("HHHHHHHHH", 80)); //構造異常,觀察是否回退
userRepository.save(new User("III", 90));
userRepository.save(new User("JJJ", 100));
// 測試findAll, 查詢所有記錄
Assert.assertEquals(10, userRepository.findAll().size());
// 測試findByName, 查詢姓名為FFF的User
Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());
// 測試findUser, 查詢姓名為FFF的User
Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());
// 測試findByNameAndAge, 查詢姓名為FFF並且年齡為60的User
Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());
// 測試刪除姓名為AAA的User
userRepository.delete(userRepository.findByName("AAA"));
// 測試findAll, 查詢所有記錄, 驗證上面的刪除是否成功
Assert.assertEquals(9, userRepository.findAll().size());
}
}
四 測試結果
2018-11-03 08:45:42.087 INFO 11740 --- [ main] com.didispace.ApplicationTests : Started ApplicationTests in 8.435 seconds (JVM running for 10.463)
2018-11-03 08:45:42.171 INFO 11740 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [[email protected] testClass = ApplicationTests, testInstance = [email protected], testMethod = [email protected], testException = [null], mergedContextConfiguration = [[email protected] testClass = ApplicationTests, locations = '{}', classes = '{class com.didispace.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.boot.test.SpringApplicationContextLoader', parent = [null]]]; transaction manager [[email protected]]; rollback [true]
2018-11-03 08:45:42.251 WARN 11740 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1406, SQLState: 22001
2018-11-03 08:45:42.251 ERROR 11740 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data truncation: Data too long for column 'name' at row 1
2018-11-03 08:45:42.253 WARN 11740 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Warning Code: 1406, SQLState: HY000
2018-11-03 08:45:42.253 WARN 11740 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data too long for column 'name' at row 1
2018-11-03 08:45:42.333 INFO 11740 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test context [[email protected] testClass = ApplicationTests, testInstance = [email protected], testMethod = [email protected], testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement, mergedContextConfiguration = [[email protected] testClass = ApplicationTests, locations = '{}', classes = '{class com.didispace.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.boot.test.SpringApplicationContextLoader', parent = [null]]].
五 參考