CLOVERšŸ€

That was when it all began.

Spring Framework恧态REQUIREDćŖä¼ę’­ćƒ¬ćƒ™ćƒ«ć®ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ćŒćƒć‚¹ćƒˆć—ćŸę™‚ć®ä¾‹å¤–ć®ę‰±ć„ć‚’ē¢ŗčŖć™ć‚‹

ć“ć‚ŒćÆć€ćŖć«ć‚’ć—ćŸćć¦ę›øć„ćŸć‚‚ć®ļ¼Ÿ

Spring Frameworkć‚’ä½æć£ć¦ć„ć‚‹ćØć€ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ē®”ē†ć‚’@Transactionalć‚¢ćƒŽćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć‚’ä½æć£ć¦å®£čØ€ēš„ć«ę›øć„ć¦ć„ć‚‹ć“ćØćŒ
å¤šć„ćØę€ć„ć¾ć™ć€‚

@Transactionalć‚’ä½æć£ćŸå “åˆć€ä¾‹å¤–ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆRuntimeExceptionć®ć‚µćƒ–ć‚Æćƒ©ć‚¹ļ¼‰ćŒć‚¹ćƒ­ćƒ¼ć•ć‚ŒćŸę™‚ć«ćƒ­ćƒ¼ćƒ«ćƒćƒƒć‚Æć•ć‚Œć‚‹
ことになっています。ここで、@Transactionalć‚¢ćƒŽćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ćŒä»˜äøŽć•ć‚ŒćŸćƒ”ć‚½ćƒƒćƒ‰ćŒćƒć‚¹ćƒˆć—ć€ć‹ć¤é€”äø­ć§ä¾‹å¤–ć‚’
ę•ę‰ć—ćŸå “åˆć«ć©ć†ć„ć†ęŒ™å‹•ć«ćŖć‚‹ć®ć‹ć€ē¢ŗčŖć—ć¦ćæćŸć„ćŖćØę€ć„ć¾ć—ć¦ć€‚

ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć®ä¼ę’­ć«ć¤ć„ć¦ćÆć€PROPAGATION_REQUIREDを主な対豔にしています。

Springć®å®£čØ€ēš„ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ćØćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć®ä¼ę’­

Springć®å®£čØ€ēš„ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć«é–¢ć™ć‚‹ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆćØć—ć¦ćÆć€ć“ć”ć‚‰ć§ć™ć­ć€‚

Declarative Transaction Management

ć¾ćŸć€@Transactionalć‚¢ćƒŽćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć«ć¤ć„ć¦ćÆć€ć“ć”ć‚‰ć«čØ˜č¼‰ć•ć‚Œć¦ć„ć¾ć™ć€‚

Using @Transactional

ć“ć“ć§ć€ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć®ä¼ę’­ć«é–¢ć™ć‚‹ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć‚’čŖ­ć‚“ć§ćæć¾ć™ć€‚

Transaction Propagation

ęœ€åˆć«ē–‘å•ć«ę›øć„ćŸć“ćØć®ē­”ćˆćŒć€å®ŸćÆć“ć“ć«ę›øć„ć¦ć„ć¾ć™ć€‚

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ć®ē©ćæé‡ć­ćØćŖć‚Šćˆć‚‹å “åˆćÆć€äø­é€”åŠē«ÆćŖćØć“ć‚ć§
ä¾‹å¤–ć‚’ę•ć¾ćˆćšć«ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć®å¢ƒē•Œå¤–ć§ę•ę‰ć™ć‚‹ć‹ć€ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³ć®å¢ƒē•Œć‚’č·ØćŒćŖć„ć†ć”ć«ę•ę‰ć™ć‚‹ć“ćØć€ćØć„ć†
ę„Ÿć˜ć«ć—ćŸę–¹ćŒč‰Æć•ćć†ć§ć™ć­ć€‚

ć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰ć‹ć‚‰ē¢ŗčŖć™ć‚‹

ć“ć®ć‚ćŸć‚Šć®å‹•ä½œć‚’ć€ć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰äøŠć§ć‚‚ē¢ŗčŖć—ć¦ćæć¾ć—ć‚‡ć†ć€‚

ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³å¢ƒē•ŒćØćŖć‚‹ćƒ”ć‚½ćƒƒćƒ‰ć‚’ä¾‹å¤–ć§ęŠœć‘ćŸę™‚ē‚¹ć§ć€ćƒ­ćƒ¼ćƒ«ćƒćƒƒć‚Æć®ćƒžćƒ¼ć‚ÆćŒč”Œć‚ć‚Œć¾ć™ć€‚

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L844

ResourceHolderSupport恧恮rollbackOnlyがtrueć«čØ­å®šć•ć‚Œć‚‹ć®ćŒć€ćć®ćƒžćƒ¼ć‚Æć§ć™ć­ć€‚

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/ResourceHolderSupport.java#L67-L69

ć“ć®ēŠ¶ę…‹ć«ćŖć‚‹ćØć€å¤–å“ć®ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³å¢ƒē•Œć‚’ć‚³ćƒŸćƒƒćƒˆć—ć‚ˆć†ćØć—ćŸć‚æć‚¤ćƒŸćƒ³ć‚°ć§ć€ćƒ­ćƒ¼ćƒ«ćƒćƒƒć‚Æć‚’č”Œć†ć‚ˆć†ć«ćƒžćƒ¼ć‚Æć•ć‚Œć¦
ć„ć‚‹ć“ćØćŒę¤œå‡ŗć•ć‚Œć¾ć™ć€‚

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L703-L709

ćć—ć¦ć€UnexpectedRollbackExceptionćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć¾ć™ć€ćØć€‚

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L869-L872

ć“ć”ć‚‰ćŒć€ć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰äøŠć§ć®ē¢ŗčŖć§ć™ć­ć€‚

ć‚ćØćÆć€ć‚‚ć†å°‘ć—ćƒćƒŖć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć‚’ē¢ŗčŖć—ć¦ćæć¾ć—ć‚‡ć†ć€‚

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ćŒćƒć‚¹ćƒˆć—ć¦ć„ć‚‹ę™‚ć«äø­é€”åŠē«ÆćŖå “ę‰€ć§ä¾‹å¤–ć‚’ę•ę‰ć—ćŸć‚Šć™ć‚‹ćØ
åŽ„ä»‹ćŖć“ćØć«ćŖć‚Šćć†ć§ć™ć­ć€‚

ä¼ę’­ćƒ¬ćƒ™ćƒ«ć‚’ć‚ć‘ć‚‹ć‹ć€ä¾‹å¤–ć‚’ę•ć¾ćˆć‚‹ć‹ć€ćƒˆćƒ©ćƒ³ć‚¶ć‚Æć‚·ćƒ§ćƒ³å¢ƒē•Œć‚’ęŠœć‘ć¦ć—ć¾ć†ć‹ć€ć“ć®ć‚ćŸć‚Šć‚’ć”ć‚ƒć‚“ćØč€ƒćˆć¦
書かないといけないですね。