ćććÆććŖć«ććććć¦ęøćććć®ļ¼
Spring Frameworkć使ć£ć¦ćććØććć©ć³ć¶ćÆć·ć§ć³ē®”ēć@Transactional
ć¢ććć¼ć·ć§ć³ć使ć£ć¦å®£čØēć«ęøćć¦ććććØć
å¤ććØęćć¾ćć
@Transactional
ć使ć£ćå “åćä¾å¤ļ¼ććć©ć«ćć§ćÆRuntimeException
ć®ćµććÆć©ć¹ļ¼ćć¹ćć¼ćććęć«ćć¼ć«ćććÆććć
ććØć«ćŖć£ć¦ćć¾ććććć§ć@Transactional
ć¢ććć¼ć·ć§ć³ćä»äøćććć”ć½ććććć¹ććććć¤éäøć§ä¾å¤ć
ęęććå “åć«ć©ćććęåć«ćŖćć®ćć確čŖćć¦ćæćććŖćØęćć¾ćć¦ć
ćć©ć³ć¶ćÆć·ć§ć³ć®ä¼ęć«ć¤ćć¦ćÆćPROPAGATION_REQUIRED
ćäø»ćŖåÆ¾č±”ć«ćć¦ćć¾ćć
Springć®å®£čØēćć©ć³ć¶ćÆć·ć§ć³ćØćć©ć³ć¶ćÆć·ć§ć³ć®ä¼ę
Springć®å®£čØēćć©ć³ć¶ćÆć·ć§ć³ć«é¢ććććć„ć”ć³ććØćć¦ćÆććć”ćć§ććć
Declarative Transaction Management
ć¾ćć@Transactional
ć¢ććć¼ć·ć§ć³ć«ć¤ćć¦ćÆććć”ćć«čØč¼ććć¦ćć¾ćć
ććć§ććć©ć³ć¶ćÆć·ć§ć³ć®ä¼ęć«é¢ććććć„ć”ć³ććčŖćć§ćæć¾ćć
ęåć«ēåć«ęøććććØć®ēćććå®ćÆććć«ęøćć¦ćć¾ćć
Understanding PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
ć使ć£ććć©ć³ć¶ćÆć·ć§ć³ććć¹ćććå
å“ć®ćć©ć³ć¶ćÆć·ć§ć³ć®ćć¼ć«ćććÆć確å®ććå “åć®
ććØćęøćć¦ććć¾ćć
When the propagation setting is PROPAGATION_REQUIRED, a logical transaction scope is created for each method upon which the setting is applied. Each such logical transaction scope can determine rollback-only status individually, with an outer transaction scope being logically independent from the inner transaction scope. In the case of standard PROPAGATION_REQUIRED behavior, all these scopes are mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transactionās chance to actually commit.
However, in the case where an inner transaction scope sets the rollback-only marker, the outer transaction has not decided on the rollback itself, so the rollback (silently triggered by the inner transaction scope) is unexpected. A corresponding UnexpectedRollbackException is thrown at that point. This is expected behavior so that the caller of a transaction can never be misled to assume that a commit was performed when it really was not. So, if an inner transaction (of which the outer caller is not aware) silently marks a transaction as rollback-only, the outer caller still calls commit. The outer caller needs to receive an UnexpectedRollbackException to indicate clearly that a rollback was performed instead.
ć¤ć¾ććå
å“ć®ćć©ć³ć¶ćÆć·ć§ć³ć®ćć¼ć«ćććÆć確å®ćć¦ćććØå¤å“ć®ćć©ć³ć¶ćÆć·ć§ć³ćÆUnexpectedRollbackException
ć
ć¹ćć¼ććććØććććØć«ćŖćććć§ćć
å
å“ć®ćć©ć³ć¶ćÆć·ć§ć³ćē¬ē«ććććå “åćÆćPROPAGATION_REQUIRES_NEW
ć使ćććć§ććć
Understanding PROPAGATION_REQUIRES_NEW
ä»åćÆćććć¾ć§å«ćć¦ē¢ŗčŖćć¦ćæććććŖććØęćć¾ćć
ē°å¢
ä»åć®ē°å¢ćÆććć”ćć§ćć
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-73-generic", arch: "amd64", family: "unix"
ćć¼ćæćć¼ć¹ćÆMySQL 8.0.25ć使ćć172.17.0.2ć§åä½ćć¦ćććć®ćØćć¾ćć
Spring Bootćććøć§ćÆććä½ęćć
ęåć«ćSpring Bootćććøć§ćÆććä½ęćć¾ććSpring Bootć®ćć¼ćøć§ć³ćÆ2.5.0ć§ćä¾åé¢äæć«ćÆjdbc
ćØmysql
ćå ćć¾ćć
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.5.0 \ -d javaVersion=11 \ -d name=transactional-nested-required \ -d groupId=org.littlewings \ -d artifactId=transactional-nested-required \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.jdbc \ -d dependencies=jdbc,mysql \ -d baseDir=transactional-nested-required | tar zxvf - $ cd transactional-nested-required $ find src -name '*.java' | xargs rm
ēęćććJavać½ć¼ć¹ć³ć¼ććÆåé¤ć
Mavenä¾åé¢äæćŖć©ćÆććććŖęćć§ćć
<properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
確čŖćÆćć¹ćć³ć¼ćć§č”ćććØęćć¾ććć@SpringBootApplication
ć¢ććć¼ć·ć§ć³ćä»äøćććÆć©ć¹ćÆä½ęćć¦
ććć¾ćć
src/main/java/org/littlewings/spring/jdbc/App.java
package org.littlewings.spring.jdbc; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { }
application.properties
ćÆććććŖčØå®ć«ćć¦ććć¾ććć
src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8 spring.datasource.username=kazuhira spring.datasource.password=password
ćé”
@Transactional
ć¢ććć¼ć·ć§ć³ćä»äøććć”ć½ćććęć¤ćÆć©ć¹ćä½ęćććµć¤ćć«å¦ēćå®äŗćććććä¾å¤ćć¹ćć¼ćć¦
ćć£ćććććććŖćć£ććā¦ćØććć¤ćććŖćØć¼ć·ć§ć³ćć¤ćć¦ē¢ŗčŖćć¦ćæć¾ćććć
ćć©ć³ć¶ćÆć·ć§ć³ć®ä¼ęćÆPROPAGATION_REQUIRES
ććå§ććPROPAGATION_REQUIRES_NEW
ćē¹ćäŗ¤ćć¦
ććććć«ćć¾ćć
ćć¹ćć³ć¼ćć®éå½¢
ć¾ććÆćć¹ćć³ć¼ćć®éå½¢ćä½ęćć¾ćććććććŖęćć«ćć¾ććć
src/test/java/org/littlewings/spring/jdbc/TransactionalTest.java
package org.littlewings.spring.jdbc; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.UnexpectedRollbackException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest public class TransactionalTest { @Autowired JdbcTemplate jdbcTemplate; @BeforeEach public void createTable() { jdbcTemplate.execute("drop table if exists sample"); jdbcTemplate.execute("create table sample(word varchar(25));"); } // ććć«ććć¹ććęøć }
ćć¹ćććØć«ććć¼ćć«ćDROP ļ¼ CREATEćć¾ćć
ServicećÆć©ć¹
ä»åćÆćServicećÆć©ć¹ć2ć¤ēØęćć¾ććććććć@Transactional
ć¢ććć¼ć·ć§ć³ćä»äøććć”ć½ćććęć”ć
å¤å“ć®ćć©ć³ć¶ćÆć·ć§ć³ćå
å“ć®ćć©ć³ć¶ćÆć·ć§ć³ć蔨ē¾ćć¾ćć
å¤å“ć«č©²å½ćććć®ćÆććć”ććJdbcTemplate
ćØćå
å“ć®ServicećÆć©ć¹ć使ēØćć¾ćć
src/main/java/org/littlewings/spring/jdbc/MyService.java
package org.littlewings.spring.jdbc; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class MyService { Logger logger = LoggerFactory.getLogger(MyService.class); JdbcTemplate jdbcTemplate; NestedService nestedService; public MyService(JdbcTemplate jdbcTemplate, NestedService nestedService) { this.jdbcTemplate = jdbcTemplate; this.nestedService = nestedService; } // ć”ć½ćććęøć }
MyService
ćÆć©ć¹ććå¼ć³åŗćććć®ćÆććć”ććåćććJdbcTemplate
ć使ēØćć¾ćć
src/main/java/org/littlewings/spring/jdbc/NestedService.java
package org.littlewings.spring.jdbc; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class NestedService { Logger logger = LoggerFactory.getLogger(NestedService.class); JdbcTemplate jdbcTemplate; public NestedService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // ć”ć½ćććęøć }
ćććć®ćÆć©ć¹ćØćć¹ćć³ć¼ćć使ć£ć¦ć確čŖćć¦ćć¾ćććć
ćć¹ćć³ć¼ćå“ć§ćÆćäøčØć®MyService
ćÆć©ć¹ć使ēØćć¾ćć
@SpringBootTest public class TransactionalTest { @Autowired JdbcTemplate jdbcTemplate; @BeforeEach public void createTable() { jdbcTemplate.execute("drop table if exists sample"); jdbcTemplate.execute("create table sample(word varchar(25));"); } @Autowired MyService myService;
ććććå
ćÆćMyService
ćNestedService
ććć¹ćć³ć¼ćć®é ć§ććććęøćć¦ē¢ŗčŖćć¦ććć¾ćć
PROPAGATION_REQUIRES
ć§ćÆć確čŖćć¦ććć¾ćććć
ć³ććććć
ć¾ććÆćµć¤ćć«ćć©ć³ć¶ćÆć·ć§ć³ćå®äŗććļ¼ļ¼ć³ćććććļ¼ććæć¼ć³ćęå®ćććwordćē»é²ććinsert
ęćå®č”ćć¦ć
ćć®wordćäŗéć«ćć¦ę¬”ć®ServicećÆć©ć¹ćå¼ć³åŗćć¾ćć
@Service public class MyService { @Transactional public int insertRequired(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequired(word + " " + word); } }
ćÆć©ć¹ć®å®ē¾©ćÆćęē²ćć¦ęøćć¦ććć¾ćć
å¼ć³åŗćå ć
@Service public class NestedService { @Transactional public int insertRequired(String word) { return jdbcTemplate.update("insert into sample(word) values(?)", word); } }
ć©ć”ććę“ę°ä»¶ę°ćčæćć®ć§ćäø”ę¹ć®ę“ę°ććć¾ććć£ćå “åćÆę»ćå¤ć2ć«ćŖćć¾ććć
ćć¹ćć³ć¼ćććć¼ćæćē»é²ććć¦ććć®ć確čŖć§ćć¾ćć
@Test public void transactionalNormally() { assertThat(myService.insertRequired("Hello!!")).isEqualTo(2); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!", "Hello!! Hello!!")); }
ćć¼ć«ćććÆćć
ꬔćÆććć¼ć«ćććÆććććæć¼ć³ć
å¤å“ć®ServicećÆć©ć¹ćÆćµć¤ćć§ćć
@Service public class MyService { @Transactional public int insertRequiredAndNestedThrown(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredAndThrown(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć§ćÆä¾å¤ćć¹ćć¼ćć¾ćć
@Service public class NestedService { @Transactional public int insertRequiredAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
ćć¹ćć³ć¼ćå“ććć¼ć«ćććÆć§ććć
@Test public void transactionalNestedFailed() { assertThatThrownBy(() -> myService.insertRequiredAndNestedThrown("Hello!!")) .isInstanceOf(RuntimeException.class) .hasMessage("Oops!!"); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEmpty(); }
å å“ć®ServicećÆć©ć¹ćä¾å¤ćć¹ćć¼ććå¤å“ć®ServicećÆć©ć¹å ć§ęęćć
ē¶ćć¦ćÆćå å“ć®ServicećÆć©ć¹ć®ć”ć½ćććä¾å¤ćć¹ćć¼ćć¦ćå¤å“ć®ServicećÆć©ć¹ć§ćć®ä¾å¤ćč£č¶³ććććæć¼ć³ć
ć¤ć¾ććå¤å“ć®ServicećÆć©ć¹ćÆćććŖęćć§ćć
@Service public class MyService { @Transactional public int insertRequiredAndNestedThrownAndCatch(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); try { nestedService.insertRequiredAndThrown(word + " " + word); } catch (RuntimeException e) { logger.error("insert failed", e); } return result; } }
å å“ć®ServicećÆć©ć¹ćÆćå ć»ć©ćØåćć§ćć
@Service public class NestedService { @Transactional public int insertRequiredAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
ćć¹ćć³ć¼ććÆå¤å“ć®ServicećÆć©ć¹ććä¾å¤ćć¹ćć¼ćććŖćā¦ćØęććććUnexpectedRollbackException
ćć¹ćć¼ććć
ććØć«ćŖćć¾ćć
ćććŖććØćčØććć¤ć¤ć
Transaction rolled back because it has been marked as rollback-only
@Test public void transactionalNestedFailedAndCatch() { assertThatThrownBy(() -> myService.insertRequiredAndNestedThrownAndCatch("Hello!!")) .isInstanceOf(UnexpectedRollbackException.class) .hasMessage("Transaction rolled back because it has been marked as rollback-only"); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEmpty(); }
ććć„ć”ć³ćéćć§ććć
å å“ć®ServicećÆć©ć¹ć§ä¾å¤ćÆēŗēćććć®ć®ćć”ć½ćććä¾å¤ć§ęććŖć
ćŖć«ćčØć£ć¦ććććØćććØćå
å“ć®ServicećÆć©ć¹ć§ä¾å¤ćÆēŗēćććć®ć®ććć©ć³ć¶ćÆć·ć§ć³ć®å¢ēćØćŖćć”ć½ćććÆ
ä¾å¤ć§č±åŗććŖćććØć
å¤å“ć®ServicećÆć©ć¹ćÆććććŖęćć§ćć
@Service public class MyService { @Transactional public int insertRequiredAndNestedIgnoreFailed(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredAndIgnoreFailed(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ćÆčŖ¤ć£ćSQLęćå®č”ććć¦ä¾å¤ćēŗēćć¾ćććć”ć½ććčŖä½ćÆä¾å¤ć§ćÆęćć¾ććć
@Service public class NestedService { @Transactional public int insertRequiredAndIgnoreFailed(String word) { try { // ę§ęčŖ¤ćć§å®č”ć«å¤±ęććSQL jdbcTemplate.update("insert into sample(word) v(?)", word); } catch (RuntimeException e) { logger.error("sql error", e); } return 0; } }
ćć¹ćć³ć¼ćć§ćÆćå¤å“ć®ServicećÆć©ć¹ć®å¦ēēµęć ććåę ļ¼ļ¼ć³ćććļ¼ććć¦ććććØć確čŖć§ćć¾ćć
@Test public void transactionalNestedIgnoreFailed() { assertThat(myService.insertRequiredAndNestedIgnoreFailed("Hello!!")).isEqualTo(1); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!")); }
äŗę³ć§ćć話ć§ćÆććć¾ććććć©ć³ć¶ćÆć·ć§ć³ć®å¢ēćä¾å¤ć§ęćć¦ćć¾ćććććć
ć¤ć¾ćććć©ć³ć¶ćÆć·ć§ć³ć®ä¼ęćPROPAGATION_REQUIRED
ć®ē©ćæéććØćŖćććå “åćÆćäøéå端ćŖćØććć§
ä¾å¤ćęć¾ććć«ćć©ć³ć¶ćÆć·ć§ć³ć®å¢ēå¤ć§ęęćććććć©ć³ć¶ćÆć·ć§ć³ć®å¢ēćč·ØććŖććć”ć«ęęććććØććØćć
ęćć«ććę¹ćčÆćććć§ććć
ć½ć¼ć¹ć³ć¼ććć確čŖćć
ćć®ćććć®åä½ććć½ć¼ć¹ć³ć¼ćäøć§ć確čŖćć¦ćæć¾ćććć
ćć©ć³ć¶ćÆć·ć§ć³å¢ēćØćŖćć”ć½ćććä¾å¤ć§ęććęē¹ć§ććć¼ć«ćććÆć®ćć¼ćÆćč”ććć¾ćć
ResourceHolderSupport
ć§ć®rollbackOnly
ćtrue
ć«čØå®ćććć®ćććć®ćć¼ćÆć§ććć
ćć®ē¶ę
ć«ćŖććØćå¤å“ć®ćć©ć³ć¶ćÆć·ć§ć³å¢ēćć³ćććććććØćććæć¤ćć³ć°ć§ććć¼ć«ćććÆćč”ćććć«ćć¼ćÆććć¦
ććććØćę¤åŗććć¾ćć
ććć¦ćUnexpectedRollbackException
ćć¹ćć¼ććć¾ćććØć
ćć”ćććć½ć¼ć¹ć³ć¼ćäøć§ć®ē¢ŗčŖć§ććć
ććØćÆćććå°ćććŖćØć¼ć·ć§ć³ć確čŖćć¦ćæć¾ćććć
PROPAGATION_REQUIRESćØPROPAGATION_REQUIRES_NEWć®ēµćæåćć
ä»åŗ¦ćÆćå¤å“ćPROPAGATION_REQUIRES
ćå
å“ćPROPAGATION_REQUIRES_NEW
ć«ćć¦ćæć¾ćć
å¤å“ćØå å“ć§ććć©ć³ć¶ćÆć·ć§ć³ćå„ć«ćŖćć¾ććć
ć³ććććć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional public int insertRequiredAndNestedNew(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNew(word + " " + word); } }
å
å“ć®ServicećÆć©ć¹ććć©ć³ć¶ćÆć·ć§ć³ć®ä¼ęć¬ćć«ććREQUIRES_NEW
ć§ćć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNew(String word) { return jdbcTemplate.update("insert into sample(word) values(?)", word); } }
ćØćÆćććć³ćććććć®ć§ēµęćÆć¾ććµć¤ćć§ćć
@Test public void transactionalNormallyNestedNew() { assertThat(myService.insertRequiredAndNestedNew("Hello!!")).isEqualTo(2); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!", "Hello!! Hello!!")); }
ćć¼ć«ćććÆćć
ē¶ćć¦ććć¼ć«ćććÆććć”ććå¤ćć£ćććØćÆććć¾ććć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional public int insertRequiredAndNestedNewThrown(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNewAndThrown(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
äø”ę¹ć®ć”ć½ćććä¾å¤ć§ęććć®ć§ććć¼ć«ćććÆćć¾ćć
@Test public void transactionalNestedNewFailed() { assertThatThrownBy(() -> myService.insertRequiredAndNestedNewThrown("Hello!!")) .isInstanceOf(RuntimeException.class) .hasMessage("Oops!!"); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEmpty(); }
å å“ć®ServicećÆć©ć¹ćä¾å¤ćć¹ćć¼ććå¤å“ć®ServicećÆć©ć¹å ć§ęęćć
ćć”ććÆćPROPAGATION_REQUIRED
ć®ęćØå¤åćććć¾ćć
å¤å“ć®ServicećÆć©ć¹ćå å“ć®ServicećÆć©ć¹ćć¹ćć¼ććä¾å¤ćęęćć¾ćć
@Service public class MyService { @Transactional public int insertRequiredAndNestedNewThrownAndCatch(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); try { nestedService.insertRequiredNewAndThrown(word + " " + word); } catch (RuntimeException e) { logger.error("insert failed", e); } return result; } }
å å“ć®ServicećÆć©ć¹ćÆćå ć»ć©ćØåćć§ćć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
ćć¹ćć³ć¼ććPROPAGATION_REQUIRED
ććć¹ććć¦ććęćØćÆē°ćŖććUnexpectedRollbackException
ćÆć¹ćć¼ććć
å¤å“ć®ćć©ć³ć¶ćÆć·ć§ć³ćÆć³ćććććć¾ćć
@Test public void transactionalNestedNewFailedAndCatch() { assertThat(myService.insertRequiredAndNestedNewThrownAndCatch("Hello!!")).isEqualTo(1); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!")); // ä¾å¤ć«ćŖććŖć }
å„ć ć®ćć©ć³ć¶ćÆć·ć§ć³ć«ćŖć£ć¦ććć®ć§ććććŖćć¾ćććć
å å“ć®ServicećÆć©ć¹ć§ä¾å¤ćÆēŗēćććć®ć®ćć”ć½ćććä¾å¤ć§ęććŖć
ćć”ćć試ćć¦ćæć¾ćć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional public int insertRequiredAndNestedNewIgnoreFailed(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNewAndIgnoreFailed(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndIgnoreFailed(String word) { try { // ę§ęčŖ¤ćć§å®č”ć«å¤±ęććSQL jdbcTemplate.update("insert into sample(word) v(?)", word); } catch (RuntimeException e) { logger.error("sql error", e); } return 0; } }
ćć¹ćć³ć¼ćććć”ćć«ć¤ćć¦ćÆćććććå„ć®ćć©ć³ć¶ćÆć·ć§ć³ćć³ććććććć ćććØćŖćć¾ćć
@Test public void transactionalNestedNewIgnoreFailed() { assertThat(myService.insertRequiredAndNestedNewIgnoreFailed("Hello!!")).isEqualTo(1); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!")); }
ććććććććŖćć¾ćććććØć
äø”ę¹ćØćPROPAGATION_REQUIRES_NEWć«ćć
ä»åć®ē¢ŗčŖę¹ę³ć§ćććććęå³ćÆćŖćę°ććć¾ćććē¶²ē¾ ēćŖęå³ć§ćÆäøåæā¦ćØććććØć§ć
ć½ć¼ć¹ć³ć¼ććØēµęć ćč¼ćć¾ćć
PROPAGATION_REQUIRES
ćPROPAGATION_REQUIRES_NEW
ć®ēµćæåćććØåćēµęć«ćŖćć¾ććććććć
ē¬ē«ććććØćę示ćć¦ććć ććŖć®ć§ć
ć³ććććć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndNestedNew(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNew(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNew(String word) { return jdbcTemplate.update("insert into sample(word) values(?)", word); } }
ćć¹ćć³ć¼ćć
@Test public void transactionalNormallyNewNestedNew() { assertThat(myService.insertRequiredNewAndNestedNew("Hello!!")).isEqualTo(2); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!", "Hello!! Hello!!")); }
ćć¼ć«ćććÆćć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndNestedNewThrown(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNewAndThrown(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
ćć¹ćć³ć¼ćć
@Test public void transactionalNewNestedNewFailed() { assertThatThrownBy(() -> myService.insertRequiredNewAndNestedNewThrown("Hello!!")) .isInstanceOf(RuntimeException.class) .hasMessage("Oops!!"); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEmpty(); }
å å“ć®ServicećÆć©ć¹ćä¾å¤ćć¹ćć¼ććå¤å“ć®ServicećÆć©ć¹å ć§ęęćć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndNestedNewThrownAndCatch(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); try { nestedService.insertRequiredNewAndThrown(word + " " + word); } catch (RuntimeException e) { logger.error("insert failed", e); } return result; } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndThrown(String word) { jdbcTemplate.update("insert into sample(word) values(?)", word); throw new RuntimeException("Oops!!"); } }
ćć¹ćć³ć¼ćć
@Test public void transactionalNewNestedNewFailedAndCatch() { assertThat(myService.insertRequiredNewAndNestedNewThrownAndCatch("Hello!!")).isEqualTo(1); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!")); // ä¾å¤ć«ćŖććŖć }
å å“ć®ServicećÆć©ć¹ć§ä¾å¤ćÆēŗēćććć®ć®ćć”ć½ćććä¾å¤ć§ęććŖć
å¤å“ć®ServicećÆć©ć¹ć
@Service public class MyService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndNestedNewIgnoreFailed(String word) { int result = jdbcTemplate.update("insert into sample(word) values(?)", word); return result + nestedService.insertRequiredNewAndIgnoreFailed(word + " " + word); } }
å å“ć®ServicećÆć©ć¹ć
@Service public class NestedService { @Transactional(propagation = Propagation.REQUIRES_NEW) public int insertRequiredNewAndIgnoreFailed(String word) { try { // ę§ęčŖ¤ćć§å®č”ć«å¤±ęććSQL jdbcTemplate.update("insert into sample(word) v(?)", word); } catch (RuntimeException e) { logger.error("sql error", e); } return 0; } }
ćć¹ćć³ć¼ćć
@Test public void transactionalNewNestedNewIgnoreFailed() { assertThat(myService.insertRequiredNewAndNestedNewIgnoreFailed("Hello!!")).isEqualTo(1); assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class)) .isEqualTo(List.of("Hello!!")); }
ć¾ćØć
Spring Frameworkć使ć£ćęć®ćREQUIREDćŖä¼ęć¬ćć«ć®ćć©ć³ć¶ćÆć·ć§ć³ććć¹ćććęć«ćä¾å¤ćć©ćę±ććć§
ć©ć®ćććŖęåććć®ćć確čŖćć¦ćæć¾ććć
ććć„ć”ć³ćć«ēććÆęøćć¦ććć®ć§ćććREQUIREDććć¹ććć¦ććęć«äøéå端ćŖå “ęć§ä¾å¤ćęęććććććØ
åä»ćŖććØć«ćŖćććć§ććć
ä¼ęć¬ćć«ććććććä¾å¤ćęć¾ćććććć©ć³ć¶ćÆć·ć§ć³å¢ēćęćć¦ćć¾ććććć®ććććć”ćććØčćć¦
ęøććŖććØćććŖćć§ććć