« Spring Batchで複数ファイルをItemReaderに使う | トップページ | WicketとSpringを悪魔合体させる その1 »

2020年4月28日 (火)

Spring + Thymeleafで検索 + ページング + ソートを同時にやる件

0. よく見るアレを自力で書く

画面上半分に検索用の入力値を入れる部分があり、下半分に検索結果を表示するテーブルとページネーションバーが置かれている、ギョーミーアプリではよく見るアレを自力で書いたことがなかったので、その時のメモを。
もう何番煎じなのかわからないけど、多分に自分用メモである。

1. 動きの仕様的なもの

ざっとこんな感じか。

  • 検索項目はPOSTで受け付ける
  • GETにクエリパラメータをぶら下げても検索できる
  • ページング処理はGETで行う

2. 各所の要点的なもの

今回は、総務省が公開している全国地方公共団体コードのExcelファイルをもとに、自治体コードと自治体名を検索表示するSpring Bootアプリケーションをネタにしてみた。

なお、ソースコード全体はこちら

2-1. 検索について

検索項目はPOSTで受け付けるがGETにクエリパラメータをぶら下げても検索できるということなので、Controllerはこうなる。

    @PostMapping("/search")
    fun search(model: Model, @PageableDefault pageable: Pageable, condition: Condition?): String {
        // クエリパラメータを組み立てる
        val b: StringBuilder = StringBuilder("redirect:/?page=${pageable.pageNumber}")

        if (condition != null) {
            if (StringUtils.hasLength(condition.cityCode)) {
                b.append("&cityCode=${condition.encodedCityCode()}")
            }
            if (StringUtils.hasLength(condition.cityName)) {
                b.append("&cityName=${condition.encodedCityName()}")
            }
        }

        return b.toString()
    }

ちと汚いが、POSTで受け付けたフォームデータをクエリパラメータに組みなおしてGETエンドポイントにリダイレクトしている。いわゆるPRG(POST-REDIRECT-GET)パターンである。

肝心要の検索処理はGETエンドポイント側で行う。

    @GetMapping("/")
    fun show(model: Model, @PageableDefault pageable: Pageable,
             @RequestParam(value = "cityCode", required = false) cityCode: String?,
             @RequestParam(value = "cityName", required = false) cityName: String?,
             @RequestParam(value = "sortItem", required = false) sortItem: String?,
             @RequestParam(value = "direction", required = false) direction: String?,
             @RequestParam(value = "sortDirection", required = false) sortDirection: String?): String {

        // 以下、クエリパラメータを一つずつ解析して検索にかける
        // :
        // :
        // 以下略

        return "/sorted"
    }

@RequestParamアノテーションで受け付けるクエリパラメータを定義するのだけど、required = falseをつけて渡されなくてもエラーとしないようにする。

また、ソートに関するパラメータもここで受け付ける。

2-2. ページングに関して

Springには、もともとページングに関する処理が入っていて、Repositoryにページ要求を渡すとページ情報付きで検索結果をもらうことができる。この辺りはSpring Web ページネーションあたりでググればいくらでも記事がヒットする。

なので、Repositoryはこうなる。

@Repository
interface CityRepository: JpaRepository<City, String> {

    fun findCitiesByCityCodeIsLike(@Param("cityCode") cityCode: String, pageable: Pageable): Page<City>
    // その他いろいろ
    // :
  }

引数のPageableが検索時に使うページ要求、戻り値のPageがページデータになる。

画面のテンプレートエンジンには、戻り値のPageをそのまま張ればページネーションとして機能する。
のだが、今回はあえてそれ専用のPOJOを用意した。

View側になるテンプレートには、ページネーションバーに受け付けられるすべてのクエリパラメータを固めるようThymeleafのリンクURL式を書いてやる。このあたりはほぼhttps://blog1.mammb.com/entry/2018/06/04/225310そのまんまを拝借させてもらった。

2-3. ソートに関して

