kotlinでJpaSpecificationExecutorを使い動的条件SQLを作成する
JpaSpecificationExecutorの使いどころ
下記のような遊戯王のカード検索ができる画面があるとします。
カード名は「ヴァレル」から始まって、攻撃力3000かつ守備力2500のカードを検索したい場合、JpaSpecificationExecutorを使わないと複数のメソッドが必要になります。
・カード名をLIKE検索するメソッド
・カード名をLIKE検索し、かつ攻撃力で絞り込むメソッド
・カード名をLIKE検索し、かつ攻撃力と守備力で絞り込むメソッド
のように複数メソッドが必要になります。
上記画面のように検索したい項目数が多いとやってられません。
そこでJpaSpecificationExecutorで動的にクエリを作成します。
Repositoryの実装
@Repository
interface DatasRepository : JpaRepository<DatasEntity, Int> , JpaSpecificationExecutor<DatasEntity> {
}
普段のJPAと異なるのはJpaSpecificationExecutor<hogeEntity>を継承していることです。他は同じでOKです。
Controllerの実装
@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の実装
@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(検索条件)の実装
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だと少ないため、備忘録としてまとめました。
ディスカッション
コメント一覧
まだ、コメントがありません