Spring Boot + Spring Data JPA で宣言的トランザクションによるDB制御をするサンプルコード

## 概要

- Spring Boot + Spring Data JPA (Java Persistence API) を使用して @Transactional アノテーションによる宣言的トランザクションなデータベース制御をするサンプルコードを示す
- Spring Framework や Hibernate ORM のデバッグログ等を出力してトランザクション処理の流れ (JpaTransactionManager, EntityManager, コミット, ロールバック) を確認する

## 動作確認環境

- Java 11 (AdoptOpenJDK 11.0.6+10)
- Gradle 6.2.1
- Spring Boot 2.2.4
- Spring Data JPA 2.2.4
- Spring Web MVC 5.2.3
- Java Persistence API (Jakarta Persistence API 2.2.3)
- Hibernate ORM 5.4.10.Final
- H2 Database 1.4.200

## ソースコード

### ソースコード一覧

```
├── build.gradle
└── src
    └── main
        ├── java
        │   └── sample
        │       ├── Person.java
        │       ├── PersonRepository.java
        │       ├── PersonService.java
        │       ├── Response.java
        │       └── SampleController.java
        └── resources
            └── application.properties
```

### build.gradle

Gradle 設定ファイル。

```groovy
plugins {
  id 'org.springframework.boot' version '2.2.4.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}

group = 'com.example'
version = '0.0.1'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  // Lombok
  compileOnly 'org.projectlombok:lombok:1.18.12'
  annotationProcessor 'org.projectlombok:lombok:1.18.12'
  // Spring
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  // H2 Database
  runtimeOnly 'com.h2database:h2'
}
```

### src/main/resources/application.properties

Spring Boot 設定ファイル。
Spring Framework と Hibernate ORM のデバッグログ等を出力するように指定。

```properties
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.sample=DEBUG
```

### src/main/java/sample/Person.java

エンティティ。

```java
package sample;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDateTime;

/**
 * DBテーブルの1レコード分に相当。
 */
@Data // Lombok でゲッターセッターなど便利なメソッド等を自動生成
@Entity // JPA エンティティとして扱う
@Table(name = "person") // DBテーブル情報
public class Person {

  @Id // JPA にこの変数をオブジェクトのIDだと認識させる
  @GeneratedValue(strategy = GenerationType.IDENTITY) // ID自動生成
  @Column(name = "id", nullable = false) // DBテーブルのカラム情報
  @JsonProperty("id") // マッピングする JSON キー (名前)
  private Long id;

  @Column(name = "name", nullable = false)
  @JsonProperty("name")
  private String name;

  @Column(name = "created_at", nullable = false)
  @JsonProperty("created_at")
  private LocalDateTime createdAt;
}
```

### src/main/java/sample/PersonRepository.java

DBアクセス用メソッドを生成するリポジトリ。

```java
package sample;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * DBアクセス用リポジトリ。
 * 何も実装しなくても、Spring Data JPA が標準で提供するメソッドが自動生成される。
 */
@Repository
public interface PersonRepository extends JpaRepository<Person, Long> { // エンティティと主キーの型を指定
}
```

###  src/main/java/sample/PersonService.java

サービスクラス。

```java
package sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.security.SecureRandom;
import java.util.List;

/**
 * サービスクラス。
 * ここでDBトランザクションの管理をする。
 */
@Service
@Transactional // メソッド開始時にトランザクションを開始、終了時にコミットする
public class PersonService {

  @Autowired
  PersonRepository repository;

  public void add(Person person) {
    // データベースに格納
    repository.save(person);

    // 50%の確率で例外を発生させる。トランザクションマネージャが自動でロールバックしてくれる
    if (new SecureRandom().nextBoolean()) {
      throw new RuntimeException("ちゅどーん");
    }
  }

  public List<Person> getPersons() {
    return repository.findAll();
  }
}
```

### src/main/java/sample/Response.java

JSON レスポンスを定義したクラス。

```java
package sample;

import lombok.Builder;
import lombok.Getter;

import java.util.List;

/**
 * レスポンス用クラス。
 */
@Getter
@Builder
public class Response {

  private Result result; // 処理結果

  private List<Person> persons; // データ

  @Getter
  @Builder
  public static class Result {
    private String message; // 処理結果メッセージ
    private int count; // データ件数
  }
}
```

### src/main/java/sample/SampleController.java

コントローラークラス (今回はアプリケーションクラスも兼ねている)。

