盞关文章掚荐

これは、なにをしたくお曞いたもの

Spring Framework で、 トランザクション の完了時に凊理を行うこずができるTransactionSynchronizationずいうものがありたす。

存圚は知っおいたものの、ちゃんず䜿ったこずがなかったので今回詊しおみるこずにしたした。

TransactionSynchronization

TransactionSynchronizationは、 Spring Framework のドキュメントには登堎したせん。

Transaction Management :: Spring Framework

Javadoc を芋るこずになりたす。

TransactionSynchronization (Spring Framework 6.0.9 API)

TransactionSynchronizationを䜿うず、 トランザクション の完了時になんらかの凊理を行うこずができたす。

Interface for transaction synchronization callbacks.

TransactionSynchronization むンタヌフェヌスを実装しお、以䞋の4皮類のメ゜ッドをオヌバヌラむドしたす。

  • beforeCommit(boolean readOnly) 
 トランザクション のコミット前に呌び出される
  • readOnly は、読み取り専甚の トランザクション かどうか
  • RuntimeException をスロヌするず、呌び出し元に䌝播する TransactionException のサブクラスをスロヌしおはいけない
  • beforeCompletion 
 トランザクション のコミット ロヌルバック の前に呌び出される
  • RuntimeException をスロヌしおも、呌び出し元に䌝播しない TransactionException のサブクラスをスロヌしおはいけない
  • afterCommit 
 トランザクション のコミット埌に呌び出される
  • RuntimeException をスロヌするず、呌び出し元に䌝播する TransactionException のサブクラスをスロヌしおはいけない
  • afterCompletion(int status) 
 トランザクション のコミット ロヌルバック の埌に呌び出される
  • status は、定数定矩 STATUS_COMMITTED 、 STATUS_ROLLED_BACK 、 STATUS_UNKNOWN
  • RuntimeException をスロヌしおも、呌び出し元に䌝播しない TransactionException のサブクラスをスロヌしおはいけない
  • TransactionSynchronization は、 TransactionSynchronizationManager#registerSynchronization で登録しお䜿いたす。

    TransactionSynchronizationManager (Spring Framework 6.0.9 API)

    TransactionSynchronization は耇数登録できたす。その順序は、 getOrder メ゜ッドを実装しおいない堎合は远加順になりたす。

    説明はこんなずころにしお、実際に䜿っおみたしょう。

    今回の環境は、こちら。

    $ java --version
    openjdk 17.0.7 2023-04-18
    OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2)
    OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing)
    $ mvn --version
    Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c)
    Maven home: $HOME/.sdkman/candidates/maven/current
    Java version: 17.0.7, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
    Default locale: ja_JP, platform encoding: UTF-8
    OS name: "linux", version: "5.15.0-73-generic", arch: "amd64", family: "unix"

    デヌタベヌスには MySQL を甚意したした。

     MySQL  localhost:3306 ssl  practice  SQL > select version();
    +-----------+
    | version() |
    +-----------+
    | 8.0.33    |
    +-----------+
    1 row in set (0.0370 sec)

    MySQL は172.17.0.2で動䜜しおいるものずし、デヌタベヌスpractice、アカりントはkazuhirapasswordで接続できるものず したす。

    Spring Bootプロゞェクトを䜜成する

    たずはSpring Bootプロゞェクトを䜜成したす。䟝存関係には、 web 、 jdbc 、 mysql を远加。

    $ curl -s https://start.spring.io/starter.tgz \
      -d bootVersion=3.1.0 \
      -d javaVersion=17 \
      -d type=maven-project \
      -d name=transaction-synchronization-example \
      -d groupId=org.littlewings \
      -d artifactId=transaction-synchronization-example \
      -d version=0.0.1-SNAPSHOT \
      -d packageName=org.littlewings.spring.tx \
      -d dependencies=web,jdbc,mysql \
      -d baseDir=transaction-synchronization-example | tar zxvf -

    ディレクト リ内に移動。

    $ cd transaction-synchronization-example

    生成された Maven 䟝存関係など。

            <properties>
                    <java.version>17</java.version>
            </properties>
            <dependencies>
                    <dependency>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-starter-jdbc</artifactId>
                    </dependency>
                    <dependency>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-starter-web</artifactId>
                    </dependency>
                    <dependency>
                            <groupId>com.mysql</groupId>
                            <artifactId>mysql-connector-j</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>
    

    自動生成された゜ヌスコヌドは削陀しおおきたす。

    $ rm src/main/java/org/littlewings/spring/tx/TransactionSynchronizationExampleApplication.java src/test/java/org/littlewings/spring/tx/TransactionSynchronizationExampleApplicationTests.java

    テヌブル定矩。お題は曞籍にしたした。

    src/main/resources/schema.sql

    drop table if exists book;
    create table book(
      isbn varchar(14),
      title varchar(100),
      price int,
      primary key(isbn)
    

    このSQLは、アプリケヌション起動時に毎回実行されるように蚭定。

    src/main/resources/application.properties

    spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
    spring.datasource.username=kazuhira
    spring.datasource.password=password
    spring.sql.init.mode=always
    

    ゚ンティティ盞圓のクラス。

    src/main/java/org/littlewings/spring/tx/Book.java

    package org.littlewings.spring.tx;
    public class Book {
        String isbn;
        String title;
        Integer price;
        public static Book create(String isbn, String title, Integer price) {
            Book book = new Book();
            book.setIsbn(isbn);
            book.setTitle(title);
            book.setPrice(price);
            return book;
        // gettersetterは省略
    

    TransactionSynchronizationを䜿ったServiceクラス。

    src/main/java/org/littlewings/spring/tx/BookService.java

    package org.littlewings.spring.tx;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.transaction.support.TransactionSynchronization;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    import java.util.List;
    import java.util.Map;
    @Transactional
    @Service
    public class BookService {
        NamedParameterJdbcTemplate jdbcTemplate;
        LoggingService loggingService;
        public BookService(NamedParameterJdbcTemplate jdbcTemplate, LoggingService loggingService) {
            this.jdbcTemplate = jdbcTemplate;
            this.loggingService = loggingService;
        public Book findByIsbn(String isbn) {
            return jdbcTemplate.queryForObject("""
                            select
                              isbn, title, price
                            where
                              isbn = :isbn""",
                    Map.of("isbn", isbn),
                    new BeanPropertyRowMapper<>(Book.class));
        public List<Book> findAll() {
            return jdbcTemplate.query("""
                            select
                              isbn, title, price
                            order by
                              price asc""",
                    new BeanPropertyRowMapper<>(Book.class));
        public void insertAfterCommit(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
        public void insertAfterCommitRollback(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
            throw new RuntimeException("Oops!!");
        public void insertAfterCommitThrowException(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    throw new RuntimeException("Oops!!, after commit");
        public void insertAfterCompletion(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCompletion(int status) {
                    loggingService.log("after completion, status = " + status);
        public void insertAfterCompletionRollback(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
    
    
    
    
        
    
                public void afterCompletion(int status) {
                    loggingService.log("after completion, status = " + status);
            throw new RuntimeException("Oops!!");
        public void insertAfterCompletionThrowException(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCompletion(int status) {
                    throw new RuntimeException("Oops!!, after completion, status = " + status);
    

    今回は、クラス自䜓に@Transactionalアノテヌションを付䞎しおいたす。

    @Transactional
    @Service
    public class BookService {
    

    TransactionSynchronizationは、こんな感じでTransactionSynchronizationクラスを継承したクラスを䜜成し、凊理を行いたいタむミングに
    応じたメ゜ッドをオヌバヌラむドしたす。そしお、TransactionSynchronizationManager#registerSynchronizationで登録したす。

        public void insertAfterCommit(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
    

    TransactionSynchronizationの郚分は、たた埌で説明したす。

    メ゜ッド内で呌び出しおいるServiceクラスは、こんな感じのものです。

    src/main/java/org/littlewings/spring/tx/LoggingService.java

    package org.littlewings.spring.tx;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    @Service
    public class LoggingService {
        Logger logger = LoggerFactory.getLogger(LoggingService.class);
        public void log(String message) {
            logger.info(message);
    

    RestController。

    src/main/java/org/littlewings/spring/tx/BookController.java

    package org.littlewings.spring.tx;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.util.UriComponentsBuilder;
    import java.util.List;
    @RestController
    @RequestMapping("books")
    public class BookController {
        BookService bookService;
        public BookController(BookService bookService) {
            this.bookService = bookService;
        @GetMapping(value = "{isbn}", produces = MediaType.APPLICATION_JSON_VALUE)
        public Book findByIsbn(@PathVariable String isbn) {
            return bookService.findByIsbn(isbn);
        @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
        public List<Book> findAll() {
            return bookService.findAll();
        @PostMapping(value = "after-commit", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommit(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommit(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-commit-rollback", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommitRollback(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommitRollback(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-commit-throw", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommitThrowException(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommitThrowException(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletion(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletion(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion-rollback", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletionRollback(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletionRollback(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion-throw", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletionThrowException(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletionThrowException(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
    

    mainメ゜ッドを定矩したクラス。

    src/main/java/org/littlewings/spring/tx/App.java

    package org.littlewings.spring.tx;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    @SpringBootApplication
    public class App {
        public static void main(String... args) {
            SpringApplication.run(App.class, args);
    

    ここたでで、準備は完了です。

    動かしおみる

    では、アプリケヌションをパッケヌゞングしお起動したす。

    $ mvn package
    $ java -jar target/transaction-synchronization-example-0.0.1-SNAPSHOT.jar

    デヌタを登録するのは、以䞋の4぀の゚ンドポむントがありたした。

        @PostMapping(value = "after-commit", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommit(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommit(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-commit-rollback", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommitRollback(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommitRollback(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-commit-throw", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCommitThrowException(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCommitThrowException(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletion(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletion(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion-rollback", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletionRollback(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletionRollback(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
        @PostMapping(value = "after-completion-throw", consumes = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Void> registerAfterCompletionThrowException(@RequestBody Book book, UriComponentsBuilder uriComponentsBuilder) {
            bookService.insertAfterCompletionThrowException(book);
            return ResponseEntity.created(uriComponentsBuilder.path("books/{isbn}").build(book.getIsbn())).build();
    

    これらを、それぞれ察応するTransactionSynchronizationを䜿ったServiceクラスのメ゜ッドず合わせお芋おいきたす。

    たずはトランザクションのコミット埌に動䜜するTransactionSynchronization#afterCommitから。

        public void insertAfterCommit(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
    

    リク゚ストを送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-commit -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400}'
    HTTP/1.1 201
    Location: http://localhost:8080/books/978-4621303252
    Content-Length: 0
    Date: Sun, 04 Jun 2023 13:13:46 GMT
    

    アプリケヌション偎には、ログが出力されたす。これはコミット埌に動䜜したこずになりたす。

    2023-06-04T22:13:46.844+09:00  INFO 22000 --- [nio-8080-exec-1] o.littlewings.spring.tx.LoggingService   : after commit

    デヌタも入りたした。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    次は、TransactionSynchronization#afterCommitを䜿い぀぀、䟋倖をスロヌしおロヌルバックさせおみたしょう。

        public void insertAfterCommitRollback(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
            throw new RuntimeException("Oops!!");
    

    リク゚ストを送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-commit-rollback -d '{"isbn": "978-4297126858", "title": "プロになるJava―仕事で必芁なプログラミングの知識がれロから身に぀く最高の指南曞", "price": 3278}'
    HTTP/1.1 500
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Sun, 04 Jun 2023 13:14:10 GMT
    Connection: close
    {"timestamp":"2023-06-04T13:14:10.905+00:00","status":500,"error":"Internal Server Error","path":"/books/after-commit-rollback"}

    ゚ラヌになりたした。

    スタックトレヌス。

    2023-06-04T22:14:10.893+09:00 ERROR 22000 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: Oops!!] with root cause
    java.lang.RuntimeException: Oops!!
            at org.littlewings.spring.tx.BookService.insertAfterCommitRollback(BookService.java:76) ~[classes!/:0.0.1-SNAPSHOT]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
            at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
            at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
            at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.littlewings.spring.tx.BookService$$SpringCGLIB$$0.insertAfterCommitRollback(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
            at org.littlewings.spring.tx.BookController.registerAfterCommitRollback(BookController.java:38) ~[classes!/:0.0.1-SNAPSHOT]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
            at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
            at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
            at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
    

    ロヌルバックされおいるので、以䞋の箇所に盞圓するログは出力されおいたせん。

            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    loggingService.log("after commit");
    

    デヌタも増えおいたせんね。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    TransactionSynchronization#afterCommitから䟋倖を投げおみたす。

        public void insertAfterCommitThrowException(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    throw new RuntimeException("Oops!!, after commit");
    

    リク゚スト送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-commit-throw -d '{"isbn": "978-4798161488", "title": "MySQL培底入門 第4版 MySQL 8.0察応", "price": 4180}'
    HTTP/1.1 500
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Sun, 04 Jun 2023 13:16:45 GMT
    Connection: close
    {"timestamp":"2023-06-04T13:16:45.006+00:00","status":500,"error":"Internal Server Error","path":"/books/after-commit-throw"}

    ゚ラヌになりたした。

    この時のスタックトレヌス。

    2023-06-04T22:16:45.003+09:00 ERROR 22000 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: Oops!!, after commit] with root cause
    java.lang.RuntimeException: Oops!!, after commit
            at org.littlewings.spring.tx.BookService$3.afterCommit(BookService.java:88) ~[classes!/:0.0.1-SNAPSHOT]
            at org.springframework.transaction.support.TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationUtils.java:135) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerAfterCommit(TransactionSynchronizationUtils.java:123) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerAfterCommit(AbstractPlatformTransactionManager.java:936) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:782) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:660) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:410) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.littlewings.spring.tx.BookService$$SpringCGLIB$$0.insertAfterCommitThrowException(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
            at org.littlewings.spring.tx.BookController.registerAfterCommitThrowException(BookController.java:45) ~[classes!/:0.0.1-SNAPSHOT]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
            at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
            at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
            at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
    

    今回はTransactionSynchronization#afterCommit内から䟋倖をスロヌしたわけですが。

            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    throw new RuntimeException("Oops!!, after commit");
    

    スタックトレヌスを芋るず、このトランザクション境界のメ゜ッドから䟋倖がスロヌされたこずになっおいたすね。

            at org.littlewings.spring.tx.BookService$$SpringCGLIB$$0.insertAfterCommitThrowException(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
            at org.littlewings.spring.tx.BookController.registerAfterCommitThrowException(BookController.java:38) ~[classes!/:0.0.1-SNAPSHOT]

    これが、呌び出し元に䌝播するずいうこずですね。

    䞀方で、この凊理はコミット埌に動䜜しおいるので、䟋倖を投げおもロヌルバックされたせん。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4798161488",
        "title": "MySQL培底入門 第4版 MySQL 8.0察応",
        "price": 4180
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    トランザクションの完了埌に動䜜するTransactionSynchronization#afterCompletion。

        public void insertAfterCompletion(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCompletion(int status) {
                    loggingService.log("after completion, status = " + status);
    

    リク゚ストを送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-completion -d '{"isbn": "978-4297124298", "title": "Spring Framework超入門 〜やさしくわかるWebアプリ開発", "price": 3058}'
    HTTP/1.1 201
    Location: http://localhost:8080/books/978-4297124298
    Content-Length: 0
    Date: Sun, 04 Jun 2023 13:19:16 GMT
    

    こちらも、ログが出力されたした。

    2023-06-04T22:19:16.678+09:00  INFO 22000 --- [nio-8080-exec-7] o.littlewings.spring.tx.LoggingService   : after completion, status = 0

    0ずいうのは、TransactionSynchronization#STATUS_COMMITTEDの倀ですね。

    デヌタも远加されたした。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4297124298",
        "title": "Spring Framework超入門 〜やさしくわかるWebアプリ開発",
        "price": 3058
        "isbn": "978-4798161488",
        "title": "MySQL培底入門 第4版 MySQL 8.0察応",
        "price": 4180
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    TransactionSynchronization#afterCompletionを䜿い぀぀、䟋倖をスロヌしおみたす。

        public void insertAfterCompletionRollback(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCompletion(int status) {
                    loggingService.log("after completion, status = " + status);
            throw new RuntimeException("Oops!!");
    

    リク゚ストを送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-completion-rollback -d '{"isbn": "978-4774189093", "title": "Java本栌入門 モダンスタむルによる基瀎からオブゞェクト指向・実甚ラむブラリたで", "price": 3278}'
    HTTP/1.1 500
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Sun, 04 Jun 2023 13:21:11 GMT
    Connection: close
    {"timestamp":"2023-06-04T13:21:11.856+00:00","status":500,"error":"Internal Server Error","path":"/books/after-completion-rollback"}

    ゚ラヌになりたした。

    この時のログずスタックトレヌス。

    2023-06-04T22:21:11.853+09:00  INFO 22000 --- [io-8080-exec-10] o.littlewings.spring.tx.LoggingService   : after completion, status = 1
    2023-06-04T22:21:11.854+09:00 ERROR 22000 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: Oops!!] with root cause
    java.lang.RuntimeException: Oops!!
            at org.littlewings.spring.tx.BookService.insertAfterCompletionRollback(BookService.java:120) ~[classes!/:0.0.1-SNAPSHOT]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
            at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
            at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
            at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702) ~[spring-aop-6.0.9.jar!/:6.0.9]
            at org.littlewings.spring.tx.BookService$$SpringCGLIB$$0.insertAfterCompletionRollback(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
            at org.littlewings.spring.tx.BookController.registerAfterCompletionRollback(BookController.java:59) ~[classes!/:0.0.1-SNAPSHOT]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
            at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
            at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
            at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
            at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
            at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.9.jar!/:6.0.9]
    

    ポむントは、TransactionSynchronization#afterCompletionはロヌルバック時も呌び出されるので、以䞋のログが出力されおいるこずです。

    2023-06-04T22:21:11.853+09:00  INFO 22000 --- [io-8080-exec-10] o.littlewings.spring.tx.LoggingService   : after completion, status = 1

    0ずいうのは、TransactionSynchronization#STATUS_ROLLED_BACKの倀ですね。

    ロヌルバックされおいるので、デヌタは増えおいたせん。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4297124298",
        "title": "Spring Framework超入門 〜やさしくわかるWebアプリ開発",
        "price": 3058
        "isbn": "978-4798161488",
        "title": "MySQL培底入門 第4版 MySQL 8.0察応",
        "price": 4180
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    最埌は、TransactionSynchronization#afterCompletion内から䟋倖をスロヌしおみたす。

        public void insertAfterCompletionThrowException(Book book) {
            jdbcTemplate.update("""
                            insert into book(isbn, title, price)
                            values(:isbn, :title, :price)""",
                    new BeanPropertySqlParameterSource(book));
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCompletion(int status) {
                    throw new RuntimeException("Oops!!, after completion, status = " + status);
    

    リク゚ストを送信。

    $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books/after-completion-throw -d '{"isbn": "978-4297131425", "title": "実践Redis入門 技術の仕組みから珟堎の掻甚たで", "price": 4180}'
    HTTP/1.1 201
    Location: http://localhost:8080/books/978-4297131425
    Content-Length: 0
    Date: Sun, 04 Jun 2023 13:30:19 GMT
    

    ふ぀うに凊理が終了したした。

    この時、アプリケヌション偎にはスタックトレヌスもなにも出力されおいたせんでした。

    デヌタはしっかり増えおいたす。

    $ curl -s localhost:8080/books | jq
        "isbn": "978-4297124298",
        "title": "Spring Framework超入門 〜やさしくわかるWebアプリ開発",
        "price": 3058
        "isbn": "978-4297131425",
        "title": "実践Redis入門 技術の仕組みから珟堎の掻甚たで",
        "price": 4180
        "isbn": "978-4798161488",
        "title": "MySQL培底入門 第4版 MySQL 8.0察応",
        "price": 4180
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400
    

    これで、TransactionSynchronization#afterCompletion内で䟋倖をスロヌしおも呌び出し元に圱響しない䌝播しないこずが
    確認できたした。

    TestTransactionを䜿っおテストで確認する

    ずころで、TransactionSynchronizationは時には䟿利な機胜ですが、TransactionSynchronizationManagerに登録した
    TransactionSynchronizationが動䜜しおいるこずを確認するにはどうしたらいいのでしょうか

    TestTransactionを䜿うのが良さそうです。

    Transaction Management / Programmatic Transaction Management

    TestTransactionを䜿うず、テストで䜿われおいるトランザクションのコミット、ロヌルバックをメ゜ッド呌び出しで制埡するこずが
    できたす。

    For example, you can use TestTransaction within test methods, before methods, and after methods to start or end the current test-managed transaction or to configure the current test-managed transaction for rollback or commit.

    TestTransaction (Spring Framework 6.0.9 API)

    䜜成したテストは、こんな感じです。

    src/test/java/org/littlewings/spring/tx/BookServiceTest.java

    package org.littlewings.spring.tx;
    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.boot.test.mock.mockito.MockBean;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.context.transaction.TestTransaction;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.transaction.support.TransactionSynchronization;
    import java.util.Collections;
    import java.util.List;
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.assertj.core.api.Assertions.assertThatThrownBy;
    import static org.mockito.Mockito.*;
    @DirtiesContext
    @SpringBootTest
    class BookServiceTest {
        @Autowired
        BookService bookService;
        @MockBean
        LoggingService loggingService;
        @Autowired
        NamedParameterJdbcTemplate jdbcTemplate;
        @BeforeEach
        void setUp() {
            reset(loggingService);
            jdbcTemplate.update("truncate table book", Collections.emptyMap());
        @Transactional
        @Test
        void query() {
            bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400));
            bookService.insertAfterCommit(Book.create("978-4798161488", "MySQL培底入門 第4版 MySQL 8.0察応", 4180));
            Book javaBook = bookService.findByIsbn("978-4621303252");
            assertThat(javaBook.getIsbn()).isEqualTo("978-4621303252");
            assertThat(javaBook.getTitle()).isEqualTo("Effective Java 第3版");
            assertThat(javaBook.getPrice()).isEqualTo(4400);
            List<Book> books = bookService.findAll();
            assertThat(books).hasSize(2);
            assertThat(books.get(0).getTitle()).isEqualTo("MySQL培底入門 第4版 MySQL 8.0察応");
            assertThat(books.get(1).getTitle()).isEqualTo("Effective Java 第3版");
        @Transactional
        @Test
        void insertAfterCommitWithCommit() {
            doNothing().when(loggingService).log("after commit");
            bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400));
            TestTransaction.flagForCommit();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after commit");
        @Transactional
        @Test
        void insertAfterCommitWithRollback() {
            bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400));
            TestTransaction.flagForRollback();
            TestTransaction.end();
            verify(loggingService, never())
                    .log(anyString());
        @Transactional
        @Test
        void insertAfterCommitWithThrowExceptionRollback() {
            assertThatThrownBy(() ->
                    bookService.insertAfterCommitRollback(Book.create("978-4297126858", "プロになるJava―仕事で必芁なプログラミングの知識がれロから身に぀く最高の指南曞", 3278)))
                    .isExactlyInstanceOf(RuntimeException.class)
                    .hasMessage("Oops!!");
            verify(loggingService, never())
                    .log(anyString());
        @Transactional
        @Test
        void insertAfterCommitThrowExceptionWithCommit() {
            bookService.insertAfterCommitThrowException(Book.create("978-4798161488", "MySQL培底入門 第4版 MySQL 8.0察応", 4180));
            TestTransaction.flagForCommit();
            assertThatThrownBy(() -> TestTransaction.end())
                    .isExactlyInstanceOf(RuntimeException.class)
                    .hasMessage("Oops!!, after commit");
            verify(loggingService, never())
                    .log(anyString());
        @Transactional
        @Test
        void insertAfterCommitThrowExceptionWithRollback() {
            bookService.insertAfterCommitThrowException(Book.create("978-4798161488", "MySQL培底入門 第4版 MySQL 8.0察応", 4180));
            TestTransaction.flagForRollback();
            TestTransaction.end();
        @Transactional
        @Test
        void insertAfterCompletionWithCommit() {
            doNothing().when(loggingService).log("after completion, status = 0");
            bookService.insertAfterCompletion(Book.create("978-4297124298", "Spring Framework超入門 〜やさしくわかるWebアプリ開発", 3058));
            TestTransaction.flagForCommit();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after completion, status = 0");
        @Transactional
        @Test
        void insertAfterCompletionWithRollback() {
            doNothing().when(loggingService).log("after completion, status = 1");
            bookService.insertAfterCompletion(Book.create("978-4774189093", "Java本栌入門 モダンスタむルによる基瀎からオブゞェクト指向・実甚ラむブラリたで", 3278));
            TestTransaction.flagForRollback();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after completion, status = 1");
        @Transactional
          @Test
          void insertAfterCompletionThrowExceptionRollback() {
              doNothing().when(loggingService).log("after completion, status = 1");
              assertThatThrownBy(() ->
                      bookService.insertAfterCompletionRollback(Book.create("978-4774189093", "Java本栌入門 モダンスタむルによる基瀎からオブゞェクト指向・実甚ラむブラリたで", 3278)));
              verify(loggingService, times(1))
                      .log("after completion, status = 1");
        @Transactional
        @Test
        void insertAfterCompletionThrowExceptionWithCommit() {
            bookService.insertAfterCompletionThrowException(Book.create("978-4297131425", "実践Redis入門 技術の仕組みから珟堎の掻甚たで", 4180));
            TestTransaction.flagForCommit();
            TestTransaction.end();
        @Test
        void transactionCompletionStatus() {
            assertThat(TransactionSynchronization.STATUS_COMMITTED).isEqualTo(0);
            assertThat(TransactionSynchronization.STATUS_ROLLED_BACK).isEqualTo(1);
            assertThat(TransactionSynchronization.STATUS_UNKNOWN).isEqualTo(2);
    

    少し、ピックアップしお芋おみたしょう。

    TransactionSynchronization#afterCommitでコミットさせる堎合。TestTransaction#flagForCommitを呌び出しおトランザクションの
    コミット甚のフラグを立おお、TestTransaction#endで確定したす。

        @Transactional
        @Test
        void insertAfterCommitWithCommit() {
            doNothing().when(loggingService).log("after commit");
            bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400));
            TestTransaction.flagForCommit();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after commit");
    

    実際に呌び出されおいるかどうかの確認は、モックを䜿いたした。

    なお、「コミット甚のフラグを立おお」ず蚀っおいるように、このテストで実行したトランザクションはコミットされたす。
    @Transactionalを付けおいるからずいっおロヌルバックされなくなるので、その点には泚意が必芁です。

    なお、TestTransactionを䜿うにはトランザクションを開始しおおく必芁があるようです。このテストから@Transactionalを削陀するず
    テストが実行できなくなりたす。

    ロヌルバックさせるには、TestTransaction#flagForRollbackを呌び出しおロヌルバック甚のフラグを立おたす。

        @Transactional
        @Test
        void insertAfterCommitWithRollback() {
            bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400));
            TestTransaction.flagForRollback();
            TestTransaction.end();
            verify(loggingService, never())
                    .log(anyString());
    

    TestTransaction#endで確定させおも、TransactionSynchronization#afterCommitの凊理が呌び出されないこずが確認できたす。

    TransactionSynchronization#afterCommit内で䟋倖をスロヌした堎合は、TestTransaction#endを呌び出した時に䟋倖がスロヌされたす。

        @Transactional
        @Test
        void insertAfterCommitThrowExceptionWithCommit() {
            bookService.insertAfterCommitThrowException(Book.create("978-4798161488", "MySQL培底入門 第4版 MySQL 8.0察応", 4180));
            TestTransaction.flagForCommit();
            assertThatThrownBy(() -> TestTransaction.end())
                    .isExactlyInstanceOf(RuntimeException.class)
                    .hasMessage("Oops!!, after commit");
            verify(loggingService, never())
                    .log(anyString());
    

    TransactionSynchronization#afterCompletionの堎合は、コミットしおもロヌルバックしおも凊理が呌び出されたす。

        @Transactional
        @Test
        void insertAfterCompletionWithCommit() {
            doNothing().when(loggingService).log("after completion, status = 0");
            bookService.insertAfterCompletion(Book.create("978-4297124298", "Spring Framework超入門 〜やさしくわかるWebアプリ開発", 3058));
            TestTransaction.flagForCommit();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after completion, status = 0");
        @Transactional
        @Test
        void insertAfterCompletionWithRollback() {
            doNothing().when(loggingService).log("after completion, status = 1");
            bookService.insertAfterCompletion(Book.create("978-4774189093", "Java本栌入門 モダンスタむルによる基瀎からオブゞェクト指向・実甚ラむブラリたで", 3278));
            TestTransaction.flagForRollback();
            TestTransaction.end();
            verify(loggingService, times(1))
                    .log("after completion, status = 1");
    

    ステヌタスずしお枡っおくる倀が倉わりたすね。

    そしお、TransactionSynchronization#afterCompletion内で䟋倖をスロヌしおも、コミット時に䟋倖は呌び出し元に䌝播したせん。

        @Transactional
        @Test
        void insertAfterCompletionThrowExceptionWithCommit() {
            bookService.insertAfterCompletionThrowException(Book.create("978-4297131425", "実践Redis入門 技術の仕組みから珟堎の掻甚たで", 4180));
            TestTransaction.flagForCommit();
            TestTransaction.end();
    

    やや脱線しおいたすが、TransactionSynchronizationのステヌタス倀の確認も。

        @Test
        void transactionCompletionStatus() {
            assertThat(TransactionSynchronization.STATUS_COMMITTED).isEqualTo(0);
            assertThat(TransactionSynchronization.STATUS_ROLLED_BACK).isEqualTo(1);
            assertThat(TransactionSynchronization.STATUS_UNKNOWN).isEqualTo(2);
    

    実装を芋る

    先皋の動䜜確認時のスタックトレヌスを芋るずわかりたすが、TransactionSynchronizationを呌び出しおいる凊理はこのあたりですね。

    https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L720-L786

    実際に呌び出しを行うのはTransactionSynchronizationUtilsですね。

    https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java

    たた、TransactionSynchronization#beforeCompletionやTransactionSynchronization#afterCompletionで䟋倖がスロヌされおも
    呌び出し元に䌝播しないのは、ふ぀うにtry〜catchしおいるからですね。

    https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java#L105-L114

    https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java#L166-L179

    オマケRestControllerのテストを曞く

    最埌に、RestControllerのテストも曞いおおいたので茉せおおきたす。curlで実行しおいた内容を確認しおいたす。
    ※デヌタが増えおいないこずの確認たではしおいたせんが

    src/test/java/org/littlewings/spring/tx/BookControllerTest.java

    package org.littlewings.spring.tx;
    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.boot.test.web.client.TestRestTemplate;
    import org.springframework.boot.test.web.server.LocalServerPort;
    import org.springframework.core.ParameterizedTypeReference;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import java.net.URI;
    import java.util.Collections;
    import java.util.Map;
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.assertj.core.api.Assertions.entry;
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class BookControllerTest {
        @Autowired
        TestRestTemplate restTemplate;
        @LocalServerPort
        int port;
        @Autowired
        NamedParameterJdbcTemplate jdbcTemplate;
        @BeforeEach
        void setUp() {
            jdbcTemplate.update("truncate table book", Collections.emptyMap());
        @Test
        void insertAfterCommit() {
            ResponseEntity<Void> response =
                    restTemplate.postForEntity(
                            "/books/after-commit",
                            new HttpEntity<>(Book.create("978-4621303252", "Effective Java 第3版", 4400)),
                            Void.class
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
            assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format("http://localhost:%d/books/978-4621303252", port)));
            Book book = restTemplate.getForObject("/books/978-4621303252", Book.class);
            assertThat(book.getIsbn()).isEqualTo("978-4621303252");
            assertThat(book.getTitle()).isEqualTo("Effective Java 第3版");
            assertThat(book.getPrice()).isEqualTo(4400);
        @Test
        void insertAfterCommitRollback() {
            ResponseEntity<Map<String, Object>> response =
                    restTemplate.exchange(
                            "/books/after-commit-rollback",
                            HttpMethod.POST,
                            new HttpEntity<>(Book.create("978-4297126858", "プロになるJava―仕事で必芁なプログラミングの知識がれロから身に぀く最高の指南曞", 3278)),
                            new ParameterizedTypeReference<>() {
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
            assertThat(response.getBody())
                    .containsOnlyKeys("timestamp", "status", "error", "path")
                    .contains(entry("status", 500), entry("error", "Internal Server Error"), entry("path", "/books/after-commit-rollback"));
            Book book = restTemplate.getForObject("/books/978-4297126858", Book.class);
            assertThat(book.getIsbn()).isNull();
            assertThat(book.getTitle()).isNull();
            assertThat(book.getPrice()).isNull();
        @Test
        void insertAfterCommitThrowException() {
            ResponseEntity<Map<String, Object>> response =
                    restTemplate.exchange(
                            "/books/after-commit-throw",
                            HttpMethod.POST,
                            new HttpEntity<>(Book.create("978-4798161488", "MySQL培底入門 第4版 MySQL 8.0察応", 4180)),
                            new ParameterizedTypeReference<>() {
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
            assertThat(response.getBody())
                    .containsOnlyKeys("timestamp", "status", "error", "path")
                    .contains(entry("status", 500), entry("error", "Internal Server Error"), entry("path", "/books/after-commit-throw"));
            Book book = restTemplate.getForObject("/books/978-4798161488", Book.class);
            assertThat(book.getIsbn()).isEqualTo("978-4798161488");
            assertThat(book.getTitle()).isEqualTo("MySQL培底入門 第4版 MySQL 8.0察応");
            assertThat(book.getPrice()).isEqualTo(4180);
        @Test
        void insertAfterCompletion() {
            ResponseEntity<Void> response =
                    restTemplate.postForEntity(
                            "/books/after-completion",
                            new HttpEntity<>(Book.create("978-4297124298", "Spring Framework超入門 〜やさしくわかるWebアプリ開発", 3058)),
                            Void.class
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
            assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format("http://localhost:%d/books/978-4297124298", port)));
            Book book = restTemplate.getForObject("/books/978-4297124298", Book.class);
            assertThat(book.getIsbn()).isEqualTo("978-4297124298");
            assertThat(book.getTitle()).isEqualTo("Spring Framework超入門 〜やさしくわかるWebアプリ開発");
            assertThat(book.getPrice()).isEqualTo(3058);
        @Test
        void insertAfterCompletionRollback() {
            ResponseEntity<Map<String, Object>> response =
                    restTemplate.exchange(
                            "/books/after-completion-rollback",
                            HttpMethod.POST,
                            new HttpEntity<>(Book.create("978-4774189093", "Java本栌入門 モダンスタむルによる基瀎からオブゞェクト指向・実甚ラむブラリたで", 3278)),
                            new ParameterizedTypeReference<>() {
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
            assertThat(response.getBody())
                    .containsOnlyKeys("timestamp", "status", "error", "path")
                    .contains(entry("status", 500), entry("error", "Internal Server Error"), entry("path", "/books/after-commit-throw"));
            Book book = restTemplate.getForObject("/books/978-4774189093", Book.class);
            assertThat(book.getIsbn()).isNull();
            assertThat(book.getTitle()).isNull();
            assertThat(book.getPrice()).isNull();
        @Test
        void insertAfterCompletionThrowException() {
            ResponseEntity<Map<String, Object>> response =
                    restTemplate.exchange(
                            "/books/after-completion-throw",
                            HttpMethod.POST,
                            new HttpEntity<>(Book.create("978-4297131425", "実践Redis入門 技術の仕組みから珟堎の掻甚たで", 3696)),
                            new ParameterizedTypeReference<>() {
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
            assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format("http://localhost:%d/books/978-4297131425", port)));
            Book book = restTemplate.getForObject("/books/978-4297131425", Book.class);
            assertThat(book.getIsbn()).isEqualTo("978-4297131425");
            assertThat(book.getTitle()).isEqualTo("実践Redis入門 技術の仕組みから珟堎の掻甚たで");
            assertThat(book.getPrice()).isEqualTo(3696);
    

    Spring FrameworkのTransactionSynchronizationを詊しおみたした。

    存圚は知っおいたしたが、ちゃんず䜿ったこずがなかったので今回確認しおおいおよかったです。
    思っおいたよりも、動䜜にバリ゚ヌションがあったなずいう感じがしたした。

     
    掚荐文ç«