Spring Boot + Spring Data JPA にて getOne, findById, クエリメソッドの挙動を確認する

## 概要

- Spring Boot + Spring Data JPA にて JpaRepository#getOne, CrudRepository#findById, クエリメソッド HogeRepository#findByFoo の挙動を確認する

## メソッドの説明

### JpaRepository#getOne

JpaRepository#getOne は遅延取得 (laze fetch) するメソッド。

[JpaRepository \(Spring Data JPA 2\.2\.7\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring-data/jpa/docs/2.2.7.RELEASE/api/org/springframework/data/jpa/repository/JpaRepository.html#getOne-ID-)

> T getOne(ID id)
> 指定された識別子を持つエンティティへの参照を返します。JPA 永続性プロバイダーの実装方法によっては、これは常にインスタンスを返し、最初のアクセスで EntityNotFoundException をスローする可能性が非常に高くなります。それらのいくつかは、無効な識別子をすぐに拒否します。
> 
> パラメーター :
> id - null であってはなりません。
> 
> 戻り値 :
> 指定された識別子を持つエンティティへの参照。
> 
> 関連事項 :
> for details on when an exception is thrown.

JpaRepository#getOne は EntityManager#getReference を呼び出している。

[EntityManager \(Java\(TM\) EE 8 Specification APIs\)](https://javaee.github.io/javaee-spec/javadocs/javax/persistence/EntityManager.html#getReference-java.lang.Class-java.lang.Object-)

> <T> T getReference(Class<T> entityClass, Object primaryKey)
> Get an instance, whose state may be lazily fetched.

### CrudRepository#findById

[CrudRepository \(Spring Data Core 2\.2\.7\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring-data/commons/docs/2.2.7.RELEASE/api/org/springframework/data/repository/CrudRepository.html#findById-ID-)

> Optional<T> findById(ID id)
> ID でエンティティを取得します。
> 
> パラメーター :
> id - null であってはなりません。
> 
> 戻り値 :
> 指定された ID を持つエンティティ、または見つからない場合は Optional#empty()
> 
> 例外 :
> IllegalArgumentException - id が null の場合。

### HogeRepository#findByFoo

今回作成する Hoge エンティティクラスの ID (主キー) である foo カラムの値を引数としてエンティティを取得するクエリメソッド。
クエリメソッドは Spring Data の命名規則に沿ったメソッド名を指定することで処理の実装が自動生成される仕組み。

[Spring Data JPA 5.3. クエリメソッド \- リファレンスドキュメント \- 日本語訳](https://spring.pleiades.io/spring-data/jpa/docs/2.2.7.RELEASE/reference/html/#jpa.query-methods)

> JPA モジュールは、クエリを文字列として手動で定義すること、またはメソッド名から派生させることをサポートしています。

[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#how-to-specify-query-mathodname-label)

> Spring Dataが定めた命名規約に則ったメソッド名にすることで実行するQuery(JPQL)を指定する。
> Spring Data JPAの機能によってメソッド名からJPQLが生成される。
> ただし、メソッド名からJPQLを作成できるのはSELECTのみで、UPDATEおよびDELETEのJPQLは生成できない。

## サンプルコード

### ソースコード一覧

```
├── build.gradle
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           ├── Hoge.java
        │           ├── HogeController.java
        │           ├── HogeRepository.java
        │           └── HogeService.java
        └── resources
            ├── application.properties
            └── data.sql
```

### build.gradle

```groovy
plugins {
  id 'org.springframework.boot' version '2.2.7.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 {
  // Spring
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  // Lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  // H2 Database
  runtimeOnly 'com.h2database:h2'
}
```

### settings.gradle

```groovy
rootProject.name = 'my-app'
```

### src/main/java/com/example/Hoge.java 

```java
package com.example;

import lombok.Data;

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

/**
 * DB テーブルの1レコード分に相当。
 */
@Data // Lombok で getter setter など便利なメソッドを自動生成
@Entity // JPA エンティティとして扱う
public class Hoge {

  @Id // JPA にこの変数をオブジェクトの ID (主キー) だと認識させる
  private String foo;

  private String bar;
}
```

### src/main/java/com/example/HogeRepository.java

```java
package com.example;

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

/**
 * DB アクセス用リポジトリ。
 * Spring Data JPA が標準で提供するメソッドが自動生成される。
 */
@Repository
public interface HogeRepository extends JpaRepository<Hoge, String> { // エンティティと主キーの型を指定

  // Spring Data JPA の命名規則に沿ったクエリメソッドを定義
  // 中身が自動生成される
  public Hoge findByFoo(String foo);
}
```

### src/main/java/com/example/HogeService.java

サービスクラス。リポジトリのメソッドをコールする。

```java
package com.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@Transactional
@Slf4j // org.slf4j.Logger 型の static final 変数 log を自動生成
public class HogeService {

  @Autowired
  private HogeRepository repository;

  public Hoge getOne(String id) {
    // JpaRepository#getOne
    log.debug("Before: JpaRepository#getOne");
    Hoge hoge = repository.getOne(id);
    log.debug("After: JpaRepository#getOne");
    return hoge;
  }

  public Hoge findById(String id) {
    // CrudRepository#findById
    log.debug("Before: CrudRepository#findById");
    Optional<Hoge> opt = repository.findById(id);
    log.debug("After: CrudRepository#findById");
    return opt.orElseThrow();
  }

  public Hoge findByFoo(String foo) {
    // HogeRepository#findByFoo
    log.debug("Before: HogeRepository#findByFoo");
    Hoge hoge = repository.findByFoo(foo);
    log.debug("After: HogeRepository#findByFoo");
    return hoge;
  }
}
```

### src/main/java/com/example/HogeController.java

コントローラークラス。サービスのメソッドをコールする。

```java
package com.example;

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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

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

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

  @Autowired
  private HogeService service;

  @GetMapping("/getOne/{foo}")
  public Map getOne(@PathVariable("foo") String foo) {

    // エンティティを取得
    log.debug("Before: HogeService#getOne");
    Hoge hoge = service.getOne(foo);
    log.debug("After: HogeService#getOne");

    // エンティティから値を取り出す
    log.debug("Before: Hoge#getFoo, Hoge#getBar");
    Map result = Map.of(hoge.getFoo(), hoge.getBar());
    log.debug("After: Hoge#getFoo, Hoge#getBar");
    return result;
  }

  @GetMapping("/findById/{foo}")
  public Map findById(@PathVariable("foo") String foo) {

    // エンティティを取得
    log.debug("Before: HogeService#findById");
    Hoge hoge = service.findById(foo);
    log.debug("After: HogeService#findById");

    // エンティティから値を取り出す
    log.debug("Before: Hoge#getFoo, Hoge#getBar");
    Map result = Map.of(hoge.getFoo(), hoge.getBar());
    log.debug("After: Hoge#getFoo, Hoge#getBar");
    return result;
  }

  @GetMapping("/findByFoo/{foo}")
  public Map findByFoo(@PathVariable("foo") String foo) {

    // エンティティを取得
    log.debug("Before: HogeService#findByFoo");
    Hoge hoge = service.findByFoo(foo);
    log.debug("After: HogeService#findByFoo");

    // エンティティから値を取り出す
    log.debug("Before: Hoge#getFoo, Hoge#getBar");
    Map result = Map.of(hoge.getFoo(), hoge.getBar());
    log.debug("After: Hoge#getFoo, Hoge#getBar");
    return result;
  }
}
```

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

```application.properties
# 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.com.example=DEBUG

# Open EntityManager in View パターンを使う
spring.jpa.open-in-view=true
```

### src/main/resources/data.sql

```sql
-- 初期データを DB に追加
INSERT INTO hoge (foo, bar) VALUES ('myfoo', 'mybar');
```

## Spring Boot アプリケーションを起動

Java 11 (AdoptOpenJDK 11.0.7+10) + Gradle 6.4.1 で Spring Boot アプリケーションを起動する。

```
$ gradle bootRun

> Task :bootRun

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

起動時のログを見る。

Spring Boot 2.2.7 + Spring 5.2.6 が使われている。

```
com.example.HogeController               : Running with Spring Boot v2.2.7.RELEASE, Spring v5.2.6.RELEASE
```

Hibernate ORM core version 5.4.15.Final が使われている。

```
o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.15.Final
o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
```

エンティティクラスの定義から自動的にテーブルが作成される。

```
org.hibernate.SQL                        : create table hoge (foo varchar(255) not null, bar varchar(255), primary key (foo))
o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
```

## アクセスしてそれぞれのメソッドの挙動を見る

### JpaRepository#getOne

curl でアクセスする。

```
$ curl http://localhost:8080/getOne/myfoo
```

Spring Boot のログを確認する。

JpaRepository#getOne で Hoge エンティティを取得する際にではなく、取得した Hoge エンティティのフィールドにアクセスする際に select 文が発行されている (lazy fetch による遅延取得)。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
com.example.HogeController               : Before: HogeService#getOne
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1004472706<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.example.HogeService.getOne]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2c01e862]
com.example.HogeService                  : Before: JpaRepository#getOne
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1004472706<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
com.example.HogeService                  : After: JpaRepository#getOne
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1004472706<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
com.example.HogeController               : After: HogeService#getOne
com.example.HogeController               : Before: Hoge#getFoo, Hoge#getBar
org.hibernate.SQL                        : select hoge0_.foo as foo1_0_0_, hoge0_.bar as bar2_0_0_ from hoge hoge0_ where hoge0_.foo=?
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [myfoo]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([bar2_0_0_] : [VARCHAR]) - [mybar]
com.example.HogeController               : After: Hoge#getFoo, Hoge#getBar
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

### CrudRepository#findById

curl でアクセスする。

```
$ curl http://localhost:8080/findById/myfoo
```

Spring Boot のログを確認する。

CrudRepository#findById で Hoge エンティティを取得する際に select 文が発行されている。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
com.example.HogeController               : Before: HogeService#findById
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(794705340<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.example.HogeService.findById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2199c627]
com.example.HogeService                  : Before: CrudRepository#findById
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(794705340<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
org.hibernate.SQL                        : select hoge0_.foo as foo1_0_0_, hoge0_.bar as bar2_0_0_ from hoge hoge0_ where hoge0_.foo=?
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [myfoo]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([bar2_0_0_] : [VARCHAR]) - [mybar]
com.example.HogeService                  : After: CrudRepository#findById
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(794705340<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
com.example.HogeController               : After: HogeService#findById
com.example.HogeController               : Before: Hoge#getFoo, Hoge#getBar
com.example.HogeController               : After: Hoge#getFoo, Hoge#getBar
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

### HogeRepository#findByFoo

curl でアクセスする。

```
$ curl http://localhost:8080/findByFoo/myfoo
```

Spring Boot のログを確認する。

HogeRepository#findByFoo で Hoge エンティティを取得する際に select 文が発行されている。

```
o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
com.example.HogeController               : Before: HogeService#findByFoo
o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1242780251<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.example.HogeService.findByFoo]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@44d4b569]
com.example.HogeService                  : Before: HogeRepository#findByFoo
org.hibernate.SQL                        : select hoge0_.foo as foo1_0_, hoge0_.bar as bar2_0_ from hoge hoge0_ where hoge0_.foo=?
o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [myfoo]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([foo1_0_] : [VARCHAR]) - [myfoo]
o.h.type.descriptor.sql.BasicExtractor   : extracted value ([bar2_0_] : [VARCHAR]) - [mybar]
com.example.HogeService                  : After: HogeRepository#findByFoo
o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1242780251<open>)]
o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
com.example.HogeController               : After: HogeService#findByFoo
com.example.HogeController               : Before: Hoge#getFoo, Hoge#getBar
com.example.HogeController               : After: Hoge#getFoo, Hoge#getBar
o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
```

## 参考資料

- [Difference between getOne and findById in Spring Data JPA?](https://www.javacodemonk.com/difference-between-getone-and-findbyid-in-spring-data-jpa-3a96c3ff)
- [When use getOne and findOne methods Spring Data JPA \- Stack Overflow](https://stackoverflow.com/questions/24482117/when-use-getone-and-findone-methods-spring-data-jpa)