概要
- 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
参考資料
- Spring Boot 入門 | JPA でデータアクセス - コードサンプル
- javax.persistence (Jakarta Persistence API documentation)
- Transactional (Spring Framework 5.2.3.RELEASE API)
- JpaTransactionManager (Spring Framework 5.2.3.RELEASE API)
- 3.2. ドメイン層の実装 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.5.1.RELEASE documentation
- 6.3. データベースアクセス(JPA編) — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.5.1.RELEASE documentation
- Hibernate ORM - Topical Guides - Logging Guide
- Project Lombok - @Log (and friends)
- Spring Boot で Spring JDBC を使う - Qiita