kagamihogeの日記

kagamihogeの日記です。

spring-bootのspring-data-jpaでJTAにAtomikosを使用する

spring-bootでspring-data-jpaを使う場合JTAはHIkariCPが特に何も設定しなくても使われる。通常はこれで何ら問題は無いが以下ではJTAをAtomikosに変更するやり方のメモ。

pom.xml

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <java.version>10.0</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
    </dependencies>

現時点(2018/08/21)最新版の2.0.4.RELEASEだとspring-data-jpaが動作しなかったので2.0.3にしている。

javax.xml.bindについては ClassNotFoundException for javax.xml.bind.JAXBException with Spring Boot when swich to Java 9 - Stack Overflow を参照。

JTAをAtomikosに変更する場合は依存性にspring-boot-starter-jta-atomikosを追加するだけで良い。

プロパティ

application.propertiesで以下のように設定する。例えば、以下によりcom.atomikos.icatch.max_timeoutを変更できる。

spring.jta.atomikos.properties.max_timeout=312345

プロパティ一覧についてはAtomikosProperties (Spring Boot Docs 2.0.4.RELEASE API) を参照。

ソースコード

動作確認用のソースコード。HelloWorldレベルのことしかやってない。テーブルについては作ってあるもんとする。

package kagamihoge.springdatajpa;

import java.util.Optional;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Hello world!
 *
 */
@SpringBootApplication
public class App  implements CommandLineRunner{
    public static void main(String[] args) {
        SpringApplication.run(App.class, args).close();
    }
    
    @Autowired
    UserRepository userRepository;
    
    @Transactional
    @Override
    public void run(String... args) throws Exception {
        Optional<User> user = userRepository.findById(1L);
        System.out.println(user.get().getEmail());
    }
}
package kagamihoge.springdatajpa;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long userId;
    private String username;
    private String password;
    private String email;
    public Long getUserId() {
        return userId;
    }
    public void setUserId(Long userId) {
        this.userId = userId;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}
package kagamihoge.springdatajpa;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}
spring.datasource.url=jdbc:postgresql://192.168.10.23:5432/testdb
spring.datasource.username=postgres
spring.datasource.password=xxxx

テスト駆動開発

昔、自分がどのようにプログラミングをしているか、を文章に書き起こせるか、という話を同僚としたことがある。それで書いてみようとしたのだが、すぐに行き詰った。最大の壁は自分の思考を文章にすることで、文章は基本的に上から順に読む線形の構造になるが、プログラミング時の脳内の思考は(おそらく)そんな単純ではない。他にも、あまり簡単な例にすると一般化されないし、かといって理論に寄り過ぎると実用性が無い……などなど。考え始めると、何をどれだけ書けば良く分からなかったので適当なところで諦めた過去がある。

そこで本書だが、テスト駆動に関する本ではあるものの、実質的には、どのようにプログラミングをすると良いのか、を解説している。数個程度のクラスから構成される機能を作るとき、いかに取り組めば良いのか。あるいは、おおむね一日単位のプログラミングにどのように取り組めば最適なパフォーマンスを発揮できるのか。

正直なところ俺はかなり感動した。自分が感じた文章化の壁をアッサリ乗り越えていたからである。どうやってこんなに読みやすい上にこんなにコンパクトな分量で済んでいるのかずっと考えながら読んでいた。最終的に、テストコードを書いてから実装する、という極めてシンプルなコンセプトに立脚しているから、という結論になった。どのページを切り取ってもこのシンプルなコンセプトに基づいた解説になっている。金太郎飴のように。この統一感は本当に素晴らしい。

テスト駆動の有用性だが、本書を読んで改めて学んだのは、見える化と不安への対処だった。見える化については言うまでもないが、本書はプログラマの心理状態への言及がとても多い。不安が不安を呼び、それが個人のパフォーマンスに壊滅的な影響を及ぼす。これを本書ではかなり重視しており、それを飼いならすためにテスト駆動を上手く使おう、としている。

不安の対処に関する箇所も十分実用的だが、他のテクニック的な箇所も実用的なアドバイスが多い。例えばTODOリストの活用だが、これは自分もマッタク同じことをしていたので驚いた。

テスト駆動が万能かというと勿論そうではない。これは、あるスタイルが合う人合わない人がいるという程度の話ではない。本書 p.274「TDDは誰のためのものか TDDは『より良いコードを書けば、よりうまくいく』という素朴で奇妙な仮説によって成り立っている」とある。これの前段にはTDDはコピペプログラマには向かんよね、とサラリと書いてある*1

そんなに分厚い本ではないが、サンプルコードは一部Pythonなものの大半はJavaなので正味の文章量はそうでもない。たかだが300ページちょいの本でこれだけの密度の本が書けるものなのか、と感動した一冊でした。

