5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-02

概要

  • 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 設定ファイル。

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 のデバッグログ等を出力するように指定。

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

エンティティ。

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アクセス用メソッドを生成するリポジトリ。

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

サービスクラス。

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 レスポンスを定義したクラス。

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

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

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 の機能 - 公式ドキュメントの日本語訳

デフォルトでは、組み込みデータベース(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 「使い方」ガイド - 公式ドキュメントの日本語訳

テーブル作成を制御したい場合は 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

参考資料

5
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?