![]() |
è¿å身çç¯æ³¡
1 幎å |
Spring Framework ã§ã ãã©ã³ã¶ã¯ã·ã§ã³ ã®å®äºæã«åŠçãè¡ãããšãã§ããTransactionSynchronizationãšãããã®ããããŸãã
ååšã¯ç¥ã£ãŠãããã®ã®ãã¡ãããšäœ¿ã£ãããšããªãã£ãã®ã§ä»å詊ããŠã¿ãããšã«ããŸããã
TransactionSynchronizationã¯ã Spring Framework ã®ããã¥ã¡ã³ãã«ã¯ç»å ŽããŸããã
Transaction Management :: Spring Framework
Javadoc ãèŠãããšã«ãªããŸãã
TransactionSynchronization (Spring Framework 6.0.9 API)
TransactionSynchronizationã䜿ããšã ãã©ã³ã¶ã¯ã·ã§ã³ ã®å®äºæã«ãªãããã®åŠçãè¡ãããšãã§ããŸãã
Interface for transaction synchronization callbacks.
TransactionSynchronization
ã€ã³ã¿ãŒãã§ãŒã¹ãå®è£
ããŠã以äžã®4çš®é¡ã®ã¡ãœããããªãŒããŒã©ã€ãããŸãã
readOnly
ã¯ãèªã¿åãå°çšã®
ãã©ã³ã¶ã¯ã·ã§ã³
ãã©ãã
RuntimeException
ãã¹ããŒãããšãåŒã³åºãå
ã«äŒæããïŒ
TransactionException
ã®ãµãã¯ã©ã¹ãã¹ããŒããŠã¯ãããªãïŒ
RuntimeException
ãã¹ããŒããŠããåŒã³åºãå
ã«äŒæããªãïŒ
TransactionException
ã®ãµãã¯ã©ã¹ãã¹ããŒããŠã¯ãããªãïŒ
RuntimeException
ãã¹ããŒãããšãåŒã³åºãå
ã«äŒæããïŒ
TransactionException
ã®ãµãã¯ã©ã¹ãã¹ããŒããŠã¯ãããªãïŒ
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ãããžã§ã¯ããäœæããŸããäŸåé¢ä¿ã«ã¯ã
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
ãåŒã³åºããŠããåŠçã¯ãã®ãããã§ãããå®éã«åŒã³åºããè¡ãã®ã¯
TransactionSynchronizationUtils
ã§ããããŸãã
TransactionSynchronization#beforeCompletion
ãTransactionSynchronization#afterCompletion
ã§äŸå€ãã¹ããŒãããŠã
åŒã³åºãå ã«äŒæããªãã®ã¯ããµã€ãã«try
ãcatch
ããŠããããã§ããããªãã±ïŒ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
ã詊ããŠã¿ãŸãããååšã¯ç¥ã£ãŠããŸããããã¡ãããšäœ¿ã£ãããšããªãã£ãã®ã§ä»å確èªããŠãããŠããã£ãã§ãã
æã£ãŠããããããåäœã«ããªãšãŒã·ã§ã³ããã£ããªãšããæããããŸããã