0
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-24

時間がなかったため、以下の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

背景

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

Criteria APIでの実装方法

[最も基本的な実装方法]Custom DAOを定義し、EntityManagerをInjectする


@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を作る

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

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

@Repository
class BookRepositoryImpl implements BookRepositoryCustom {

    EntityManager em;

    // constructor

    @Override
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        // implementation
    }

}

EntityManagerCriteriaBuilder/CriteriaQueryを使った実装方法については上記と同じ。

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

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

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

例:

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

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

@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の定義は以下のようになっているので、

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

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

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を拡張する。

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

そうすると、builder patternを使いdescriptiveにクエリを定義することができる。
例:

bookRepository.findAll(hasAuthor(author));

Specification APIの注意

  • 上記のようなシンプルなCriteriaにしか対応していない
  • 例えば、Groupingやsubqueries、複数にまたがるEntityクラスのフェッチ(上記例であれば、BookEntity以外のJoinされたEntityなど) といった機能にはまだまだ対応していない
0
2
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
0
2