[baeldung/まとめ]Spring Data JPAで動的クエリを生成/実行する方法

時間がなかったため、以下のLINKを間違って理解してしまっていた。
https://www.baeldung.com/spring-data-criteria-queries
なので、自分用備忘録として理解した内容を忘れないようにメモ。

##まとめ##
- Custom DAO Repositoryを使い、`EntityManager`をInjectし、クエリを発行する
    - JPAなので、必ず JPA DAO Repository用マーキングアノテーション `@Repository`をつけること
- JAPがデフォルトでサポートしているJPARepositoryへCustom DAO Repositoryをアドオンさせて使うことができる
    - デフォルトのRepositoryへカスタム機能を自由に追加することができる
    - デフォルトのRepositoryが持つ機能を利用しながら、並行でカスタムDAO機能を利用することもできる
- Specification APIを利用するとランタイムクエリのランタイム追加条件(optionalになる場合など)をよりクリーンに実装することができる
    - ただシンプルなCriteriaしかサポートしていないので注意

##関連LINK##
- https://dzone.com/articles/add-custom-functionality-to-a-spring-data-reposito

##背景##
- Spring JPAではクエリを実装する方法が静的である。
    - 例：`＠Query` など
- ランタイムでのメソッドやパラメータ引数で動的に変更されるクエリに対応させるためのSolution
    - **Criteria API**を利用する


##Criteria APIでの実装方法##
**[最も基本的な実装方法]Custom DAOを定義し、`EntityManager`をInjectする**

```java

@Repository
class BookDao {
 
    @PersistenceContext
    EntityManager em;
 
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Book> cq = cb.createQuery(Book.class);
 
        Root<Book> book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);
 
        TypedQuery<Book> query = em.createQuery(cq);
        return query.getResultList();
    }
 
}
```
- baeldungのLINkには `@PersistenceContext` がなかったのだが、付けなくて良いのだろうか。。
- `@Repository` : Spring applicationでDAOオブジェクトとして利用したいクラスにつけるアノテーション
- 実際にはコードの流れの通りだが、、
  - `CriteriaBuilder`を取得する
  - 取得した `CreateBuilder`より `CriteriaQuery<Book>`オブジェクトを作成する
    - ここで `Book`というオブジェクトをクエリの戻り値として欲しい旨を指定する
    - `CriteriaQuery<Book>` からクエリ指定のエントリーポイントとなるオブジェクト `Root<Book>` を取得する
        - 上記例では 変数：`book`として参照させる
    - `CriteriaBuilder` と クエリエントリーオブジェクト(``Root<Book>`)を利用して、`Predicate`を作成する(一つのSearch Criteria Objectのようなもの?)
        - 上記例では `auther=%AUTHER%` と `title like %TITLE%` に対応するCriteriaを作った
    - 作成後`CriteriaQuery`へセットする
        - `CriteriaQuery.CriteriaQuery.where(Predicate…) `
            - **ここで各Search CriteriaはSQLとバインドされる**
            - **複数のPredicateが入力された場合、入力順から`AND`結合されていく**

---
上記の方法でもOKなのだが、Spring JPAがサポートしているデフォルトのCRUD Repositoryへアドオンさせて使うことも可能 :thumbsup:

###Extending Repository with Custom Methods###
まずはCustom Repository用のInterfaceを作る

```java
interface BookRepositoryCustom {
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}
```

そのCustom Repository Intefaceを実装したクラスを作り、`@Repository`アノテーションをつける

```java
@Repository
class BookRepositoryImpl implements BookRepositoryCustom {
 
    EntityManager em;
 
    // constructor
 
    @Override
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        // implementation
    }
 
}
```
`EntityManager`や`CriteriaBuilder`/`CriteriaQuery`を使った実装方法については上記と同じ。

通常の `JpaRepository`をimplementsしたRepository Interfaceを作成する、が **カスタムレポジトリインタフェース(`BookRepositoryCustom`)も一緒にimplementsするようにする。**

```java
interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}
```

あとは通常通り、`BookRepository`をinjectするようにすれば、カスタム実装を含んだJPARepositoryが利用できるようになる。

例：

```java
@RestController
public class TestController {
    @Autowired
    private BookRepository bookRepository;
    @RequestMapping("/book")
    public List<Book> getTest() {
        return bookRepository.findBooksByAuthorNameAndTitle("auther", "title");
    }
}
```

---
さらに、実際のプロジェクトなどでは、全部が全部のパラメータがmandatoryということはなく、各クエリパラメータがOptinalになるということはよくあるだろう。
上記の実装を踏襲すると、`If/Else` で切り分けるしかなさそうなので、切り分けてみる。。。

```java
@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);
 
    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
    
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));
 
    return em.createQuery(cq).getResultList();
}
```

これくらいの例ならまだましだが、実際にもっと複雑になってくると、 
- **コードがMessy/スパゲティ**
- **デバッグがしずらくなる**
- **そもそもクリーンコードの概念としては 無用な IF/ELSEは避けるべきである**
という問題が発生してしまう。

それを防ぐために Spring JPAが提供する **JPA Specification API** を導入してみよう。

###Extending Repository with Custom Methods###

Specification APIを利用すると、簡単なCriteria (=Predicate Object)ならクラスコンポーネント化させて、Reusabilityをあげることができる。

例えば、Specification interfaceの定義は以下のようになっているので、

```java
interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}
```

無名Specification実装クラスをリターンさせるメソッドを作る。

```java
static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
 
static Specification<Book> titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}
```

**こうして作成したSpecificationを`JPARepository`へ組み込ませれば、上記のようなIF/Elseを使わずにOptinalなランタイムクエリをクリーンに実装できる。**

`org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>` を実装するようにRepositoryを拡張する。

```java
interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book>, BookRepositoryCustom {} 
```
そうすると、builder patternを使いdescriptiveにクエリを定義することができる。
例：

```java
bookRepository.findAll(hasAuthor(author));
```
###Specification APIの注意###
- 上記のようなシンプルなCriteriaにしか対応していない
- 例えば、Groupingやsubqueries、複数にまたがるEntityクラスのフェッチ(上記例であれば、`Book`Entity以外のJoinされたEntityなど) といった機能にはまだまだ対応していない