ソートもページネーションの応用でいける。

    <table class="table table-bordered">
        <thead>
        <tr>
            <th class="text-center">
                <a href="#" th:if="${sortItem} eq 'cityCode'"
                   th:href="@{${url}(page=(${page.number} - 1), cityCode=${condition.cityCode}, cityName=${condition.cityName}, sortItem='cityCode', direction=${direction})}">
                    団体コード<span th:if="${sortItem} eq 'cityCode'"><span th:if="${direction} eq 'ASC'">▽</span><span th:if="${direction} eq 'DESC'">△</span></span>
                </a>
                <a href="#" th:unless="${sortItem} eq 'cityCode'"
                   th:href="@{${url}(page=(${page.number} - 1), cityCode=${condition.cityCode}, cityName=${condition.cityName}, sortItem='cityCode', direction='ASC')}">
                    団体コード
                </a>
            </th>
        </tr>
        </thead>
    :

前述のソートとあわせると、Controllerはこうなる。

    @GetMapping("/")
    fun show(model: Model, @PageableDefault pageable: Pageable,
             @RequestParam(value = "cityCode", required = false) cityCode: String?,
             @RequestParam(value = "cityName", required = false) cityName: String?,
             @RequestParam(value = "sortItem", required = false) sortItem: String?,
             @RequestParam(value = "direction", required = false) direction: String?,
             @RequestParam(value = "sortDirection", required = false) sortDirection: String?): String {

        // ソートとページ要求は割と密な関係なので、条件を同時に整理する
        var sort: Sort? = null
        if (StringUtils.hasLength(sortDirection)) {
            val sd = if (sortDirection == Sort.Direction.ASC.name) Sort.Direction.ASC.name else Sort.Direction.DESC.name
            val si: String
            if (StringUtils.hasLength(sortItem)) {
                si = sortItem!!
                model.addAttribute("sortItem", si)
            } else {
                si = "cityCode"
            }
            sort = Sort.by(Sort.Direction.fromString(sd), si)

            model.addAttribute("sortDirection", sd)
            model.addAttribute("direction", sd)
        } else if (StringUtils.hasLength(sortItem) && StringUtils.hasLength(direction)) {
            val dir = if (direction == Sort.Direction.DESC.name) Sort.Direction.ASC.name else Sort.Direction.DESC.name
            val searchDir = if (direction == Sort.Direction.DESC.name) Sort.Direction.DESC else Sort.Direction.ASC
            model.addAttribute("sortItem", sortItem)
            model.addAttribute("direction", dir)
            model.addAttribute("sortDirection", direction)
            val si = if (sortItem == "prefName") "pref.prefName" else sortItem
            sort = Sort.by(searchDir, si)
        }

        // ページ要求を作り直して検索
        val condition = Condition(cityCode, cityName)
        val p: Pageable = if (sort == null) {
            pageable
        } else {
            PageRequest.of(pageable.pageNumber, pageable.pageSize, sort)
        }
        val cities: Page<City> = citySearchService.searchCity(condition, p)
        val page = Pagination(cities)
        // クエリパラメータで渡された検索条件を画面のModelに張りなおす
        model.addAttribute("cityList", cities)
        model.addAttribute("page", page)
        model.addAttribute("condition", condition)

        return "/sorted"
    }

3. 最後に

受け付けるクエリパラメータを公開しておけば、トラブル対応もしやすいんじゃなかろうか。
まぁ、クエリパラメータがURLにいっぱいぶら下がっていると「URLが汚くなるから全部POSTにしたい」という人も中にはいそう(※あくまで個人の見解です)だけど。

それはそうと、ページネーションはサーバサイドではあまりコードを書かなくても実現できるけど、テンプレートエンジン側は結構めんどいことをやらないといけないのがつらい....。

« Spring Batchで複数ファイルをItemReaderに使う | トップページ | WicketとSpringを悪魔合体させる その1 »

コメント

コメントを書く

(ウェブ上には掲載しません)

« Spring Batchで複数ファイルをItemReaderに使う | トップページ | WicketとSpringを悪魔合体させる その1 »

2023年12月
          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            

最近のトラックバック

無料ブログはココログ