時間がなかったため、以下のLINKを間違って理解してしまっていた。
https://www.baeldung.com/spring-data-criteria-queries
なので、自分用備忘録として理解した内容を忘れないようにメモ。
##まとめ##
- Custom DAO Repositoryを使い、
EntityManager
をInjectし、クエリを発行する- JPAなので、必ず JPA DAO Repository用マーキングアノテーション
@Repository
をつけること
- JPAなので、必ず JPA DAO 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)を利用して、
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へアドオンさせて使うことも可能
###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
}
}
EntityManager
やCriteriaBuilder
/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クラスのフェッチ(上記例であれば、
Book
Entity以外のJoinされたEntityなど) といった機能にはまだまだ対応していない