```java
package sample;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;

@SpringBootApplication
@RestController
@Slf4j // org.slf4j.Logger 型の static final 変数 log を自動生成
public class SampleController {

  public static void main(String[] args) {
    SpringApplication.run(SampleController.class, args);
  }

  @Autowired
  private PersonService service;

  // 指定した name のデータを追加する
  @RequestMapping("/add")
  public Response add(@RequestBody Person person) {

    log.debug("SampleController#add");

    String message = "success";
    try {
      person.setCreatedAt(LocalDateTime.now());
      log.debug("Before PersonService#add");
      // データを追加する
      service.add(person);
      log.debug("After PersonService#add");
    } catch (Exception e) {
      log.debug("PersonService#add threw an exception.");
      message = e.getMessage();
    }

    // 全データを取得
    log.debug("Before PersonService#getPersons");
    List<Person> persons = service.getPersons();
    log.debug("After PersonService#getPersons");

    // JSON レスポンスを生成
    return Response.builder()
      .result(
        Response.Result.builder()
          .message(message)
          .count(persons.size())
          .build())
      .persons(persons)
      .build();
  }
}
```

## 動作確認

curl でアクセスして、レスポンスとサーバ側ログを確認する。

### Spring Boot サーバを起動

```
$ gradle bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.4.RELEASE)
```

今回は H2 Database を使っているので、エンティティクラスの情報から自動でテーブルが作成される。

[Spring Boot の機能 \- 公式ドキュメントの日本語訳](https://spring.pleiades.io/spring-boot/docs/2.2.4.RELEASE/reference/html/spring-boot-features.html#boot-features-creating-and-dropping-jpa-databases)

> デフォルトでは、組み込みデータベース（H2、HSQL、またはDerby）を使用する場合にのみ、JPAデータベースが自動的に作成されます。

Spring Boot の起動時に、テーブル作成についての情報が org.hibernate.SQL の DEBUG ログで出力されている。

```
drop table person if exists
create table person (id bigint generated by default as identity, created_at timestamp not null, name varchar(255) not null, primary key (id))
```


[Spring Boot 「使い方」ガイド \- 公式ドキュメントの日本語訳](https://spring.pleiades.io/spring-boot/docs/2.2.4.RELEASE/reference/html/howto.html#howto-initialize-a-database-using-hibernate)

テーブル作成を制御したい場合は application.properties の spring.jpa.hibernate.ddl-auto で設定できる。

> 10.2. Hibernateを使用したデータベースの初期化
> spring.jpa.hibernate.ddl-autoを明示的に設定でき、標準のHibernateプロパティ値はnone、validate,update,createおよびcreate-dropです。

### 1回目のアクセス

curl でアクセスして正常にデータが追加された。1件のデータを返却。

```
$ curl -H 'Content-Type: application/json' -d '{"name": "ふー"}' http://localhost:8080/add
{"result":{"message":"success","count":1},"persons":[{"id":1,"name":"ふー","created_at":"2020-03-02T21:20:16.181886"}]}
```

サーバ側のログを確認。

JPA EntityManager をオープン。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
sample.SampleController                  : SampleController#add
```

データ追加処理。
トランザクション開始、insert、コミットの流れが確認できる。

```
sample.SampleController                  : Before PersonService#add
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1402599951<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.add]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@24b1000c]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1402599951<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : insert into person (id, created_at, name) values (null, ?, ?)
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [2020-03-02T21:20:16.181886]
o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [ふー]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1402599951<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : After PersonService#add
```

データ取得処理。
トランザクション開始、select、コミットの流れが確認できる。

```
sample.SampleController                  : Before PersonService#getPersons
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1402599951<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.getPersons]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@4cffd9f7]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1402599951<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : select person0_.id as id1_0_, person0_.created_at as created_2_0_, person0_.name as name3_0_ from person person0_
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_] : [BIGINT]) - [1]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1402599951<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : After PersonService#getPersons
```

JPA EntityManager をクローズ。

```
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

### 2回目のアクセス

データ追加時にエラーが発生。「ちゅどーん」メッセージと現在登録されている1件のデータを返却。

```
$ curl -H 'Content-Type: application/json' -d '{"name": "ばあ"}' http://localhost:8080/add
{"result":{"message":"ちゅどーん","count":1},"persons":[{"id":1,"name":"ふー","created_at":"2020-03-02T21:20:16.181886"}]}
```

