CINXE.COM
Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀
<!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="Kazuhira" data-avail-langs="ja en" data-blog="kazuhira-r.hatenablog.com" data-blog-host="kazuhira-r.hatenablog.com" data-blog-is-public="1" data-blog-name="CLOVER🍀" data-blog-owner="Kazuhira" data-blog-show-ads="1" data-blog-show-sleeping-ads="" data-blog-uri="https://kazuhira-r.hatenablog.com/" data-blog-uuid="10257846132617921657" data-blogs-uri-base="https://kazuhira-r.hatenablog.com" data-brand="hatenablog" data-data-layer="{"hatenablog":{"admin":{},"analytics":{"brand_property_id":"","measurement_id":"G-VZ4P54MK8T","non_sampling_property_id":"","property_id":"","separated_property_id":"UA-29716941-26"},"blog":{"blog_id":"10257846132617921657","content_seems_japanese":"true","disable_ads":"","enable_ads":"true","enable_keyword_link":"true","entry_show_footer_related_entries":"true","force_pc_view":"false","is_public":"true","is_responsive_view":"false","is_sleeping":"false","lang":"ja","name":"CLOVER\ud83c\udf40","owner_name":"Kazuhira","uri":"https://kazuhira-r.hatenablog.com/"},"brand":"hatenablog","page_id":"entry","permalink_entry":{"author_name":"Kazuhira","categories":"Spring","character_count":51568,"date":"2023-06-04","entry_id":"820878482937449108","first_category":"Spring","hour":"23","title":"Spring Framework\u306eTransactionSynchronization\u3092\u8a66\u3059","uri":"https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200"},"pro":"free","router_type":"blogs"}}" data-device="pc" data-dont-recommend-pro="false" data-global-domain="https://hatena.blog" data-globalheader-color="b" data-globalheader-type="pc" data-has-touch-view="1" data-help-url="https://help.hatenablog.com" data-page="entry" data-parts-domain="https://hatenablog-parts.com" data-plus-available="" data-pro="false" data-router-type="blogs" data-sentry-dsn="https://03a33e4781a24cf2885099fed222b56d@sentry.io/1195218" data-sentry-environment="production" data-sentry-sample-rate="0.1" data-static-domain="https://cdn.blog.st-hatena.com" data-version="f55a19e7a4f1ff17a5d764dab83046" data-initial-state="{}" > <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#"> <meta name="robots" content="max-image-preview:large" /> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=7; IE=9; IE=10; IE=11" /> <title>Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀</title> <link rel="canonical" href="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200"/> <meta itemprop="name" content="Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀"/> <meta itemprop="image" content="https://ogimage.blog.st-hatena.com/10257846132617921657/820878482937449108/1685887920"/> <meta property="og:title" content="Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀"/> <meta property="og:type" content="article"/> <meta property="og:url" content="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200"/> <meta property="og:image" content="https://ogimage.blog.st-hatena.com/10257846132617921657/820878482937449108/1685887920"/> <meta property="og:image:alt" content="Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀"/> <meta property="og:description" content="これは、なにをしたくて書いたもの? Spring Frameworkで、トランザクションの完了時に処理を行うことができるTransactionSynchronizationというものがあります。 存在は知っていたものの、ちゃんと使ったことがなかったので今回試してみることにしました。 TransactionSynchronization TransactionSynchronizationは、Spring Frameworkのドキュメントには登場しません。 Transaction Management :: Spring Framework Javadocを見ることになります。 Transact…" /> <meta property="og:site_name" content="CLOVER🍀"/> <meta property="article:published_time" content="2023-06-04T14:12:00Z" /> <meta property="article:tag" content="Spring" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:image" content="https://ogimage.blog.st-hatena.com/10257846132617921657/820878482937449108/1685887920" /> <meta name="twitter:title" content="Spring FrameworkのTransactionSynchronizationを試す - CLOVER🍀" /> <meta name="twitter:description" content="これは、なにをしたくて書いたもの? Spring Frameworkで、トランザクションの完了時に処理を行うことができるTransactionSynchronizationというものがあります。 存在は知っていたものの、ちゃんと使ったことがなかったので今回試してみることにしました。 TransactionSynchro…" /> <meta name="twitter:app:name:iphone" content="はてなブログアプリ" /> <meta name="twitter:app:id:iphone" content="583299321" /> <meta name="twitter:app:url:iphone" content="hatenablog:///open?uri=https%3A%2F%2Fkazuhira-r.hatenablog.com%2Fentry%2F2023%2F06%2F04%2F231200" /> <meta name="twitter:site" content="@kazuhira_r" /> <meta name="description" content="これは、なにをしたくて書いたもの? Spring Frameworkで、トランザクションの完了時に処理を行うことができるTransactionSynchronizationというものがあります。 存在は知っていたものの、ちゃんと使ったことがなかったので今回試してみることにしました。 TransactionSynchronization TransactionSynchronizationは、Spring Frameworkのドキュメントには登場しません。 Transaction Management :: Spring Framework Javadocを見ることになります。 Transact…" /> <script id="embed-gtm-data-layer-loader" data-data-layer-page-specific="{"hatenablog":{"blogs_permalink":{"is_author_pro":"false","entry_afc_issued":"false","is_blog_sleeping":"false","has_related_entries_with_elasticsearch":"true","blog_afc_issued":"false"}}}" > (function() { function loadDataLayer(elem, attrName) { if (!elem) { return {}; } var json = elem.getAttribute(attrName); if (!json) { return {}; } return JSON.parse(json); } var globalVariables = loadDataLayer( document.documentElement, 'data-data-layer' ); var pageSpecificVariables = loadDataLayer( document.getElementById('embed-gtm-data-layer-loader'), 'data-data-layer-page-specific' ); var variables = [globalVariables, pageSpecificVariables]; if (!window.dataLayer) { window.dataLayer = []; } for (var i = 0; i < variables.length; i++) { window.dataLayer.push(variables[i]); } })(); </script> <!-- Google Tag Manager --> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-P4CXTW');</script> <!-- End Google Tag Manager --> <link rel="shortcut icon" href="https://kazuhira-r.hatenablog.com/icon/favicon"> <link rel="apple-touch-icon" href="https://kazuhira-r.hatenablog.com/icon/touch"> <link rel="icon" sizes="192x192" href="https://kazuhira-r.hatenablog.com/icon/link"> <link rel="alternate" type="application/atom+xml" title="Atom" href="https://kazuhira-r.hatenablog.com/feed"/> <link rel="alternate" type="application/rss+xml" title="RSS2.0" href="https://kazuhira-r.hatenablog.com/rss"/> <link rel="alternate" type="application/json+oembed" href="https://hatena.blog/oembed?url=https%3A%2F%2Fkazuhira-r.hatenablog.com%2Fentry%2F2023%2F06%2F04%2F231200&format=json" title="oEmbed Profile of Spring FrameworkのTransactionSynchronizationを試す"/> <link rel="alternate" type="text/xml+oembed" href="https://hatena.blog/oembed?url=https%3A%2F%2Fkazuhira-r.hatenablog.com%2Fentry%2F2023%2F06%2F04%2F231200&format=xml" title="oEmbed Profile of Spring FrameworkのTransactionSynchronizationを試す"/> <link rel="author" href="http://www.hatena.ne.jp/Kazuhira/"> <link rel="stylesheet" type="text/css" href="https://cdn.blog.st-hatena.com/css/blog.css?version=f55a19e7a4f1ff17a5d764dab83046"/> <link rel="stylesheet" type="text/css" href="https://usercss.blog.st-hatena.com/blog_style/10257846132617921657/72202c9bd0b47ee51f48793f4bd475072a6541be"/> <script> </script> <style> div#google_afc_user, div.google-afc-user-container, div.google_afc_image, div.google_afc_blocklink { display: block !important; } </style> <script src="https://cdn.pool.st-hatena.com/valve/valve.js" async></script> <script id="test-valve-definition"> var valve = window.valve || []; valve.push(function(v) { v.config({ service: 'blog', content: { result: 'adtrust', documentIds: ["blog:entry:820878482937449108"] } }); v.defineDFPSlot({"lazy":1,"sizes":{"mappings":[[[320,568],[[336,280],[300,250],"fluid"]],[[0,0],[[300,250]]]]},"slotId":"ad-in-entry","unit":"/4374287/blog_pc_entry_sleep_in-article"}); v.defineDFPSlot({"lazy":"","sizes":[[300,250],[336,280],[468,60],"fluid"],"slotId":"google_afc_user_container_0","unit":"/4374287/blog_user"}); v.sealDFPSlots(); }); </script> <script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","dateModified":"2023-06-04T23:12:00+09:00","datePublished":"2023-06-04T23:12:00+09:00","headline":"Spring FrameworkのTransactionSynchronizationを試す","image":["https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png"]}</script> <script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.2.1/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: true }); </script> </head> <body class="page-entry category-Spring globalheader-ng-enabled"> <div id="globalheader-container" data-brand="hatenablog" > <iframe id="globalheader" height="37" frameborder="0" allowTransparency="true"></iframe> </div> <nav class=" blog-controlls "> <div class="blog-controlls-blog-icon"> <a href="https://kazuhira-r.hatenablog.com/"> <img src="https://cdn.blog.st-hatena.com/images/admin/blog-icon-noimage.png" alt="CLOVER🍀"/> </a> </div> <div class="blog-controlls-title"> <a href="https://kazuhira-r.hatenablog.com/">CLOVER🍀</a> </div> <a href="https://blog.hatena.ne.jp/Kazuhira/kazuhira-r.hatenablog.com/subscribe?utm_source=blogs_topright_button&utm_medium=button&utm_campaign=subscribe_blog" class="blog-controlls-subscribe-btn test-blog-header-controlls-subscribe"> 読者になる </a> </nav> <div id="container"> <div id="container-inner"> <header id="blog-title" data-brand="hatenablog"> <div id="blog-title-inner" > <div id="blog-title-content"> <h1 id="title"><a href="https://kazuhira-r.hatenablog.com/">CLOVER🍀</a></h1> <h2 id="blog-description">That was when it all began.</h2> </div> </div> </header> <div id="content" class="hfeed" > <div id="content-inner"> <div id="wrapper"> <div id="main"> <div id="main-inner"> <!-- google_ad_section_start --> <!-- rakuten_ad_target_begin --> <article class="entry hentry test-hentry js-entry-article date-first autopagerize_page_element chars-20000 words-2800 mode-markdown entry-odd" id="entry-820878482937449108" data-keyword-campaign="" data-uuid="820878482937449108" data-publication-type="entry"> <div class="entry-inner"> <header class="entry-header"> <div class="date entry-date first"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/06/04" rel="nofollow"> <time datetime="2023-06-04T14:12:00Z" title="2023-06-04T14:12:00Z"> <span class="date-year">2023</span><span class="hyphen">-</span><span class="date-month">06</span><span class="hyphen">-</span><span class="date-day">04</span> </time> </a> </div> <h1 class="entry-title"> <a href="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200" class="entry-title-link bookmark">Spring FrameworkのTransactionSynchronizationを試す</a> </h1> <div class="entry-categories categories"> <a href="https://kazuhira-r.hatenablog.com/archive/category/Spring" class="entry-category-link category-Spring">Spring</a> </div> </header> <div class="entry-content hatenablog-entry"> <h4 id="これはなにをしたくて書いたもの">これは、なにをしたくて書いたもの?</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Spring%20Framework">Spring Framework</a>で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の完了時に処理を行うことができるTransactionSynchronizationというものがあります。</p> <p>存在は知っていたものの、ちゃんと使ったことがなかったので今回試してみることにしました。</p> <h4 id="TransactionSynchronization">TransactionSynchronization</h4> <p>TransactionSynchronizationは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Spring%20Framework">Spring Framework</a>のドキュメントには登場しません。</p> <p><a href="https://docs.spring.io/spring-framework/reference/data-access/transaction.html">Transaction Management :: Spring Framework</a></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Javadoc">Javadoc</a>を見ることになります。</p> <p><a href="https://docs.spring.io/spring-framework/docs/6.0.9/javadoc-api/org/springframework/transaction/support/TransactionSynchronization.html">TransactionSynchronization (Spring Framework 6.0.9 API)</a></p> <p>TransactionSynchronizationを使うと、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の完了時になんらかの処理を行うことができます。</p> <blockquote><p>Interface for transaction synchronization callbacks.</p></blockquote> <p><code>TransactionSynchronization</code>インターフェースを実装して、以下の4種類のメソッドをオーバーライドします。</p> <ul> <li>beforeCommit(boolean readOnly) … <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット前に呼び出される <ul> <li><code>readOnly</code>は、読み取り専用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>かどうか</li> <li><code>RuntimeException</code>をスローすると、呼び出し元に伝播する(<code>TransactionException</code>のサブクラスをスローしてはいけない)</li> </ul> </li> <li>beforeCompletion … <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット/<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>の前に呼び出される <ul> <li><code>RuntimeException</code>をスローしても、呼び出し元に伝播しない(<code>TransactionException</code>のサブクラスをスローしてはいけない)</li> </ul> </li> <li>afterCommit … <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット後に呼び出される <ul> <li><code>RuntimeException</code>をスローすると、呼び出し元に伝播する(<code>TransactionException</code>のサブクラスをスローしてはいけない)</li> </ul> </li> <li>afterCompletion(int status) … <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット/<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>の後に呼び出される <ul> <li><code>status</code>は、定数定義(<code>STATUS_COMMITTED</code>、<code>STATUS_ROLLED_BACK</code>、<code>STATUS_UNKNOWN</code></li> <li><code>RuntimeException</code>をスローしても、呼び出し元に伝播しない(<code>TransactionException</code>のサブクラスをスローしてはいけない)</li> </ul> </li> </ul> <p><code>TransactionSynchronization</code>は、<code>TransactionSynchronizationManager#registerSynchronization</code>で登録して使います。</p> <p><a href="https://docs.spring.io/spring-framework/docs/6.0.9/javadoc-api/org/springframework/transaction/support/TransactionSynchronizationManager.html">TransactionSynchronizationManager (Spring Framework 6.0.9 API)</a></p> <p><code>TransactionSynchronization</code>は複数登録できます。その順序は、<code>getOrder</code>メソッドを実装していない場合は追加順になります。</p> <p>説明はこんなところにして、実際に使ってみましょう。</p> <h4 id="環境">環境</h4> <p>今回の環境は、こちら。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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"</pre> <p>データベースには<a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a>を用意しました。</p> <pre class="code shell" data-lang="shell" data-unlink> MySQL localhost:3306 ssl practice SQL > select version(); +-----------+ | version() | +-----------+ | 8.0.33 | +-----------+ 1 row in set (0.0370 sec)</pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a>は172.17.0.2で動作しているものとし、データベースpractice、アカウントはkazuhira/passwordで接続できるものと します。</p> <h4 id="Spring-Bootプロジェクトを作成する">Spring Bootプロジェクトを作成する</h4> <p>まずはSpring Bootプロジェクトを作成します。依存関係には、<code>web</code>、<code>jdbc</code>、<code>mysql</code>を追加。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 -</pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内に移動。</p> <pre class="code shell" data-lang="shell" data-unlink>$ cd transaction-synchronization-example</pre> <p>生成された<a class="keyword" href="https://d.hatena.ne.jp/keyword/Maven">Maven</a>依存関係など。</p> <pre class="code lang-xml" data-lang="xml" data-unlink> <span class="synIdentifier"><properties></span> <span class="synIdentifier"><java</span><span class="synComment">.</span><span class="synIdentifier">version></span>17<span class="synIdentifier"></java</span><span class="synComment">.</span><span class="synIdentifier">version></span> <span class="synIdentifier"></properties></span> <span class="synIdentifier"><dependencies></span> <span class="synIdentifier"><dependency></span> <span class="synIdentifier"><groupId></span>org.springframework.boot<span class="synIdentifier"></groupId></span> <span class="synIdentifier"><artifactId></span>spring-boot-starter-jdbc<span class="synIdentifier"></artifactId></span> <span class="synIdentifier"></dependency></span> <span class="synIdentifier"><dependency></span> <span class="synIdentifier"><groupId></span>org.springframework.boot<span class="synIdentifier"></groupId></span> <span class="synIdentifier"><artifactId></span>spring-boot-starter-web<span class="synIdentifier"></artifactId></span> <span class="synIdentifier"></dependency></span> <span class="synIdentifier"><dependency></span> <span class="synIdentifier"><groupId></span>com.mysql<span class="synIdentifier"></groupId></span> <span class="synIdentifier"><artifactId></span>mysql-connector-j<span class="synIdentifier"></artifactId></span> <span class="synIdentifier"><scope></span>runtime<span class="synIdentifier"></scope></span> <span class="synIdentifier"></dependency></span> <span class="synIdentifier"><dependency></span> <span class="synIdentifier"><groupId></span>org.springframework.boot<span class="synIdentifier"></groupId></span> <span class="synIdentifier"><artifactId></span>spring-boot-starter-test<span class="synIdentifier"></artifactId></span> <span class="synIdentifier"><scope></span>test<span class="synIdentifier"></scope></span> <span class="synIdentifier"></dependency></span> <span class="synIdentifier"></dependencies></span> <span class="synIdentifier"><build></span> <span class="synIdentifier"><plugins></span> <span class="synIdentifier"><plugin></span> <span class="synIdentifier"><groupId></span>org.springframework.boot<span class="synIdentifier"></groupId></span> <span class="synIdentifier"><artifactId></span>spring-boot-maven-plugin<span class="synIdentifier"></artifactId></span> <span class="synIdentifier"></plugin></span> <span class="synIdentifier"></plugins></span> <span class="synIdentifier"></build></span> </pre> <p>自動生成された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>は削除しておきます。</p> <pre class="code shell" data-lang="shell" data-unlink>$ rm src/main/java/org/littlewings/spring/tx/TransactionSynchronizationExampleApplication.java src/test/java/org/littlewings/spring/tx/TransactionSynchronizationExampleApplicationTests.java</pre> <p>テーブル定義。お題は書籍にしました。</p> <p><code>src/main/resources/schema.sql</code></p> <pre class="code lang-java" data-lang="java" data-unlink>drop table <span class="synStatement">if</span> exists book; create table book( isbn varchar(<span class="synConstant">14</span>), title varchar(<span class="synConstant">100</span>), price <span class="synType">int</span>, primary key(isbn) ); </pre> <p>この<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a>は、アプリケーション起動時に毎回実行されるように設定。</p> <p><code>src/main/resources/application.properties</code></p> <pre class="code lang-jproperties" data-lang="jproperties" data-unlink><span class="synIdentifier">spring.datasource.url</span>=<span class="synConstant">jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin</span> <span class="synIdentifier">spring.datasource.username</span>=<span class="synConstant">kazuhira</span> <span class="synIdentifier">spring.datasource.password</span>=<span class="synConstant">password</span> <span class="synIdentifier">spring.sql.init.mode</span>=<span class="synConstant">always</span> </pre> <p>エンティティ相当のクラス。</p> <p><code>src/main/java/org/littlewings/spring/tx/Book.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synType">public</span> <span class="synType">class</span> Book { String isbn; String title; Integer price; <span class="synType">public</span> <span class="synType">static</span> Book create(String isbn, String title, Integer price) { Book book = <span class="synStatement">new</span> Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); <span class="synStatement">return</span> book; } <span class="synComment">// getter/setterは省略</span> } </pre> <p><code>TransactionSynchronization</code>を使ったServiceクラス。</p> <p><code>src/main/java/org/littlewings/spring/tx/BookService.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.springframework.jdbc.core.BeanPropertyRowMapper; <span class="synPreProc">import</span> org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; <span class="synPreProc">import</span> org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; <span class="synPreProc">import</span> org.springframework.stereotype.Service; <span class="synPreProc">import</span> org.springframework.transaction.annotation.Transactional; <span class="synPreProc">import</span> org.springframework.transaction.support.TransactionSynchronization; <span class="synPreProc">import</span> org.springframework.transaction.support.TransactionSynchronizationManager; <span class="synPreProc">import</span> java.util.List; <span class="synPreProc">import</span> java.util.Map; <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Service</span> <span class="synType">public</span> <span class="synType">class</span> BookService { NamedParameterJdbcTemplate jdbcTemplate; LoggingService loggingService; <span class="synType">public</span> BookService(NamedParameterJdbcTemplate jdbcTemplate, LoggingService loggingService) { <span class="synType">this</span>.jdbcTemplate = jdbcTemplate; <span class="synType">this</span>.loggingService = loggingService; } <span class="synType">public</span> Book findByIsbn(String isbn) { <span class="synStatement">return</span> jdbcTemplate.queryForObject(<span class="synConstant">"""</span> select isbn, title, price from book where isbn = :isbn<span class="synConstant">""",</span> Map.of(<span class="synConstant">"isbn"</span>, isbn), <span class="synStatement">new</span> BeanPropertyRowMapper<>(Book.<span class="synType">class</span>)); } <span class="synType">public</span> List<Book> findAll() { <span class="synStatement">return</span> jdbcTemplate.query(<span class="synConstant">"""</span> select isbn, title, price from book order by price asc<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertyRowMapper<>(Book.<span class="synType">class</span>)); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCommit(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCommitRollback(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!"</span>); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCommitThrowException(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!, after commit"</span>); } }); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletion(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { loggingService.log(<span class="synConstant">"after completion, status = "</span> + status); } }); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletionRollback(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { loggingService.log(<span class="synConstant">"after completion, status = "</span> + status); } }); <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!"</span>); } <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletionThrowException(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!, after completion, status = "</span> + status); } }); } } </pre> <p>今回は、クラス自体に<code>@Transactional</code><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>を付与しています。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">@Transactional</span> <span class="synPreProc">@Service</span> <span class="synType">public</span> <span class="synType">class</span> BookService { </pre> <p><code>TransactionSynchronization</code>は、こんな感じで<code>TransactionSynchronization</code>クラスを継承したクラスを作成し、処理を行いたいタイミングに<br/> 応じたメソッドをオーバーライドします。そして、<code>TransactionSynchronizationManager#registerSynchronization</code>で登録します。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCommit(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); } </pre> <p><code>TransactionSynchronization</code>の部分は、また後で説明します。</p> <p>メソッド内で呼び出しているServiceクラスは、こんな感じのものです。</p> <p><code>src/main/java/org/littlewings/spring/tx/LoggingService.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.slf4j.Logger; <span class="synPreProc">import</span> org.slf4j.LoggerFactory; <span class="synPreProc">import</span> org.springframework.stereotype.Service; <span class="synPreProc">@Service</span> <span class="synType">public</span> <span class="synType">class</span> LoggingService { Logger logger = LoggerFactory.getLogger(LoggingService.<span class="synType">class</span>); <span class="synType">public</span> <span class="synType">void</span> log(String message) { logger.info(message); } } </pre> <p>RestController。</p> <p><code>src/main/java/org/littlewings/spring/tx/BookController.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.springframework.http.MediaType; <span class="synPreProc">import</span> org.springframework.http.ResponseEntity; <span class="synPreProc">import</span> org.springframework.web.bind.annotation.*; <span class="synPreProc">import</span> org.springframework.web.util.UriComponentsBuilder; <span class="synPreProc">import</span> java.util.List; <span class="synPreProc">@RestController</span> <span class="synPreProc">@RequestMapping</span>(<span class="synConstant">"books"</span>) <span class="synType">public</span> <span class="synType">class</span> BookController { BookService bookService; <span class="synType">public</span> BookController(BookService bookService) { <span class="synType">this</span>.bookService = bookService; } <span class="synPreProc">@GetMapping</span>(value = <span class="synConstant">"{isbn}"</span>, produces = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> Book findByIsbn(<span class="synPreProc">@PathVariable</span> String isbn) { <span class="synStatement">return</span> bookService.findByIsbn(isbn); } <span class="synPreProc">@GetMapping</span>(produces = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> List<Book> findAll() { <span class="synStatement">return</span> bookService.findAll(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommit(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommit(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit-rollback"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommitRollback(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommitRollback(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit-throw"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommitThrowException(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommitThrowException(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletion(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletion(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion-rollback"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletionRollback(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletionRollback(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion-throw"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletionThrowException(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletionThrowException(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } } </pre> <p><code>main</code>メソッドを定義したクラス。</p> <p><code>src/main/java/org/littlewings/spring/tx/App.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.springframework.boot.SpringApplication; <span class="synPreProc">import</span> org.springframework.boot.autoconfigure.SpringBootApplication; <span class="synPreProc">@SpringBootApplication</span> <span class="synType">public</span> <span class="synType">class</span> App { <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> main(String<span class="synIdentifier">...</span> args) { SpringApplication.run(App.<span class="synType">class</span>, args); } } </pre> <p>ここまでで、準備は完了です。</p> <h4 id="動かしてみる">動かしてみる</h4> <p>では、アプリケーションをパッケージングして起動します。</p> <pre class="code shell" data-lang="shell" data-unlink>$ mvn package $ java -jar target/transaction-synchronization-example-0.0.1-SNAPSHOT.jar</pre> <p>データを登録するのは、以下の4つのエンドポイントがありました。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommit(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommit(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit-rollback"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommitRollback(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommitRollback(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-commit-throw"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCommitThrowException(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCommitThrowException(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletion(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletion(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion-rollback"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletionRollback(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletionRollback(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } <span class="synPreProc">@PostMapping</span>(value = <span class="synConstant">"after-completion-throw"</span>, consumes = MediaType.APPLICATION_JSON_VALUE) <span class="synType">public</span> ResponseEntity<Void> registerAfterCompletionThrowException(<span class="synPreProc">@RequestBody</span> Book book, UriComponentsBuilder uriComponentsBuilder) { bookService.insertAfterCompletionThrowException(book); <span class="synStatement">return</span> ResponseEntity.created(uriComponentsBuilder.path(<span class="synConstant">"books/{isbn}"</span>).build(book.getIsbn())).build(); } </pre> <p>これらを、それぞれ対応する(<code>TransactionSynchronization</code>を使った)Serviceクラスのメソッドと合わせて見ていきます。</p> <p>まずは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット後に動作する<code>TransactionSynchronization#afterCommit</code>から。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCommit(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 </pre> <p>アプリケーション側には、ログが出力されます。これはコミット後に動作したことになります。</p> <pre class="code shell" data-lang="shell" data-unlink>2023-06-04T22:13:46.844+09:00 INFO 22000 --- [nio-8080-exec-1] o.littlewings.spring.tx.LoggingService : after commit</pre> <p>データも入りました。</p> <pre class="code shell" data-lang="shell" data-unlink>$ curl -s localhost:8080/books | jq [ { "isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400 } ]</pre> <p>次は、<code>TransactionSynchronization#afterCommit</code>を使いつつ、例外をスローして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>させてみましょう。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCommitRollback(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!"</span>); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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"}</pre> <p>エラーになりました。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>。</p> <pre class="code shell" data-lang="shell" data-unlink>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] 〜省略〜</pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>されているので、以下の箇所に相当するログは出力されていません。</p> <pre class="code lang-java" data-lang="java" data-unlink> TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { loggingService.log(<span class="synConstant">"after commit"</span>); } }); </pre> <p>データも増えていませんね。</p> <pre class="code shell" data-lang="shell" data-unlink>$ curl -s localhost:8080/books | jq [ { "isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400 } ]</pre> <p><code>TransactionSynchronization#afterCommit</code>から例外を投げてみます。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCommitThrowException(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!, after commit"</span>); } }); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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"}</pre> <p>エラーになりました。</p> <p>この時の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>。</p> <pre class="code shell" data-lang="shell" data-unlink>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] 〜省略〜</pre> <p>今回は<code>TransactionSynchronization#afterCommit</code>内から例外をスローしたわけですが。</p> <pre class="code lang-java" data-lang="java" data-unlink> TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCommit() { <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!, after commit"</span>); } }); </pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>を見ると、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>境界のメソッドから例外がスローされたことになっていますね。</p> <pre class="code shell" data-lang="shell" data-unlink> 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]</pre> <p>これが、呼び出し元に伝播するということですね。</p> <p>一方で、この処理はコミット後に動作しているので、例外を投げても<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>されません。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 } ]</pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の完了後に動作する<code>TransactionSynchronization#afterCompletion</code>。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletion(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { loggingService.log(<span class="synConstant">"after completion, status = "</span> + status); } }); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 </pre> <p>こちらも、ログが出力されました。</p> <pre class="code shell" data-lang="shell" data-unlink>2023-06-04T22:19:16.678+09:00 INFO 22000 --- [nio-8080-exec-7] o.littlewings.spring.tx.LoggingService : after completion, status = 0</pre> <p><code>0</code>というのは、<code>TransactionSynchronization#STATUS_COMMITTED</code>の値ですね。</p> <p>データも追加されました。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 } ]</pre> <p><code>TransactionSynchronization#afterCompletion</code>を使いつつ、例外をスローしてみます。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletionRollback(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { loggingService.log(<span class="synConstant">"after completion, status = "</span> + status); } }); <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!"</span>); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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"}</pre> <p>エラーになりました。</p> <p>この時のログと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>。</p> <pre class="code shell" data-lang="shell" data-unlink>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] 〜省略〜</pre> <p>ポイントは、<code>TransactionSynchronization#afterCompletion</code>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>時も呼び出されるので、以下のログが出力されていることです。</p> <pre class="code shell" data-lang="shell" data-unlink>2023-06-04T22:21:11.853+09:00 INFO 22000 --- [io-8080-exec-10] o.littlewings.spring.tx.LoggingService : after completion, status = 1</pre> <p><code>0</code>というのは、<code>TransactionSynchronization#STATUS_ROLLED_BACK</code>の値ですね。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>されているので、データは増えていません。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 } ]</pre> <p>最後は、<code>TransactionSynchronization#afterCompletion</code>内から例外をスローしてみます。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synType">public</span> <span class="synType">void</span> insertAfterCompletionThrowException(Book book) { jdbcTemplate.update(<span class="synConstant">"""</span> insert into book(isbn, title, price) values(:isbn, :title, :price)<span class="synConstant">""",</span> <span class="synStatement">new</span> BeanPropertySqlParameterSource(book)); TransactionSynchronizationManager.registerSynchronization(<span class="synStatement">new</span> TransactionSynchronization() { <span class="synPreProc">@Override</span> <span class="synType">public</span> <span class="synType">void</span> afterCompletion(<span class="synType">int</span> status) { <span class="synStatement">throw</span> <span class="synStatement">new</span> RuntimeException(<span class="synConstant">"Oops!!, after completion, status = "</span> + status); } }); } </pre> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 </pre> <p>ふつうに処理が終了しました。</p> <p>この時、アプリケーション側には<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>もなにも出力されていませんでした。</p> <p>データはしっかり増えています。</p> <pre class="code shell" data-lang="shell" data-unlink>$ 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 } ]</pre> <p>これで、<code>TransactionSynchronization#afterCompletion</code>内で例外をスローしても呼び出し元に影響しない(伝播しない)ことが<br/> 確認できました。</p> <h4 id="TestTransactionを使ってテストで確認する">TestTransactionを使ってテストで確認する</h4> <p>ところで、<code>TransactionSynchronization</code>は時には便利な機能ですが、<code>TransactionSynchronizationManager</code>に登録した<br/> <code>TransactionSynchronization</code>が動作していることを確認するにはどうしたらいいのでしょうか?</p> <p><code>TestTransaction</code>を使うのが良さそうです。</p> <p><a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/tx.html#testcontext-tx-programmatic-tx-mgt">Transaction Management / Programmatic Transaction Management</a></p> <p><code>TestTransaction</code>を使うと、テストで使われている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>のコミット、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>をメソッド呼び出しで制御することが<br/> できます。</p> <blockquote><p>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.</p></blockquote> <p><a href="https://docs.spring.io/spring-framework/docs/6.0.9/javadoc-api/org/springframework/test/context/transaction/TestTransaction.html">TestTransaction (Spring Framework 6.0.9 API)</a></p> <p>作成したテストは、こんな感じです。</p> <p><code>src/test/java/org/littlewings/spring/tx/BookServiceTest.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.junit.jupiter.api.BeforeEach; <span class="synPreProc">import</span> org.junit.jupiter.api.Test; <span class="synPreProc">import</span> org.springframework.beans.factory.annotation.Autowired; <span class="synPreProc">import</span> org.springframework.boot.test.context.SpringBootTest; <span class="synPreProc">import</span> org.springframework.boot.test.mock.mockito.MockBean; <span class="synPreProc">import</span> org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; <span class="synPreProc">import</span> org.springframework.test.annotation.DirtiesContext; <span class="synPreProc">import</span> org.springframework.test.context.transaction.TestTransaction; <span class="synPreProc">import</span> org.springframework.transaction.annotation.Transactional; <span class="synPreProc">import</span> org.springframework.transaction.support.TransactionSynchronization; <span class="synPreProc">import</span> java.util.Collections; <span class="synPreProc">import</span> java.util.List; <span class="synPreProc">import static</span> org.assertj.core.api.Assertions.assertThat; <span class="synPreProc">import static</span> org.assertj.core.api.Assertions.assertThatThrownBy; <span class="synPreProc">import static</span> org.mockito.Mockito.*; <span class="synPreProc">@DirtiesContext</span> <span class="synPreProc">@SpringBootTest</span> <span class="synType">class</span> BookServiceTest { <span class="synPreProc">@Autowired</span> BookService bookService; <span class="synPreProc">@MockBean</span> LoggingService loggingService; <span class="synPreProc">@Autowired</span> NamedParameterJdbcTemplate jdbcTemplate; <span class="synPreProc">@BeforeEach</span> <span class="synType">void</span> setUp() { reset(loggingService); jdbcTemplate.update(<span class="synConstant">"truncate table book"</span>, Collections.emptyMap()); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> query() { bookService.insertAfterCommit(Book.create(<span class="synConstant">"978-4621303252"</span>, <span class="synConstant">"Effective Java 第3版"</span>, <span class="synConstant">4400</span>)); bookService.insertAfterCommit(Book.create(<span class="synConstant">"978-4798161488"</span>, <span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>, <span class="synConstant">4180</span>)); Book javaBook = bookService.findByIsbn(<span class="synConstant">"978-4621303252"</span>); assertThat(javaBook.getIsbn()).isEqualTo(<span class="synConstant">"978-4621303252"</span>); assertThat(javaBook.getTitle()).isEqualTo(<span class="synConstant">"Effective Java 第3版"</span>); assertThat(javaBook.getPrice()).isEqualTo(<span class="synConstant">4400</span>); List<Book> books = bookService.findAll(); assertThat(books).hasSize(<span class="synConstant">2</span>); assertThat(books.get(<span class="synConstant">0</span>).getTitle()).isEqualTo(<span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>); assertThat(books.get(<span class="synConstant">1</span>).getTitle()).isEqualTo(<span class="synConstant">"Effective Java 第3版"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitWithCommit() { doNothing().when(loggingService).log(<span class="synConstant">"after commit"</span>); bookService.insertAfterCommit(Book.create(<span class="synConstant">"978-4621303252"</span>, <span class="synConstant">"Effective Java 第3版"</span>, <span class="synConstant">4400</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after commit"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitWithRollback() { bookService.insertAfterCommit(Book.create(<span class="synConstant">"978-4621303252"</span>, <span class="synConstant">"Effective Java 第3版"</span>, <span class="synConstant">4400</span>)); TestTransaction.flagForRollback(); TestTransaction.end(); verify(loggingService, never()) .log(anyString()); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitWithThrowExceptionRollback() { assertThatThrownBy(() -> bookService.insertAfterCommitRollback(Book.create(<span class="synConstant">"978-4297126858"</span>, <span class="synConstant">"プロになるJava―仕事で必要なプログラミングの知識がゼロから身につく最高の指南書"</span>, <span class="synConstant">3278</span>))) .isExactlyInstanceOf(RuntimeException.<span class="synType">class</span>) .hasMessage(<span class="synConstant">"Oops!!"</span>); verify(loggingService, never()) .log(anyString()); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitThrowExceptionWithCommit() { bookService.insertAfterCommitThrowException(Book.create(<span class="synConstant">"978-4798161488"</span>, <span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>, <span class="synConstant">4180</span>)); TestTransaction.flagForCommit(); assertThatThrownBy(() -> TestTransaction.end()) .isExactlyInstanceOf(RuntimeException.<span class="synType">class</span>) .hasMessage(<span class="synConstant">"Oops!!, after commit"</span>); verify(loggingService, never()) .log(anyString()); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitThrowExceptionWithRollback() { bookService.insertAfterCommitThrowException(Book.create(<span class="synConstant">"978-4798161488"</span>, <span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>, <span class="synConstant">4180</span>)); TestTransaction.flagForRollback(); TestTransaction.end(); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionWithCommit() { doNothing().when(loggingService).log(<span class="synConstant">"after completion, status = 0"</span>); bookService.insertAfterCompletion(Book.create(<span class="synConstant">"978-4297124298"</span>, <span class="synConstant">"Spring Framework超入門 〜やさしくわかるWebアプリ開発"</span>, <span class="synConstant">3058</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after completion, status = 0"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionWithRollback() { doNothing().when(loggingService).log(<span class="synConstant">"after completion, status = 1"</span>); bookService.insertAfterCompletion(Book.create(<span class="synConstant">"978-4774189093"</span>, <span class="synConstant">"Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"</span>, <span class="synConstant">3278</span>)); TestTransaction.flagForRollback(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after completion, status = 1"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionThrowExceptionRollback() { doNothing().when(loggingService).log(<span class="synConstant">"after completion, status = 1"</span>); assertThatThrownBy(() -> bookService.insertAfterCompletionRollback(Book.create(<span class="synConstant">"978-4774189093"</span>, <span class="synConstant">"Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"</span>, <span class="synConstant">3278</span>))); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after completion, status = 1"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionThrowExceptionWithCommit() { bookService.insertAfterCompletionThrowException(Book.create(<span class="synConstant">"978-4297131425"</span>, <span class="synConstant">"実践Redis入門 技術の仕組みから現場の活用まで"</span>, <span class="synConstant">4180</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); } <span class="synPreProc">@Test</span> <span class="synType">void</span> transactionCompletionStatus() { assertThat(TransactionSynchronization.STATUS_COMMITTED).isEqualTo(<span class="synConstant">0</span>); assertThat(TransactionSynchronization.STATUS_ROLLED_BACK).isEqualTo(<span class="synConstant">1</span>); assertThat(TransactionSynchronization.STATUS_UNKNOWN).isEqualTo(<span class="synConstant">2</span>); } } </pre> <p>少し、ピックアップして見てみましょう。</p> <p><code>TransactionSynchronization#afterCommit</code>でコミットさせる場合。<code>TestTransaction#flagForCommit</code>を呼び出して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>の<br/> コミット用のフラグを立てて、<code>TestTransaction#end</code>で確定します。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitWithCommit() { doNothing().when(loggingService).log(<span class="synConstant">"after commit"</span>); bookService.insertAfterCommit(Book.create(<span class="synConstant">"978-4621303252"</span>, <span class="synConstant">"Effective Java 第3版"</span>, <span class="synConstant">4400</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after commit"</span>); } </pre> <p>実際に呼び出されているかどうかの確認は、モックを使いました。</p> <p>なお、「コミット用のフラグを立てて」と言っているように、このテストで実行した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>はコミットされます。<br/> <code>@Transactional</code>を付けているからといって<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>されなくなるので、その点には注意が必要です。</p> <p>なお、<code>TestTransaction</code>を使うには<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>を開始しておく必要があるようです。このテストから<code>@Transactional</code>を削除すると<br/> テストが実行できなくなります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>させるには、<code>TestTransaction#flagForRollback</code>を呼び出して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>用のフラグを立てます。</p> <pre class="code" data-lang="" data-unlink> @Transactional @Test void insertAfterCommitWithRollback() { bookService.insertAfterCommit(Book.create("978-4621303252", "Effective Java 第3版", 4400)); TestTransaction.flagForRollback(); TestTransaction.end(); verify(loggingService, never()) .log(anyString()); }</pre> <p><code>TestTransaction#end</code>で確定させても、<code>TransactionSynchronization#afterCommit</code>の処理が呼び出されないことが確認できます。</p> <p><code>TransactionSynchronization#afterCommit</code>内で例外をスローした場合は、<code>TestTransaction#end</code>を呼び出した時に例外がスローされます。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitThrowExceptionWithCommit() { bookService.insertAfterCommitThrowException(Book.create(<span class="synConstant">"978-4798161488"</span>, <span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>, <span class="synConstant">4180</span>)); TestTransaction.flagForCommit(); assertThatThrownBy(() -> TestTransaction.end()) .isExactlyInstanceOf(RuntimeException.<span class="synType">class</span>) .hasMessage(<span class="synConstant">"Oops!!, after commit"</span>); verify(loggingService, never()) .log(anyString()); } </pre> <p><code>TransactionSynchronization#afterCompletion</code>の場合は、コミットしても<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>しても処理が呼び出されます。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionWithCommit() { doNothing().when(loggingService).log(<span class="synConstant">"after completion, status = 0"</span>); bookService.insertAfterCompletion(Book.create(<span class="synConstant">"978-4297124298"</span>, <span class="synConstant">"Spring Framework超入門 〜やさしくわかるWebアプリ開発"</span>, <span class="synConstant">3058</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after completion, status = 0"</span>); } <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionWithRollback() { doNothing().when(loggingService).log(<span class="synConstant">"after completion, status = 1"</span>); bookService.insertAfterCompletion(Book.create(<span class="synConstant">"978-4774189093"</span>, <span class="synConstant">"Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"</span>, <span class="synConstant">3278</span>)); TestTransaction.flagForRollback(); TestTransaction.end(); verify(loggingService, times(<span class="synConstant">1</span>)) .log(<span class="synConstant">"after completion, status = 1"</span>); } </pre> <p>ステータスとして渡ってくる値が変わりますね。</p> <p>そして、<code>TransactionSynchronization#afterCompletion</code>内で例外をスローしても、コミット時に例外は呼び出し元に伝播しません。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@Transactional</span> <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionThrowExceptionWithCommit() { bookService.insertAfterCompletionThrowException(Book.create(<span class="synConstant">"978-4297131425"</span>, <span class="synConstant">"実践Redis入門 技術の仕組みから現場の活用まで"</span>, <span class="synConstant">4180</span>)); TestTransaction.flagForCommit(); TestTransaction.end(); } </pre> <p>やや脱線していますが、<code>TransactionSynchronization</code>のステータス値の確認も。</p> <pre class="code lang-java" data-lang="java" data-unlink> <span class="synPreProc">@Test</span> <span class="synType">void</span> transactionCompletionStatus() { assertThat(TransactionSynchronization.STATUS_COMMITTED).isEqualTo(<span class="synConstant">0</span>); assertThat(TransactionSynchronization.STATUS_ROLLED_BACK).isEqualTo(<span class="synConstant">1</span>); assertThat(TransactionSynchronization.STATUS_UNKNOWN).isEqualTo(<span class="synConstant">2</span>); } </pre> <h4 id="実装を見る">実装を見る</h4> <p>先程の動作確認時の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C3%A5%AF%A5%C8%A5%EC%A1%BC%A5%B9">スタックトレース</a>を見るとわかりますが、<code>TransactionSynchronization</code>を呼び出している処理はこのあたりですね。</p> <p><a href="https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L720-L786">https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L720-L786</a></p> <p>実際に呼び出しを行うのは<code>TransactionSynchronizationUtils</code>ですね。</p> <p><a href="https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java">https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java</a></p> <p>また、<code>TransactionSynchronization#beforeCompletion</code>や<code>TransactionSynchronization#afterCompletion</code>で例外がスローされても<br/> 呼び出し元に伝播しないのは、ふつうに<code>try</code>〜<code>catch</code>しているからですね。</p> <p><a href="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#L105-L114</a></p> <p><a href="https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java#L166-L179">https://github.com/spring-projects/spring-framework/blob/v6.0.9/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java#L166-L179</a></p> <h4 id="オマケRestControllerのテストを書く">オマケ:RestControllerのテストを書く</h4> <p>最後に、RestControllerのテストも書いておいたので載せておきます。<code>curl</code>で実行していた内容を確認しています。<br/> ※データが増えていないことの確認まではしていませんが</p> <p><code>src/test/java/org/littlewings/spring/tx/BookControllerTest.java</code></p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synPreProc">package</span> org.littlewings.spring.tx; <span class="synPreProc">import</span> org.junit.jupiter.api.BeforeEach; <span class="synPreProc">import</span> org.junit.jupiter.api.Test; <span class="synPreProc">import</span> org.springframework.beans.factory.annotation.Autowired; <span class="synPreProc">import</span> org.springframework.boot.test.context.SpringBootTest; <span class="synPreProc">import</span> org.springframework.boot.test.web.client.TestRestTemplate; <span class="synPreProc">import</span> org.springframework.boot.test.web.server.LocalServerPort; <span class="synPreProc">import</span> org.springframework.core.ParameterizedTypeReference; <span class="synPreProc">import</span> org.springframework.http.HttpEntity; <span class="synPreProc">import</span> org.springframework.http.HttpMethod; <span class="synPreProc">import</span> org.springframework.http.HttpStatus; <span class="synPreProc">import</span> org.springframework.http.ResponseEntity; <span class="synPreProc">import</span> org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; <span class="synPreProc">import</span> java.net.URI; <span class="synPreProc">import</span> java.util.Collections; <span class="synPreProc">import</span> java.util.Map; <span class="synPreProc">import static</span> org.assertj.core.api.Assertions.assertThat; <span class="synPreProc">import static</span> org.assertj.core.api.Assertions.entry; <span class="synPreProc">@SpringBootTest</span>(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) <span class="synType">class</span> BookControllerTest { <span class="synPreProc">@Autowired</span> TestRestTemplate restTemplate; <span class="synPreProc">@LocalServerPort</span> <span class="synType">int</span> port; <span class="synPreProc">@Autowired</span> NamedParameterJdbcTemplate jdbcTemplate; <span class="synPreProc">@BeforeEach</span> <span class="synType">void</span> setUp() { jdbcTemplate.update(<span class="synConstant">"truncate table book"</span>, Collections.emptyMap()); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommit() { ResponseEntity<Void> response = restTemplate.postForEntity( <span class="synConstant">"/books/after-commit"</span>, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4621303252"</span>, <span class="synConstant">"Effective Java 第3版"</span>, <span class="synConstant">4400</span>)), Void.<span class="synType">class</span> ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format(<span class="synConstant">"http://localhost:%d/books/978-4621303252"</span>, port))); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4621303252"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isEqualTo(<span class="synConstant">"978-4621303252"</span>); assertThat(book.getTitle()).isEqualTo(<span class="synConstant">"Effective Java 第3版"</span>); assertThat(book.getPrice()).isEqualTo(<span class="synConstant">4400</span>); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitRollback() { ResponseEntity<Map<String, Object>> response = restTemplate.exchange( <span class="synConstant">"/books/after-commit-rollback"</span>, HttpMethod.POST, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4297126858"</span>, <span class="synConstant">"プロになるJava―仕事で必要なプログラミングの知識がゼロから身につく最高の指南書"</span>, <span class="synConstant">3278</span>)), <span class="synStatement">new</span> ParameterizedTypeReference<>() { } ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(response.getBody()) .containsOnlyKeys(<span class="synConstant">"timestamp"</span>, <span class="synConstant">"status"</span>, <span class="synConstant">"error"</span>, <span class="synConstant">"path"</span>) .contains(entry(<span class="synConstant">"status"</span>, <span class="synConstant">500</span>), entry(<span class="synConstant">"error"</span>, <span class="synConstant">"Internal Server Error"</span>), entry(<span class="synConstant">"path"</span>, <span class="synConstant">"/books/after-commit-rollback"</span>)); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4297126858"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isNull(); assertThat(book.getTitle()).isNull(); assertThat(book.getPrice()).isNull(); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCommitThrowException() { ResponseEntity<Map<String, Object>> response = restTemplate.exchange( <span class="synConstant">"/books/after-commit-throw"</span>, HttpMethod.POST, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4798161488"</span>, <span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>, <span class="synConstant">4180</span>)), <span class="synStatement">new</span> ParameterizedTypeReference<>() { } ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(response.getBody()) .containsOnlyKeys(<span class="synConstant">"timestamp"</span>, <span class="synConstant">"status"</span>, <span class="synConstant">"error"</span>, <span class="synConstant">"path"</span>) .contains(entry(<span class="synConstant">"status"</span>, <span class="synConstant">500</span>), entry(<span class="synConstant">"error"</span>, <span class="synConstant">"Internal Server Error"</span>), entry(<span class="synConstant">"path"</span>, <span class="synConstant">"/books/after-commit-throw"</span>)); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4798161488"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isEqualTo(<span class="synConstant">"978-4798161488"</span>); assertThat(book.getTitle()).isEqualTo(<span class="synConstant">"MySQL徹底入門 第4版 MySQL 8.0対応"</span>); assertThat(book.getPrice()).isEqualTo(<span class="synConstant">4180</span>); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletion() { ResponseEntity<Void> response = restTemplate.postForEntity( <span class="synConstant">"/books/after-completion"</span>, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4297124298"</span>, <span class="synConstant">"Spring Framework超入門 〜やさしくわかるWebアプリ開発"</span>, <span class="synConstant">3058</span>)), Void.<span class="synType">class</span> ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format(<span class="synConstant">"http://localhost:%d/books/978-4297124298"</span>, port))); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4297124298"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isEqualTo(<span class="synConstant">"978-4297124298"</span>); assertThat(book.getTitle()).isEqualTo(<span class="synConstant">"Spring Framework超入門 〜やさしくわかるWebアプリ開発"</span>); assertThat(book.getPrice()).isEqualTo(<span class="synConstant">3058</span>); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionRollback() { ResponseEntity<Map<String, Object>> response = restTemplate.exchange( <span class="synConstant">"/books/after-completion-rollback"</span>, HttpMethod.POST, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4774189093"</span>, <span class="synConstant">"Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"</span>, <span class="synConstant">3278</span>)), <span class="synStatement">new</span> ParameterizedTypeReference<>() { } ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(response.getBody()) .containsOnlyKeys(<span class="synConstant">"timestamp"</span>, <span class="synConstant">"status"</span>, <span class="synConstant">"error"</span>, <span class="synConstant">"path"</span>) .contains(entry(<span class="synConstant">"status"</span>, <span class="synConstant">500</span>), entry(<span class="synConstant">"error"</span>, <span class="synConstant">"Internal Server Error"</span>), entry(<span class="synConstant">"path"</span>, <span class="synConstant">"/books/after-commit-throw"</span>)); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4774189093"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isNull(); assertThat(book.getTitle()).isNull(); assertThat(book.getPrice()).isNull(); } <span class="synPreProc">@Test</span> <span class="synType">void</span> insertAfterCompletionThrowException() { ResponseEntity<Map<String, Object>> response = restTemplate.exchange( <span class="synConstant">"/books/after-completion-throw"</span>, HttpMethod.POST, <span class="synStatement">new</span> HttpEntity<>(Book.create(<span class="synConstant">"978-4297131425"</span>, <span class="synConstant">"実践Redis入門 技術の仕組みから現場の活用まで"</span>, <span class="synConstant">3696</span>)), <span class="synStatement">new</span> ParameterizedTypeReference<>() { } ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getHeaders().getLocation()).isEqualTo(URI.create(String.format(<span class="synConstant">"http://localhost:%d/books/978-4297131425"</span>, port))); Book book = restTemplate.getForObject(<span class="synConstant">"/books/978-4297131425"</span>, Book.<span class="synType">class</span>); assertThat(book.getIsbn()).isEqualTo(<span class="synConstant">"978-4297131425"</span>); assertThat(book.getTitle()).isEqualTo(<span class="synConstant">"実践Redis入門 技術の仕組みから現場の活用まで"</span>); assertThat(book.getPrice()).isEqualTo(<span class="synConstant">3696</span>); } } </pre> <h4 id="まとめ">まとめ</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Spring%20Framework">Spring Framework</a>の<code>TransactionSynchronization</code>を試してみました。</p> <p>存在は知っていましたが、ちゃんと使ったことがなかったので今回確認しておいてよかったです。<br/> 思っていたよりも、動作にバリエーションがあったなという感じがしました。</p> </div> <footer class="entry-footer"> <div class="entry-tags-wrapper"> <div class="entry-tags"> </div> </div> <p class="entry-footer-section track-inview-by-gtm" data-gtm-track-json="{"area": "finish_reading"}"> <span class="author vcard"><span class="fn" data-load-nickname="1" data-user-name="Kazuhira" >Kazuhira</span></span> <span class="entry-footer-time"><a href="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200"><time data-relative datetime="2023-06-04T14:12:00Z" title="2023-06-04T14:12:00Z" class="updated">2023-06-04 23:12</time></a></span> <span class=" entry-footer-subscribe " data-test-blog-controlls-subscribe> <a href="https://blog.hatena.ne.jp/Kazuhira/kazuhira-r.hatenablog.com/subscribe?utm_medium=button&utm_campaign=subscribe_blog&utm_source=blogs_entry_footer"> 読者になる </a> </span> </p> <div class="hatena-star-container" data-hatena-star-container data-hatena-star-url="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200" data-hatena-star-title="Spring FrameworkのTransactionSynchronizationを試す" data-hatena-star-variant="profile-icon" data-hatena-star-profile-url-template="https://blog.hatena.ne.jp/{username}/" ></div> <div class="social-buttons"> <div class="social-button-item"> <a href="https://b.hatena.ne.jp/entry/s/kazuhira-r.hatenablog.com/entry/2023/06/04/231200" class="hatena-bookmark-button" data-hatena-bookmark-url="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200" data-hatena-bookmark-layout="vertical-balloon" data-hatena-bookmark-lang="ja" title="この記事をはてなブックマークに追加"><img src="https://b.st-hatena.com/images/entry-button/button-only.gif" alt="この記事をはてなブックマークに追加" width="20" height="20" style="border: none;" /></a> </div> <div class="social-button-item"> <div class="fb-share-button" data-layout="box_count" data-href="https://kazuhira-r.hatenablog.com/entry/2023/06/04/231200"></div> </div> <div class="social-button-item"> <a class="entry-share-button entry-share-button-twitter test-share-button-twitter" href="https://x.com/intent/tweet?text=Spring+Framework%E3%81%AETransactionSynchronization%E3%82%92%E8%A9%A6%E3%81%99+-+CLOVER%F0%9F%8D%80&url=https%3A%2F%2Fkazuhira-r.hatenablog.com%2Fentry%2F2023%2F06%2F04%2F231200" title="X(Twitter)で投稿する" ></a> </div> </div> <div class="google-afc-image test-google-rectangle-ads"> <div id="google_afc_user_container_0" class="google-afc-user-container google_afc_blocklink2_5 google_afc_boder" data-test-unit="/4374287/blog_user"></div> <a href="http://blog.hatena.ne.jp/guide/pro" class="open-pro-modal" data-guide-pro-modal-ad-url="https://hatena.blog/guide/pro/modal/ad">広告を非表示にする</a> </div> <div class="customized-footer"> <div class="entry-footer-modules" id="entry-footer-secondary-modules"> <div class="hatena-module hatena-module-related-entries" > <!-- Hatena-Epic-has-related-entries-with-elasticsearch:true --> <div class="hatena-module-title"> 関連記事 </div> <div class="hatena-module-body"> <ul class="related-entries hatena-urllist urllist-with-thumbnails"> <li class="urllist-item related-entries-item"> <div class="urllist-item-inner related-entries-item-inner"> <div class="urllist-date-link related-entries-date-link"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/01/14" rel="nofollow"> <time datetime="2024-01-13T15:46:39Z" title="2024年1月14日"> 2024-01-14 </time> </a> </div> <a href="https://kazuhira-r.hatenablog.com/entry/2024/01/14/004639" class="urllist-title-link related-entries-title-link urllist-title related-entries-title">Spring Test × Database Riderで、データを作成する時にテーブル間の依存関係…</a> <div class="urllist-entry-body related-entries-entry-body">これは、なにをしたくて書いたもの? こちらのエントリーで、Da…</div> </div> </li> <li class="urllist-item related-entries-item"> <div class="urllist-item-inner related-entries-item-inner"> <div class="urllist-date-link related-entries-date-link"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/03/25" rel="nofollow"> <time datetime="2023-03-24T15:56:54Z" title="2023年3月25日"> 2023-03-25 </time> </a> </div> <a href="https://kazuhira-r.hatenablog.com/entry/2023/03/25/005654" class="urllist-title-link related-entries-title-link urllist-title related-entries-title">Spring TestのTestExecutionListenerを試す</a> <div class="urllist-entry-body related-entries-entry-body">これは、なにをしたくて書いたもの? タイトルどおり、Spring T…</div> </div> </li> <li class="urllist-item related-entries-item"> <div class="urllist-item-inner related-entries-item-inner"> <div class="urllist-date-link related-entries-date-link"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/05/27" rel="nofollow"> <time datetime="2022-05-26T17:07:41Z" title="2022年5月27日"> 2022-05-27 </time> </a> </div> <a href="https://kazuhira-r.hatenablog.com/entry/2022/05/27/020741" class="urllist-title-link related-entries-title-link urllist-title related-entries-title">Spring BatchのトランザクションがRuntimeExceptionでもException(…</a> <div class="urllist-entry-body related-entries-entry-body">これは、なにをしたくて書いたもの? Spring Frameworkでのトラ…</div> </div> </li> <li class="urllist-item related-entries-item"> <div class="urllist-item-inner related-entries-item-inner"> <div class="urllist-date-link related-entries-date-link"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/04/28" rel="nofollow"> <time datetime="2022-04-27T17:01:44Z" title="2022年4月28日"> 2022-04-28 </time> </a> </div> <a href="https://kazuhira-r.hatenablog.com/entry/2022/04/28/020144" class="urllist-title-link related-entries-title-link urllist-title related-entries-title">Spring Batchで、Bean ValidationとItemのスキップの動作を確認する</a> <div class="urllist-entry-body related-entries-entry-body">これは、なにをしたくて書いたもの? Spring BatchとBean Valid…</div> </div> </li> <li class="urllist-item related-entries-item"> <div class="urllist-item-inner related-entries-item-inner"> <div class="urllist-date-link related-entries-date-link"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/03/19" rel="nofollow"> <time datetime="2021-03-19T14:09:00Z" title="2021年3月19日"> 2021-03-19 </time> </a> </div> <a href="https://kazuhira-r.hatenablog.com/entry/2021/03/19/230900" class="urllist-title-link related-entries-title-link urllist-title related-entries-title">Spring Cloud Functionを試してみる</a> <div class="urllist-entry-body related-entries-entry-body">これは、なにをしたくて書いたもの? Spring Cloud Functionを…</div> </div> </li> </ul> </div> </div> </div> </div> <div class="comment-box js-comment-box"> <ul class="comment js-comment"> <li class="read-more-comments" style="display: none;"><a>もっと読む</a></li> </ul> <a class="leave-comment-title js-leave-comment-title">コメントを書く</a> </div> </footer> </div> </article> <!-- rakuten_ad_target_end --> <!-- google_ad_section_end --> <div class="pager pager-permalink permalink"> <span class="pager-prev"> <a href="https://kazuhira-r.hatenablog.com/entry/2023/06/10/162135" rel="prev"> <span class="pager-arrow">« </span> Ubuntu Linuxの各LTS(14.04〜22.04)で、… </a> </span> <span class="pager-next"> <a href="https://kazuhira-r.hatenablog.com/entry/2023/05/21/010059" rel="next"> Javaソースコードのアーキテクチャーをチ… <span class="pager-arrow"> »</span> </a> </span> </div> </div> </div> <aside id="box1"> <div id="box1-inner"> </div> </aside> </div><!-- #wrapper --> <aside id="box2"> <div id="box2-inner"> <div class="hatena-module hatena-module-profile"> <div class="hatena-module-title"> プロフィール </div> <div class="hatena-module-body"> <a href="https://kazuhira-r.hatenablog.com/about" class="profile-icon-link"> <img src="https://cdn.profile-image.st-hatena.com/users/Kazuhira/profile.png?1356877606" alt="id:Kazuhira" class="profile-icon" /> </a> <span class="id"> <a href="https://kazuhira-r.hatenablog.com/about" class="hatena-id-link"><span data-load-nickname="1" data-user-name="Kazuhira">id:Kazuhira</span></a> </span> <div class="profile-description"> <p>Sunday Programmer's Diary</p> </div> <div class="hatena-follow-button-box btn-subscribe js-hatena-follow-button-box" > <a href="#" class="hatena-follow-button js-hatena-follow-button"> <span class="subscribing"> <span class="foreground">読者です</span> <span class="background">読者をやめる</span> </span> <span class="unsubscribing" data-track-name="profile-widget-subscribe-button" data-track-once> <span class="foreground">読者になる</span> <span class="background">読者になる</span> </span> </a> <div class="subscription-count-box js-subscription-count-box"> <i></i> <u></u> <span class="subscription-count js-subscription-count"> </span> </div> </div> <div class="profile-about"> <a href="https://kazuhira-r.hatenablog.com/about">このブログについて</a> </div> </div> </div> <div class="hatena-module hatena-module-search-box"> <div class="hatena-module-title"> 検索 </div> <div class="hatena-module-body"> <form class="search-form" role="search" action="https://kazuhira-r.hatenablog.com/search" method="get"> <input type="text" name="q" class="search-module-input" value="" placeholder="記事を検索" required> <input type="submit" value="検索" class="search-module-button" /> </form> </div> </div> <div class="hatena-module hatena-module-links"> <div class="hatena-module-title"> リンク </div> <div class="hatena-module-body"> <ul class="hatena-urllist"> <li> <a href="https://hatena.blog/">はてなブログ</a> </li> <li> <a href="https://hatena.blog/guide?via=200109">ブログをはじめる</a> </li> <li> <a href="http://blog.hatenablog.com">週刊はてなブログ</a> </li> <li> <a href="https://hatena.blog/guide/pro">はてなブログPro</a> </li> </ul> </div> </div> <div class="hatena-module hatena-module-archive" data-archive-type="default" data-archive-url="https://kazuhira-r.hatenablog.com/archive"> <div class="hatena-module-title"> <a href="https://kazuhira-r.hatenablog.com/archive">月別アーカイブ</a> </div> <div class="hatena-module-body"> <ul class="hatena-urllist"> <li class="archive-module-year archive-module-year-hidden" data-year="2025"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2025" class="archive-module-year-title archive-module-year-2025"> 2025 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2025/03" class="archive-module-month-title archive-module-month-2025-3"> 2025 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2025/02" class="archive-module-month-title archive-module-month-2025-2"> 2025 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2025/01" class="archive-module-month-title archive-module-month-2025-1"> 2025 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2024"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2024" class="archive-module-year-title archive-module-year-2024"> 2024 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/12" class="archive-module-month-title archive-module-month-2024-12"> 2024 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/11" class="archive-module-month-title archive-module-month-2024-11"> 2024 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/10" class="archive-module-month-title archive-module-month-2024-10"> 2024 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/09" class="archive-module-month-title archive-module-month-2024-9"> 2024 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/08" class="archive-module-month-title archive-module-month-2024-8"> 2024 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/07" class="archive-module-month-title archive-module-month-2024-7"> 2024 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/06" class="archive-module-month-title archive-module-month-2024-6"> 2024 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/05" class="archive-module-month-title archive-module-month-2024-5"> 2024 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/04" class="archive-module-month-title archive-module-month-2024-4"> 2024 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/03" class="archive-module-month-title archive-module-month-2024-3"> 2024 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/02" class="archive-module-month-title archive-module-month-2024-2"> 2024 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2024/01" class="archive-module-month-title archive-module-month-2024-1"> 2024 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2023"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2023" class="archive-module-year-title archive-module-year-2023"> 2023 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/12" class="archive-module-month-title archive-module-month-2023-12"> 2023 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/11" class="archive-module-month-title archive-module-month-2023-11"> 2023 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/10" class="archive-module-month-title archive-module-month-2023-10"> 2023 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/09" class="archive-module-month-title archive-module-month-2023-9"> 2023 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/08" class="archive-module-month-title archive-module-month-2023-8"> 2023 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/07" class="archive-module-month-title archive-module-month-2023-7"> 2023 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/06" class="archive-module-month-title archive-module-month-2023-6"> 2023 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/05" class="archive-module-month-title archive-module-month-2023-5"> 2023 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/04" class="archive-module-month-title archive-module-month-2023-4"> 2023 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/03" class="archive-module-month-title archive-module-month-2023-3"> 2023 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/02" class="archive-module-month-title archive-module-month-2023-2"> 2023 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2023/01" class="archive-module-month-title archive-module-month-2023-1"> 2023 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2022"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2022" class="archive-module-year-title archive-module-year-2022"> 2022 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/12" class="archive-module-month-title archive-module-month-2022-12"> 2022 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/11" class="archive-module-month-title archive-module-month-2022-11"> 2022 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/10" class="archive-module-month-title archive-module-month-2022-10"> 2022 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/09" class="archive-module-month-title archive-module-month-2022-9"> 2022 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/08" class="archive-module-month-title archive-module-month-2022-8"> 2022 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/07" class="archive-module-month-title archive-module-month-2022-7"> 2022 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/06" class="archive-module-month-title archive-module-month-2022-6"> 2022 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/05" class="archive-module-month-title archive-module-month-2022-5"> 2022 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/04" class="archive-module-month-title archive-module-month-2022-4"> 2022 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/03" class="archive-module-month-title archive-module-month-2022-3"> 2022 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/02" class="archive-module-month-title archive-module-month-2022-2"> 2022 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2022/01" class="archive-module-month-title archive-module-month-2022-1"> 2022 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2021"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2021" class="archive-module-year-title archive-module-year-2021"> 2021 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/12" class="archive-module-month-title archive-module-month-2021-12"> 2021 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/11" class="archive-module-month-title archive-module-month-2021-11"> 2021 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/10" class="archive-module-month-title archive-module-month-2021-10"> 2021 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/09" class="archive-module-month-title archive-module-month-2021-9"> 2021 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/08" class="archive-module-month-title archive-module-month-2021-8"> 2021 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/07" class="archive-module-month-title archive-module-month-2021-7"> 2021 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/06" class="archive-module-month-title archive-module-month-2021-6"> 2021 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/05" class="archive-module-month-title archive-module-month-2021-5"> 2021 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/04" class="archive-module-month-title archive-module-month-2021-4"> 2021 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/03" class="archive-module-month-title archive-module-month-2021-3"> 2021 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/02" class="archive-module-month-title archive-module-month-2021-2"> 2021 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2021/01" class="archive-module-month-title archive-module-month-2021-1"> 2021 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2020"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2020" class="archive-module-year-title archive-module-year-2020"> 2020 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/12" class="archive-module-month-title archive-module-month-2020-12"> 2020 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/11" class="archive-module-month-title archive-module-month-2020-11"> 2020 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/10" class="archive-module-month-title archive-module-month-2020-10"> 2020 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/09" class="archive-module-month-title archive-module-month-2020-9"> 2020 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/08" class="archive-module-month-title archive-module-month-2020-8"> 2020 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/07" class="archive-module-month-title archive-module-month-2020-7"> 2020 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/06" class="archive-module-month-title archive-module-month-2020-6"> 2020 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/05" class="archive-module-month-title archive-module-month-2020-5"> 2020 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/04" class="archive-module-month-title archive-module-month-2020-4"> 2020 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/03" class="archive-module-month-title archive-module-month-2020-3"> 2020 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/02" class="archive-module-month-title archive-module-month-2020-2"> 2020 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2020/01" class="archive-module-month-title archive-module-month-2020-1"> 2020 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2019"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2019" class="archive-module-year-title archive-module-year-2019"> 2019 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/12" class="archive-module-month-title archive-module-month-2019-12"> 2019 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/11" class="archive-module-month-title archive-module-month-2019-11"> 2019 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/10" class="archive-module-month-title archive-module-month-2019-10"> 2019 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/09" class="archive-module-month-title archive-module-month-2019-9"> 2019 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/08" class="archive-module-month-title archive-module-month-2019-8"> 2019 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/07" class="archive-module-month-title archive-module-month-2019-7"> 2019 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/06" class="archive-module-month-title archive-module-month-2019-6"> 2019 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/05" class="archive-module-month-title archive-module-month-2019-5"> 2019 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/04" class="archive-module-month-title archive-module-month-2019-4"> 2019 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/03" class="archive-module-month-title archive-module-month-2019-3"> 2019 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/02" class="archive-module-month-title archive-module-month-2019-2"> 2019 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2019/01" class="archive-module-month-title archive-module-month-2019-1"> 2019 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2018"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2018" class="archive-module-year-title archive-module-year-2018"> 2018 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/12" class="archive-module-month-title archive-module-month-2018-12"> 2018 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/11" class="archive-module-month-title archive-module-month-2018-11"> 2018 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/10" class="archive-module-month-title archive-module-month-2018-10"> 2018 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/09" class="archive-module-month-title archive-module-month-2018-9"> 2018 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/08" class="archive-module-month-title archive-module-month-2018-8"> 2018 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/07" class="archive-module-month-title archive-module-month-2018-7"> 2018 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/06" class="archive-module-month-title archive-module-month-2018-6"> 2018 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/05" class="archive-module-month-title archive-module-month-2018-5"> 2018 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/04" class="archive-module-month-title archive-module-month-2018-4"> 2018 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/03" class="archive-module-month-title archive-module-month-2018-3"> 2018 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/02" class="archive-module-month-title archive-module-month-2018-2"> 2018 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2018/01" class="archive-module-month-title archive-module-month-2018-1"> 2018 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2017"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2017" class="archive-module-year-title archive-module-year-2017"> 2017 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/12" class="archive-module-month-title archive-module-month-2017-12"> 2017 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/11" class="archive-module-month-title archive-module-month-2017-11"> 2017 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/10" class="archive-module-month-title archive-module-month-2017-10"> 2017 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/09" class="archive-module-month-title archive-module-month-2017-9"> 2017 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/08" class="archive-module-month-title archive-module-month-2017-8"> 2017 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/07" class="archive-module-month-title archive-module-month-2017-7"> 2017 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/06" class="archive-module-month-title archive-module-month-2017-6"> 2017 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/05" class="archive-module-month-title archive-module-month-2017-5"> 2017 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/04" class="archive-module-month-title archive-module-month-2017-4"> 2017 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/03" class="archive-module-month-title archive-module-month-2017-3"> 2017 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/02" class="archive-module-month-title archive-module-month-2017-2"> 2017 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2017/01" class="archive-module-month-title archive-module-month-2017-1"> 2017 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2016"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2016" class="archive-module-year-title archive-module-year-2016"> 2016 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/12" class="archive-module-month-title archive-module-month-2016-12"> 2016 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/11" class="archive-module-month-title archive-module-month-2016-11"> 2016 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/10" class="archive-module-month-title archive-module-month-2016-10"> 2016 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/09" class="archive-module-month-title archive-module-month-2016-9"> 2016 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/08" class="archive-module-month-title archive-module-month-2016-8"> 2016 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/07" class="archive-module-month-title archive-module-month-2016-7"> 2016 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/06" class="archive-module-month-title archive-module-month-2016-6"> 2016 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/05" class="archive-module-month-title archive-module-month-2016-5"> 2016 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/04" class="archive-module-month-title archive-module-month-2016-4"> 2016 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/03" class="archive-module-month-title archive-module-month-2016-3"> 2016 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/02" class="archive-module-month-title archive-module-month-2016-2"> 2016 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2016/01" class="archive-module-month-title archive-module-month-2016-1"> 2016 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2015"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2015" class="archive-module-year-title archive-module-year-2015"> 2015 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/12" class="archive-module-month-title archive-module-month-2015-12"> 2015 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/11" class="archive-module-month-title archive-module-month-2015-11"> 2015 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/10" class="archive-module-month-title archive-module-month-2015-10"> 2015 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/09" class="archive-module-month-title archive-module-month-2015-9"> 2015 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/08" class="archive-module-month-title archive-module-month-2015-8"> 2015 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/07" class="archive-module-month-title archive-module-month-2015-7"> 2015 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/06" class="archive-module-month-title archive-module-month-2015-6"> 2015 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/05" class="archive-module-month-title archive-module-month-2015-5"> 2015 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/04" class="archive-module-month-title archive-module-month-2015-4"> 2015 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/03" class="archive-module-month-title archive-module-month-2015-3"> 2015 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/02" class="archive-module-month-title archive-module-month-2015-2"> 2015 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2015/01" class="archive-module-month-title archive-module-month-2015-1"> 2015 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2014"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2014" class="archive-module-year-title archive-module-year-2014"> 2014 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/12" class="archive-module-month-title archive-module-month-2014-12"> 2014 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/11" class="archive-module-month-title archive-module-month-2014-11"> 2014 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/10" class="archive-module-month-title archive-module-month-2014-10"> 2014 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/09" class="archive-module-month-title archive-module-month-2014-9"> 2014 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/08" class="archive-module-month-title archive-module-month-2014-8"> 2014 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/07" class="archive-module-month-title archive-module-month-2014-7"> 2014 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/06" class="archive-module-month-title archive-module-month-2014-6"> 2014 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/05" class="archive-module-month-title archive-module-month-2014-5"> 2014 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/04" class="archive-module-month-title archive-module-month-2014-4"> 2014 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/03" class="archive-module-month-title archive-module-month-2014-3"> 2014 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/02" class="archive-module-month-title archive-module-month-2014-2"> 2014 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2014/01" class="archive-module-month-title archive-module-month-2014-1"> 2014 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2013"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2013" class="archive-module-year-title archive-module-year-2013"> 2013 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/12" class="archive-module-month-title archive-module-month-2013-12"> 2013 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/11" class="archive-module-month-title archive-module-month-2013-11"> 2013 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/10" class="archive-module-month-title archive-module-month-2013-10"> 2013 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/09" class="archive-module-month-title archive-module-month-2013-9"> 2013 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/08" class="archive-module-month-title archive-module-month-2013-8"> 2013 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/07" class="archive-module-month-title archive-module-month-2013-7"> 2013 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/06" class="archive-module-month-title archive-module-month-2013-6"> 2013 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/05" class="archive-module-month-title archive-module-month-2013-5"> 2013 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/04" class="archive-module-month-title archive-module-month-2013-4"> 2013 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/03" class="archive-module-month-title archive-module-month-2013-3"> 2013 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/02" class="archive-module-month-title archive-module-month-2013-2"> 2013 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2013/01" class="archive-module-month-title archive-module-month-2013-1"> 2013 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2012"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2012" class="archive-module-year-title archive-module-year-2012"> 2012 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/12" class="archive-module-month-title archive-module-month-2012-12"> 2012 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/11" class="archive-module-month-title archive-module-month-2012-11"> 2012 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/10" class="archive-module-month-title archive-module-month-2012-10"> 2012 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/09" class="archive-module-month-title archive-module-month-2012-9"> 2012 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/08" class="archive-module-month-title archive-module-month-2012-8"> 2012 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/07" class="archive-module-month-title archive-module-month-2012-7"> 2012 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/06" class="archive-module-month-title archive-module-month-2012-6"> 2012 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/05" class="archive-module-month-title archive-module-month-2012-5"> 2012 / 5 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/04" class="archive-module-month-title archive-module-month-2012-4"> 2012 / 4 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/03" class="archive-module-month-title archive-module-month-2012-3"> 2012 / 3 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/02" class="archive-module-month-title archive-module-month-2012-2"> 2012 / 2 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2012/01" class="archive-module-month-title archive-module-month-2012-1"> 2012 / 1 </a> </li> </ul> </li> <li class="archive-module-year archive-module-year-hidden" data-year="2011"> <div class="archive-module-button"> <span class="archive-module-hide-button">▼</span> <span class="archive-module-show-button">▶</span> </div> <a href="https://kazuhira-r.hatenablog.com/archive/2011" class="archive-module-year-title archive-module-year-2011"> 2011 </a> <ul class="archive-module-months"> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/12" class="archive-module-month-title archive-module-month-2011-12"> 2011 / 12 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/11" class="archive-module-month-title archive-module-month-2011-11"> 2011 / 11 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/10" class="archive-module-month-title archive-module-month-2011-10"> 2011 / 10 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/09" class="archive-module-month-title archive-module-month-2011-9"> 2011 / 9 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/08" class="archive-module-month-title archive-module-month-2011-8"> 2011 / 8 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/07" class="archive-module-month-title archive-module-month-2011-7"> 2011 / 7 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/06" class="archive-module-month-title archive-module-month-2011-6"> 2011 / 6 </a> </li> <li class="archive-module-month"> <a href="https://kazuhira-r.hatenablog.com/archive/2011/05" class="archive-module-month-title archive-module-month-2011-5"> 2011 / 5 </a> </li> </ul> </li> </ul> </div> </div> </div> </aside> </div> </div> </div> </div> <footer id="footer" data-brand="hatenablog"> <div id="footer-inner"> <div style="display:none !important" class="guest-footer js-guide-register test-blogs-register-guide" data-action="guide-register"> <div class="guest-footer-content"> <h3>はてなブログをはじめよう!</h3> <p>Kazuhiraさんは、はてなブログを使っています。あなたもはてなブログをはじめてみませんか?</p> <div class="guest-footer-btn-container"> <div class="guest-footer-btn"> <a class="btn btn-register js-inherit-ga" href="https://blog.hatena.ne.jp/register?via=200227" target="_blank">はてなブログをはじめる(無料)</a> </div> <div class="guest-footer-btn"> <a href="https://hatena.blog/guide" target="_blank">はてなブログとは</a> </div> </div> </div> </div> <address class="footer-address"> <a href="https://kazuhira-r.hatenablog.com/"> <img src="https://cdn.blog.st-hatena.com/images/admin/blog-icon-noimage.png" width="16" height="16" alt="CLOVER🍀"/> <span class="footer-address-name">CLOVER🍀</span> </a> </address> <p class="services"> Powered by <a href="https://hatena.blog/">Hatena Blog</a> | <a href="https://blog.hatena.ne.jp/-/abuse_report?target_url=https%3A%2F%2Fkazuhira-r.hatenablog.com%2Fentry%2F2023%2F06%2F04%2F231200" class="report-abuse-link test-report-abuse-link" target="_blank">ブログを報告する</a> </p> </div> </footer> <script async src="https://s.hatena.ne.jp/js/widget/star.js"></script> <script> if (typeof window.Hatena === 'undefined') { window.Hatena = {}; } if (!Hatena.hasOwnProperty('Star')) { Hatena.Star = { VERSION: 2, }; } </script> <div id="fb-root"></div> <script>(function(d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) return; js = d.createElement(s); js.id = id; js.src = "//connect.facebook.net/ja_JP/sdk.js#xfbml=1&appId=719729204785177&version=v17.0"; fjs.parentNode.insertBefore(js, fjs); }(document, 'script', 'facebook-jssdk'));</script> <div class="quote-box"> <div class="tooltip-quote tooltip-quote-stock"> <i class="blogicon-quote" title="引用をストック"></i> </div> <div class="tooltip-quote tooltip-quote-tweet js-tooltip-quote-tweet"> <a class="js-tweet-quote" target="_blank" data-track-name="quote-tweet" data-track-once> <img src="https://cdn.blog.st-hatena.com/images/admin/quote/quote-x-icon.svg?version=f55a19e7a4f1ff17a5d764dab83046" title="引用して投稿する" > </a> </div> </div> <div class="quote-stock-panel" id="quote-stock-message-box" style="position: absolute; z-index: 3000"> <div class="message-box" id="quote-stock-succeeded-message" style="display: none"> <p>引用をストックしました</p> <button class="btn btn-primary" id="quote-stock-show-editor-button" data-track-name="curation-quote-edit-button">ストック一覧を見る</button> <button class="btn quote-stock-close-message-button">閉じる</button> </div> <div class="message-box" id="quote-login-required-message" style="display: none"> <p>引用するにはまずログインしてください</p> <button class="btn btn-primary" id="quote-login-button">ログイン</button> <button class="btn quote-stock-close-message-button">閉じる</button> </div> <div class="error-box" id="quote-stock-failed-message" style="display: none"> <p>引用をストックできませんでした。再度お試しください</p> <button class="btn quote-stock-close-message-button">閉じる</button> </div> <div class="error-box" id="unstockable-quote-message-box" style="display: none; position: absolute; z-index: 3000;"> <p>限定公開記事のため引用できません。</p> </div> </div> <script type="x-underscore-template" id="js-requote-button-template"> <div class="requote-button js-requote-button"> <button class="requote-button-btn tipsy-top" title="引用する"><i class="blogicon-quote"></i></button> </div> </script> <div id="hidden-subscribe-button" style="display: none;"> <div class="hatena-follow-button-box btn-subscribe js-hatena-follow-button-box" > <a href="#" class="hatena-follow-button js-hatena-follow-button"> <span class="subscribing"> <span class="foreground">読者です</span> <span class="background">読者をやめる</span> </span> <span class="unsubscribing" data-track-name="profile-widget-subscribe-button" data-track-once> <span class="foreground">読者になる</span> <span class="background">読者になる</span> </span> </a> <div class="subscription-count-box js-subscription-count-box"> <i></i> <u></u> <span class="subscription-count js-subscription-count"> </span> </div> </div> </div> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <script src="https://b.st-hatena.com/js/bookmark_button.js" charset="utf-8" async="async"></script> <script type="text/javascript" src="https://cdn.blog.st-hatena.com/js/external/jquery.min.js?v=1.12.4&version=f55a19e7a4f1ff17a5d764dab83046"></script> <script src="https://cdn.blog.st-hatena.com/js/texts-ja.js?version=f55a19e7a4f1ff17a5d764dab83046"></script> <script id="vendors-js" data-env="production" src="https://cdn.blog.st-hatena.com/js/vendors.js?version=f55a19e7a4f1ff17a5d764dab83046" crossorigin="anonymous"></script> <script id="hatenablog-js" data-env="production" src="https://cdn.blog.st-hatena.com/js/hatenablog.js?version=f55a19e7a4f1ff17a5d764dab83046" crossorigin="anonymous" data-page-id="entry"></script> <script>Hatena.Diary.GlobalHeader.init()</script> <script id="valve-dmp" data-service="blog" src="https://cdn.pool.st-hatena.com/valve/dmp.js" data-test-id="dmpjs" async></script> </body> </html>