kotlinでJpaSpecificationExecutorを使い動的条件SQLを作成する

IT

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だと少ないため、備忘録としてまとめました。

IT

Posted by raishin