サーバ側のログを確認。

JPA EntityManager をオープン。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
sample.SampleController                  : SampleController#add
```

データ追加処理。
トランザクション開始、insert、ロールバックの流れが確認できる。

```
sample.SampleController                  : Before PersonService#add
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(861038071<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.add]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3d958ded]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(861038071<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : insert into person (id, created_at, name) values (null, ?, ?)
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [2020-03-02T21:20:22.682108]
o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [ばあ]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback
o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(861038071<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : PersonService#add threw an exception.
```

データ取得処理。
トランザクション開始、select、コミットの流れが確認できる。

```
sample.SampleController                  : Before PersonService#getPersons
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(861038071<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.getPersons]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1a43c958]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(861038071<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : select person0_.id as id1_0_, person0_.created_at as created_2_0_, person0_.name as name3_0_ from person person0_
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_] : [BIGINT]) - [1]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([created_2_0_] : [TIMESTAMP]) - [2020-03-02T21:20:16.181886]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name3_0_] : [VARCHAR]) - [ふー]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(861038071<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : After PersonService#getPersons
```

JPA EntityManager をクローズ。

```
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

### 3回目のアクセス

正常にデータが追加された。現在登録されている2件のデータを返却。

```
$ curl -H 'Content-Type: application/json' -d '{"name": "ばず"}' http://localhost:8080/add
{"result":{"message":"success","count":2},"persons":[{"id":1,"name":"ふー","created_at":"2020-03-02T21:20:16.181886"},{"id":3,"name":"ばず","created_at":"2020-03-02T21:20:26.872843"}]}
```

サーバ側のログを確認。

JPA EntityManager をオープン。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
sample.SampleController                  : SampleController#add
```

データ追加処理。
トランザクション開始、insert、コミットの流れが確認できる。

```
sample.SampleController                  : Before PersonService#add
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(612825978<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.add]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2a41119a]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(612825978<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : insert into person (id, created_at, name) values (null, ?, ?)
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [2020-03-02T21:20:26.872843]
o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [ばず]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(612825978<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : After PersonService#add
```

データ取得処理。
トランザクション開始、select、コミットの流れが確認できる。

```
sample.SampleController                  : Before PersonService#getPersons
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(612825978<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [sample.PersonService.getPersons]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1b7df811]
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(612825978<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : select person0_.id as id1_0_, person0_.created_at as created_2_0_, person0_.name as name3_0_ from person person0_
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_] : [BIGINT]) - [1]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([created_2_0_] : [TIMESTAMP]) - [2020-03-02T21:20:16.181886]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name3_0_] : [VARCHAR]) - [ふー]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_] : [BIGINT]) - [3]
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(612825978<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
sample.SampleController                  : After PersonService#getPersons
```

JPA EntityManager をクローズ。

```
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

## 参考資料

- [Spring Boot 入門 \| JPA でデータアクセス \- コードサンプル](https://spring.pleiades.io/guides/gs/accessing-data-jpa/)
- [javax\.persistence \(Jakarta Persistence API documentation\)](https://jakarta.ee/specifications/persistence/2.2/apidocs/javax/persistence/package-summary.html)
- [Transactional \(Spring Framework 5\.2\.3\.RELEASE API\)](https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/javadoc-api/org/springframework/transaction/annotation/Transactional.html)
- [JpaTransactionManager \(Spring Framework 5\.2\.3\.RELEASE API\)](https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/javadoc-api/org/springframework/orm/jpa/JpaTransactionManager.html)
- [3\.2\. ドメイン層の実装 — TERASOLUNA Server Framework for Java \(5\.x\) Development Guideline 5\.5\.1\.RELEASE documentation](http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/ImplementationAtEachLayer/DomainLayer.html)
- [6\.3\. データベースアクセス（JPA編） — TERASOLUNA Server Framework for Java \(5\.x\) Development Guideline 5\.5\.1\.RELEASE documentation](http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/ArchitectureInDetail/DataAccessDetail/DataAccessJpa.html)
- [Hibernate ORM \- Topical Guides \- Logging Guide](https://docs.jboss.org/hibernate/orm/5.4/topical/html_single/logging/Logging.html)
- [Project Lombok \- @Log \(and friends\)](https://projectlombok.org/features/log)
- [Spring Boot で Spring JDBC を使う \- Qiita](https://qiita.com/niwasawa/items/bd97d4eb179fe5a7ec3f)