kotlinでJpaSpecificationExecutorを使い動的条件SQLを作成する
目次
JpaSpecificationExecutorの使いどころ
下記のような遊戯王のカード検索ができる画面があるとします。

カード名は「ヴァレル」から始まって、攻撃力3000かつ守備力2500のカードを検索したい場合、JpaSpecificationExecutorを使わないと複数のメソッドが必要になります。
・カード名をLIKE検索するメソッド
・カード名をLIKE検索し、かつ攻撃力で絞り込むメソッド
・カード名をLIKE検索し、かつ攻撃力と守備力で絞り込むメソッド
のように複数メソッドが必要になります。
上記画面のように検索したい項目数が多いとやってられません。
そこでJpaSpecificationExecutorで動的にクエリを作成します。
Repositoryの実装
1 2 3 |
@Repository interface DatasRepository : JpaRepository<DatasEntity, Int> , JpaSpecificationExecutor<DatasEntity> { } |
普段のJPAと異なるのはJpaSpecificationExecutor<hogeEntity>を継承していることです。他は同じでOKです。
Controllerの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Controller class SearchController { @Autowired lateinit var seachService: SearchService @RequestMapping("/card/search") fun search(@ModelAttribute form: SearchForm): String { // カード検索 var cards = seachService.getCardDatas(form) // 表示上限数100で絞り込む form.cardList = CardsDomain.filterCards(cards) return "index" } } |
よく見るControllerです。
“/card/search”にアクセスするとカードを検索してViewを表示します。
JpaSpecificationExecutorを使用した検索は
seachService.getCardDatas(form)で処理しています。
Serviceの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Service @Transactional(readOnly = true) class SearchService { @Autowired lateinit var datasRepository: DatasRepository fun getCardDatas(form: SearchForm): MutableList<DatasEntity> { return datasRepository.findAll( Specifications.where(CardSpecs.nameLike(form.name)) .and(CardSpecs.atkEquals(form.atk)) .and(CardSpecs.defEquals(form.def)) .and(CardSpecs.sumEquals(form.sum)) .and(CardSpecs.typeEquals(form)) ) } } |
RepositoryでJpaSpecificationExecutorを継承しているため
findAll(Specification<hogeEntity!>?)が使用可能になっています。
findAllの引数Specificationに各検索条件を設定します。
getCardDatas(form: SearchForm)は各条件をクリアしたデータを返すだけの単純なメソッドです。
Specification(検索条件)の実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
class CardSpecs { companion object { fun nameLike(name: String?): Specification<DatasEntity>? { // nullを返すとこの検索条件を無効にすることができる。 return if (name == null) null // カード名でLIKE検索する else Specification { root: Root<DatasEntity>, criteriaQuery, criteriaBuilder -> criteriaBuilder.like(root.get<String>("name"), "%$name%") } } fun atkEquals(atk: Long?): Specification<DatasEntity>? { // nullを返すとこの検索条件を無効にすることができる。 return if (atk == null) null // 攻撃力が等しいカードを取得 else Specification { root: Root<DatasEntity>, criteriaQuery, criteriaBuilder -> criteriaBuilder.equal(root.get<Long>("atk"), atk) } } fun defEquals(def: Long?): Specification<DatasEntity>? { // nullを返すとこの検索条件を無効にすることができる。 return if (def == null) null // 守備力が等しいカードを取得 else Specification { root: Root<DatasEntity>, criteriaQuery, criteriaBuilder -> criteriaBuilder.equal(root.get<Long>("def"), def) } } fun sumEquals(sum: Long?): Specification<DatasEntity>? { // nullを返すとこの検索条件を無効にすることができる。 return if (sum == null) null // 攻守合計が等しいカードを取得 else Specification { root: Root<DatasEntity>, criteriaQuery, criteriaBuilder -> criteriaBuilder.equal(root.get<Long>("sum"), sum) } } fun typeEquals(form: SearchForm): Specification<DatasEntity>? { // nullを返すとこの検索条件を無効にすることができる。 return if (form.type == null) null // カード種別が種別リストのいずれかに該当するカードを取得 else Specification { root: Root<DatasEntity>, criteriaQuery, criteriaBuilder -> root.get<Long>("type").`in`(form.getTypeList()) } } } } |
今回の肝の部分です。
各検索条件を作成しています。
大事なのはnullを返すことで検索条件を無効にできることです。
例えば最初の条件であるfun nameLike(name: String?)は
引数のnameがnullの場合、nullをreturnしています。
これによりカード名のLIKE検索は無効化されます。
他の条件が正常であれば、
・指定した攻撃力
・指定した守備力
・指定した攻守合計値
・指定した種別
のカードが検索されます。
nullを返すと検索条件が無効化できる仕様を使って
動的クエリが作成できるのです。
まとめ
JpaSpecificationExecutorはJavaだと色々な日本語資料があるのに、
Kotlinだと少ないため、備忘録としてまとめました。
ディスカッション
コメント一覧
まだ、コメントがありません