テスト駆動開発

テスト駆動開発

*1:良いコードが良いビジネス上の成功に繋がるのか的な議論もあるだろうが、さすがに本書の内容からはズレるだろう

spring bootとthymeleafでfragmentsを使用してajaxをする

準備

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <java.version>10.0</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies>

コード

controller

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class FragmentAjaxSampleController {
    @GetMapping("/fragsample")
    public ModelAndView index(ModelAndView model) {
        model.addObject("now", LocalDateTime.now().toString() + " init");
        model.setViewName("fragmentsajax/sample");
        return model;
    }
    
    @GetMapping("/fragsample/frag")
    public String frag(Model model) {
        model.addAttribute("now", LocalDateTime.now().toString() + " frag");
        return "fragmentsajax/sample :: sample-fragment";
    }
    
    @GetMapping("/fragsample/frag-with-selector")
    public String frag002(Model model) {
        model.addAttribute("now", LocalDateTime.now().toString() + " frag-with-selector");
        return "fragmentsajax/sample :: sample-fragment-with-selector//div[id='frag2-sub']";
    }
}

html

src/main/resources/templates/fragmentsajax/sample.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>fragments ajax sample</title>
</head>
<body>
    <div>
        <a href="#" onclick="frag001Click();">ajax fragments</a><br />
        <a href="#" onclick="frag002Click();">ajax fragments-with-selector</a><br />
    </div>
    <div id="frag1" th:fragment="sample-fragment">
        <span th:text="${now}"></span>
    </div>
    
    <div id="frag2" th:fragment="sample-fragment-with-selector">
        <span th:text="${now}"></span> non ajax
        <div id="frag2-sub" class="class1">
            <span th:text="${now}"></span>
        </div>
    </div>
    
    <br />
</body>
<script type="text/javascript">
function frag001Click() {
   var frag1 = document.querySelector('div#frag1');
   
   fetch("/fragsample/frag", {
       method: "get"
   }).then(response => {
       return response.text();
   }).then(body => {
       frag1.outerHTML = body;
   });
}

function frag002Click() {
   var frag2sub = document.querySelector('div#frag2-sub');
   
   fetch("/fragsample/frag-with-selector", {
       method: "get"
   }).then(response => {
       return response.text();
   }).then(body => {
       frag2sub.outerHTML = body;
   });
}
</script>
</html>

説明

fragments + ajax

まず/fragsampleにアクセスすると上記のhtmlが表示される。

次にfrag001Click()をすると/fragsample/fragのコントローラーメソッドが呼ばれ、fragmentsajax/sample :: sample-fragmentを返している。fragmentsajax/sampleは上記のsample.htmlを指しており、その次の:: sample-fragmentはそのhtmlファイル内のth:fragment="sample-fragment"を指している。つまり<div id="frag1" ...の箇所だけを使ってレンダリングせよ、という意味になる。そのため/fragsample/fragにブラウザでアクセスすると次のようなhtmlの断片が返ってくる。

<div id="frag1">
        <span>2018-07-07T15:13:14.685116700 frag</span>
    </div>

最後に、このhtmlの断片をJavaScriptで適当に処理する。

fragments + dom selector + ajax

また、dom selectorでfragments内の要素を指定することもできる。

frag002Click();をすると対象のコントローラーメソッドはfragmentsajax/sample :: sample-fragment-with-selector//div[id='frag2-sub']を返している。これはsample-fragment-with-selectorというfragment内のdivタグでidがfrag2-subのもの、という意味になる。つまり<div id="frag2-sub" ...の箇所だけを使う、という意味になる。そのため/fragsample/frag-with-selectorにブラウザでアクセスすると次のようなhtmlの断片が返ってくる。

<div id="frag2-sub" class="class1">
            <span>2018-07-07T15:18:39.535689500 frag-with-selector</span>
        </div>

fragmentsの外部化

fragmentsの使用例で良く見かけるのはヘッダー・フッターなど共通部分を外部ファイル化することだが、この外部化したfragmentsを使用することもできる。使用例は省略。

<div id="frag001" th:insert="~{footer :: frag001}" class="hoge"></div>

ただし、複数画面に共通では無い、ある画面のajaxでしか使用しないfragmentsを外部化しても見通しが悪くなるだけだと思われる。

感想

この機能使ってるのを初めて見たときちょっと混乱した。というのも、ajaxだからjsonxmlを返してJavaScriptで云々してるだろう、という思い込みがあったためである。とはいえ、個人的には、一度理解してしまえばspringとthymeleafの世界に寄せられるのでまぁまぁ便利な機能ではないか、という感触がある。

参考リンク