【Spring Boot】並び替え・ソート機能を実装する|簡単な掲示板アプリの作成

Spring BootのWebアプリにデータを並び替える(ソート)機能を実装する方法を解説します。Spring Data JPAを活用し、掲示板の投稿リストを「作成日」や「タイトル」で昇順・降順にソートする実装方法を、画面操作からデータベース連携まで一貫して学べます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Java: OpenJDK 23
  • Spring Boot: 3.4.0
  • Lombok: 1.18.30
  • Thymeleaf Layout Dialect: 3.3.0
  • Spring Security: 6.1.1
  • Thymeleaf Extras Spring Security: 3.1.2.RELEASE
  • maven-compiler-plugin: 3.8.1

サンプルコード

/src/main/java/com/example/bbs/controller/PostController.java

/src/main/java/com/example/bbs/controller/PostController.java
1     @GetMapping
2     public String listPosts(
3             @RequestParam(value = "keyword", required = false) String keyword,
4-            @RequestParam(value = "matchType", required = false) String matchType,
5+            @RequestParam(value = "matchType", required = false, defaultValue = "contains") String matchType,
6+            @RequestParam(value = "sortBy", required = false, defaultValue = "createdAt") String sortBy,
7+            @RequestParam(value = "sortOrder", required = false, defaultValue = "asc") String sortOrder,
8             Model model) {
9 
10         // ログインユーザーを取得してモデルに渡す
11         // 検索フォームの入力値を保持
12         List<Post> posts;
13         if (keyword != null && !keyword.isEmpty() && matchType != null && !matchType.isEmpty()) {
14-            posts = postService.searchPosts(keyword, matchType);
15+            posts = postService.searchPosts(keyword, matchType, sortBy, sortOrder);
16         } else {
17-            posts = postService.findAll();
18+            posts = postService.findAll(sortBy, sortOrder);
19         }
20 
21+        // 検索結果をモデルに追加
22         model.addAttribute("posts", posts);
23 
24         return "posts/list";
25

/src/main/java/com/example/bbs/repository/PostRepository.java

/src/main/java/com/example/bbs/repository/PostRepository.java
1 import java.util.List;
2 
3 import org.springframework.data.jpa.repository.JpaRepository;
4+import org.springframework.data.domain.Sort;
5 
6 public interface PostRepository extends JpaRepository<Post, Long> {
7     // 部分一致
8-    List<Post> findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword);
9+    List<Post> findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword, Sort sort);
10 
11     // 前方一致
12-    List<Post> findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix);
13+    List<Post> findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix, Sort sort);
14 
15     // 後方一致
16-    List<Post> findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix);
17+    List<Post> findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix, Sort sort);
18 }
19

/src/main/java/com/example/bbs/service/PostService.java

/src/main/java/com/example/bbs/service/PostService.java
1 import com.example.bbs.repository.PostRepository;
2 import org.springframework.stereotype.Service;
3 
4+import org.springframework.data.domain.Sort;
5+
6 import java.util.List;
7 import java.util.Optional;
8 
9         this.postRepository = postRepository;
10     }
11 
12-    public List<Post> findAll() {
13-        return postRepository.findAll();
14+    public List<Post> findAll(String sortBy, String sortOrder) {
15+        // Sort.Orderを使ってソート順を設定
16+        Sort.Order order;
17+        if (sortOrder.equals("asc")) {
18+            order = new Sort.Order(Sort.Direction.ASC, sortBy);
19+        } else {
20+            order = new Sort.Order(Sort.Direction.DESC, sortBy);
21+        }
22+
23+        // Sortオブジェクトを作成
24+        Sort sort = Sort.by(order);
25+
26+        return postRepository.findAll(sort);
27     }
28 
29-    public List<Post> searchPosts(String keyword, String matchType) {
30+    public List<Post> searchPosts(String keyword, String matchType, String sortBy, String sortOrder) {
31+        // Sort.Orderを使ってソート順を設定
32+        Sort.Order order;
33+        if (sortOrder.equals("asc")) {
34+            order = new Sort.Order(Sort.Direction.ASC, sortBy); // 昇順
35+        } else {
36+            order = new Sort.Order(Sort.Direction.DESC, sortBy); // 降順
37+        }
38+
39+        // Sortオブジェクトを作成
40+        Sort sort = Sort.by(order);
41+
42+        // 検索条件に基づいて投稿を取得
43         switch (matchType) {
44             case "startswith": // 前方一致
45-                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword);
46+                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword, sort);
47             case "endswith": // 後方一致
48-                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword);
49+                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword, sort);
50             case "contains": // 部分一致(デフォルト)
51             default:
52-                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword);
53+                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword, sort);
54         }
55     }
56 
57

/src/main/resources/templates/posts/list.html

/src/main/resources/templates/posts/list.html
1     <!-- 検索フォーム -->
2     <form th:action="@{/posts}" method="get" class="mb-3">
3         <div class="row">
4-            <div class="col-md-6">
5+            <!-- 検索キーワード -->
6+            <div class="col-md-4">
7                 <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>
8             </div>
9+            <!-- マッチタイプ -->
10             <div class="col-md-2">
11                 <select name="matchType" class="form-control">
12                     <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>
13                     <option value="endswith" th:selected="${param.matchType?.toString() == 'endswith'}">後方一致</option>
14                 </select>
15             </div>
16+            <!-- ソート基準 -->
17+            <div class="col-md-2">
18+                <select name="sortBy" class="form-control">
19+                    <option value="createdAt" th:selected="${param.sortBy?.toString() == 'createdAt'}">作成日</option>
20+                    <option value="title" th:selected="${param.sortBy?.toString() == 'title'}">タイトル</option>
21+                </select>
22+            </div>
23+            <!-- 並び順 -->
24+            <div class="col-md-2">
25+                <select name="sortOrder" class="form-control">
26+                    <option value="asc" th:selected="${param.sortOrder?.toString() == 'asc'}">昇順</option>
27+                    <option value="desc" th:selected="${param.sortOrder?.toString() == 'desc'}">降順</option>
28+                </select>
29+            </div>
30+            <!-- 検索ボタン -->
31             <div class="col-md-2">
32                 <button type="submit" class="btn btn-primary">検索</button>
33             </div>
34         </div>
35     </form>
36-
37     <h2>掲示板一覧</h2>
38     <table class="table table-striped">
39         <thead>
40

コード解説

変更点: Controllerにソート条件のパラメータを追加

/src/main/java/com/example/bbs/controller/PostController.java
1     @GetMapping
2     public String listPosts(
3             @RequestParam(value = "keyword", required = false) String keyword,
4-            @RequestParam(value = "matchType", required = false) String matchType,
5+            @RequestParam(value = "matchType", required = false, defaultValue = "contains") String matchType,
6+            @RequestParam(value = "sortBy", required = false, defaultValue = "createdAt") String sortBy,
7+            @RequestParam(value = "sortOrder", required = false, defaultValue = "asc") String sortOrder,
8             Model model) {
9 

この変更では、PostControllerlistPosts メソッドに、新しく二つの @RequestParam アノテーションを追加しました。これは、Webブラウザから送られてくるHTTPリクエストのパラメータを受け取るための設定です。

  1. @RequestParam(value = "sortBy", required = false, defaultValue = "createdAt") String sortBy

    • value = "sortBy": HTTPリクエストの中で sortBy という名前のパラメータを探し、その値を取得します。例えば、URLが .../posts?sortBy=title の場合、sortBy 変数には "title" が格納されます。
    • required = false: このパラメータは必須ではないことを意味します。もし sortBy パラメータがリクエストに含まれていなくてもエラーになりません。
    • defaultValue = "createdAt": もし sortBy パラメータがリクエストに含まれていなかった場合、デフォルトの値として "createdAt"sortBy 変数に設定されます。これにより、どのカラムでソートするか指定がない場合は「作成日」でソートされるようになります。
  2. @RequestParam(value = "sortOrder", required = false, defaultValue = "asc") String sortOrder

    • value = "sortOrder": HTTPリクエストの中で sortOrder という名前のパラメータを探し、その値を取得します。例えば、URLが .../posts?sortOrder=desc の場合、sortOrder 変数には "desc" が格納されます。
    • required = false: このパラメータも必須ではありません。
    • defaultValue = "asc": もし sortOrder パラメータがリクエストに含まれていなかった場合、デフォルトの値として "asc" (昇順) が sortOrder 変数に設定されます。

これらの変更により、コントローラがソートの基準(sortBy)と並び順(sortOrder)をWebリクエストから受け取れるようになり、デフォルト値のおかげでパラメータが指定されない場合でも、作成日の昇順で表示されるようになります。また、matchType パラメータにもデフォルト値 "contains" が設定されました。

変更点: ControllerからServiceへのソート条件の引き渡し

/src/main/java/com/example/bbs/controller/PostController.java
1         // 検索フォームの入力値を保持
2         List<Post> posts;
3         if (keyword != null && !keyword.isEmpty() && matchType != null && !matchType.isEmpty()) {
4-            posts = postService.searchPosts(keyword, matchType);
5+            posts = postService.searchPosts(keyword, matchType, sortBy, sortOrder);
6         } else {
7-            posts = postService.findAll();
8+            posts = postService.findAll(sortBy, sortOrder);
9         }
10 
11+        // 検索結果をモデルに追加
12         model.addAttribute("posts", posts);
13 
14         return "posts/list";

この変更では、PostControllerlistPosts メソッド内で呼び出しているサービス層のメソッドに、ソート条件を示す sortBysortOrder の二つの引数を追加しました。

具体的には、以下の二つの箇所が変更されています。

  1. postService.searchPosts(keyword, matchType, sortBy, sortOrder);

    • ユーザーが検索キーワードを入力した場合に呼ばれる検索処理のメソッドです。これまでの keywordmatchType に加えて、どの基準で(sortBy)、どのように並べ替えるか(sortOrder)の情報をサービス層に渡すように変更しました。
  2. postService.findAll(sortBy, sortOrder);

    • 検索キーワードが入力されていない場合に、全ての投稿を取得するメソッドです。こちらも、新たに sortBysortOrder の情報をサービス層に渡し、取得する際にソートされるように変更しました。

このように、コントローラが受け取ったソート条件を、データの取得や加工を行うサービス層に渡すことで、実際にデータベースからソートされたデータを取得するための準備が整います。

変更点: RepositoryでのSortクラスのインポート

/src/main/java/com/example/bbs/repository/PostRepository.java
1 import java.util.List;
2 
3 import org.springframework.data.jpa.repository.JpaRepository;
4+import org.springframework.data.domain.Sort;
5 
6 public interface PostRepository extends JpaRepository<Post, Long> {

この変更では、PostRepository.java ファイルに org.springframework.data.domain.Sort クラスをインポートしました。

Sort クラスは、Spring Data JPA が提供する機能の一つで、データベースからデータを取得する際に、そのデータの並び順(ソート順)を動的に指定するために使います。

Repositoryインターフェースのメソッドに Sort オブジェクトを引数として渡すことで、SQLの ORDER BY 句に相当する処理をSpring Data JPAが自動的に生成し、ソートされた結果を取得できるようになります。この後の変更で、この Sort オブジェクトを実際にメソッドの引数として利用します。

変更点: Repository検索メソッドへのSort引数追加

/src/main/java/com/example/bbs/repository/PostRepository.java
1 public interface PostRepository extends JpaRepository<Post, Long> {
2     // 部分一致
3-    List<Post> findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword);
4+    List<Post> findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword, Sort sort);
5 
6     // 前方一致
7-    List<Post> findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix);
8+    List<Post> findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix, Sort sort);
9 
10     // 後方一致
11-    List<Post> findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix);
12+    List<Post> findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix, Sort sort);
13 }

この変更では、PostRepository インターフェースで定義されている各検索メソッドに、新たに Sort sort という引数を追加しました。

具体的には、以下の三つのメソッドが変更されました。

  1. List<Post> findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword, Sort sort);
  2. List<Post> findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix, Sort sort);
  3. List<Post> findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix, Sort sort);

これらのメソッドは、これまでタイトルまたはコンテンツで部分一致、前方一致、後方一致の検索を行うためのものでした。今回 Sort sort を引数に追加することで、これらの検索結果をさらに指定された Sort オブジェクトの条件に従って並べ替えることができるようになりました。

Spring Data JPAでは、メソッド名に OrderBy を含めることで静的にソート条件を指定できますが、このように Sort オブジェクトを引数として渡すことで、実行時に動的にソートの基準や方向を切り替えることが可能になります。これにより、柔軟なソート機能を実現できます。

変更点: ServiceでのSortクラスのインポート

/src/main/java/com/example/bbs/service/PostService.java
1 import com.example.bbs.repository.PostRepository;
2 import org.springframework.stereotype.Service;
3 
4+import org.springframework.data.domain.Sort;
5+
6 import java.util.List;
7 import java.util.Optional;
8 

この変更では、PostService.java ファイルに org.springframework.data.domain.Sort クラスをインポートしました。

サービス層はビジネスロジックを記述する場所であり、ここで Sort オブジェクトを生成し、それをRepository層に渡すことでデータの並び替えを指示します。この Sort クラスを扱うために、事前にインポートが必要となります。

変更点: ServiceのfindAllメソッドにソートロジックを追加

/src/main/java/com/example/bbs/service/PostService.java
1         this.postRepository = postRepository;
2     }
3 
4-    public List<Post> findAll() {
5-        return postRepository.findAll();
6+    public List<Post> findAll(String sortBy, String sortOrder) {
7+        // Sort.Orderを使ってソート順を設定
8+        Sort.Order order;
9+        if (sortOrder.equals("asc")) {
10+            order = new Sort.Order(Sort.Direction.ASC, sortBy);
11+        } else {
12+            order = new Sort.Order(Sort.Direction.DESC, sortBy);
13+        }
14+
15+        // Sortオブジェクトを作成
16+        Sort sort = Sort.by(order);
17+
18+        return postRepository.findAll(sort);
19     }
20 

この変更では、PostService クラスの findAll メソッドを修正しました。

変更点は以下の通りです。

  1. メソッドシグネチャの変更:

    • これまでは引数なしの public List<Post> findAll() でしたが、ソート基準と並び順を受け取れるように public List<Post> findAll(String sortBy, String sortOrder) に変更されました。sortBy はソート対象のフィールド名(例: "createdAt", "title")を、sortOrder は並び順("asc": 昇順, "desc": 降順)を受け取ります。
  2. Sort.Order オブジェクトの作成:

    • Sort.Order は、具体的なソートの条件(どのフィールドを、どちらの向きでソートするか)を定義するクラスです。
    • if (sortOrder.equals("asc")) の条件で、受け取った sortOrder が "asc"(昇順)であれば Sort.Direction.ASC を、そうでなければ Sort.Direction.DESC (降順) を使用して Sort.Order オブジェクトを生成しています。
    • new Sort.Order(Sort.Direction.ASC, sortBy) のように、ソートの方向とソート基準となるフィールド名を指定します。
  3. Sort オブジェクトの作成:

    • Sort.by(order) を使って、作成した Sort.Order オブジェクトを元に Sort オブジェクトを生成します。Sort オブジェクトは複数のソート条件をまとめることもできますが、今回は一つの条件のみを指定しています。
  4. Repositoryメソッドの呼び出し:

    • 最後に postRepository.findAll(sort) を呼び出しています。JpaRepositoryfindAll メソッドは、引数として Sort オブジェクトを受け取ることができ、この Sort オブジェクトに基づいてデータベースからソートされたデータを取得します。

この修正により、サービス層で動的にソート条件を組み立て、それをリポジトリ層に渡すことで、ユーザーの指定に応じた並び順で投稿リストを取得できるようになりました。

変更点: ServiceのsearchPostsメソッドにソートロジックを追加

/src/main/java/com/example/bbs/service/PostService.java
1 
2-    public List<Post> searchPosts(String keyword, String matchType) {
3+    public List<Post> searchPosts(String keyword, String matchType, String sortBy, String sortOrder) {
4+        // Sort.Orderを使ってソート順を設定
5+        Sort.Order order;
6+        if (sortOrder.equals("asc")) {
7+            order = new Sort.Order(Sort.Direction.ASC, sortBy); // 昇順
8+        } else {
9+            order = new Sort.Order(Sort.Direction.DESC, sortBy); // 降順
10+        }
11+
12+        // Sortオブジェクトを作成
13+        Sort sort = Sort.by(order);
14+
15+        // 検索条件に基づいて投稿を取得
16         switch (matchType) {
17             case "startswith": // 前方一致
18-                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword);
19+                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword, sort);
20             case "endswith": // 後方一致
21-                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword);
22+                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword, sort);
23             case "contains": // 部分一致(デフォルト)
24             default:
25-                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword);
26+                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword, sort);
27         }
28     }
29 

この変更では、PostService クラスの searchPosts メソッドも修正しました。

主な変更点は以下の通りです。

  1. メソッドシグネチャの変更:

    • これまでの public List<Post> searchPosts(String keyword, String matchType) に加えて、String sortByString sortOrder の引数が追加されました。これにより、検索と同時にソート条件も受け取れるようになります。
  2. Sort.Order および Sort オブジェクトの作成:

    • findAll メソッドと同様に、受け取った sortBysortOrder の値を使って Sort.Order オブジェクトを生成し、そこから Sort オブジェクトを作成しています。
    • if (sortOrder.equals("asc")) の条件で昇順か降順かを判断し、適切な Sort.Direction を指定します。
  3. Repository検索メソッドの呼び出し:

    • switch 文の中の各検索ケースで、postRepository の対応するメソッドを呼び出す際に、新しく作成した Sort オブジェクトを引数として渡しています。
    • 例えば、postRepository.findByTitleContainingOrContentContaining(keyword, keyword, sort); のように変更されました。

この修正により、検索機能とソート機能を組み合わせることが可能になりました。ユーザーが指定したキーワードで投稿を検索し、その検索結果をさらにユーザーが指定した基準と順序で並べ替えて表示できるようになります。

変更点: HTMLにソート基準選択用のドロップダウンを追加

/src/main/resources/templates/posts/list.html
1     <!-- 検索フォーム -->
2     <form th:action="@{/posts}" method="get" class="mb-3">
3         <div class="row">
4-            <div class="col-md-6">
5+            <!-- 検索キーワード -->
6+            <div class="col-md-4">
7                 <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>
8             </div>
9+            <!-- マッチタイプ -->
10             <div class="col-md-2">
11                 <select name="matchType" class="form-control">
12                     <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>
13                     <option value="endswith" th:selected="${param.matchType?.toString() == 'endswith'}">後方一致</option>
14                 </select>
15             </div>
16+            <!-- ソート基準 -->
17+            <div class="col-md-2">
18+                <select name="sortBy" class="form-control">
19+                    <option value="createdAt" th:selected="${param.sortBy?.toString() == 'createdAt'}">作成日</option>
20+                    <option value="title" th:selected="${param.sortBy?.toString() == 'title'}">タイトル</option>
21+                </select>
22+            </div>

この変更では、掲示板一覧画面のHTMLファイル (posts/list.html) に、ユーザーがソート基準を選択するための新しいドロップダウンメニュー(<select> 要素)を追加しました。

  • <div class="col-md-2">: Bootstrapのグリッドシステムを利用して、このドロップダウンが画面上で占める横幅を定義しています。

  • <select name="sortBy" class="form-control">:

    • name="sortBy": この select 要素で選択された値が、HTTPリクエストの sortBy パラメータとして送信されます。この sortBy という名前は、Controllerで定義した @RequestParam("sortBy") と一致している必要があります。
    • class="form-control": Bootstrapのスタイルを適用し、見た目を整えています。
  • <option value="createdAt" th:selected="${param.sortBy?.toString() == 'createdAt'}">作成日</option>:

    • value="createdAt": このオプションが選択されたときに、HTTPリクエストの sortBy パラメータとして "createdAt" という値が送信されます。この値は、Postエンティティ(データベースのテーブル)のフィールド名と一致している必要があります。
    • th:selected="${param.sortBy?.toString() == 'createdAt'}": これはThymeleafの機能で、もし現在のリクエストの sortBy パラメータが "createdAt" であれば、このオプションが初期状態で選択された状態になります。これにより、ページを再読み込みしたり検索を実行したりしても、以前選択していたソート基準が保持されます。
    • 作成日: ユーザーに表示される選択肢のテキストです。
  • <option value="title" th:selected="${param.sortBy?.toString() == 'title'}">タイトル</option>:

    • value="title": このオプションが選択されると "title" が送信されます。これもPostエンティティのフィールド名です。
    • th:selected は「作成日」のオプションと同様に、現在の選択状態を保持します。
    • タイトル: ユーザーに表示される選択肢のテキストです。

このHTMLの追加により、ユーザーは「作成日」または「タイトル」をソートの基準として選択できるようになり、選択した値がバックエンドに送信されることで、動的なソートが可能になります。

変更点: HTMLに並び順選択用のドロップダウンを追加

/src/main/resources/templates/posts/list.html
1+            <!-- 並び順 -->
2+            <div class="col-md-2">
3+                <select name="sortOrder" class="form-control">
4+                    <option value="asc" th:selected="${param.sortOrder?.toString() == 'asc'}">昇順</option>
5+                    <option value="desc" th:selected="${param.sortOrder?.toString() == 'desc'}">降順</option>
6+                </select>
7+            </div>
8+            <!-- 検索ボタン -->
9             <div class="col-md-2">
10                 <button type="submit" class="btn btn-primary">検索</button>
11             </div>

この変更では、掲示板一覧画面のHTMLファイル (posts/list.html) に、ユーザーが並び順(昇順か降順か)を選択するための新しいドロップダウンメニュー(<select> 要素)を追加しました。

  • <div class="col-md-2">: Bootstrapのグリッドシステムを利用して、このドロップダウンが画面上で占める横幅を定義しています。

  • <select name="sortOrder" class="form-control">:

    • name="sortOrder": この select 要素で選択された値が、HTTPリクエストの sortOrder パラメータとして送信されます。この sortOrder という名前は、Controllerで定義した @RequestParam("sortOrder") と一致している必要があります。
    • class="form-control": Bootstrapのスタイルを適用しています。
  • <option value="asc" th:selected="${param.sortOrder?.toString() == 'asc'}">昇順</option>:

    • value="asc": このオプションが選択されたときに、HTTPリクエストの sortOrder パラメータとして "asc" という値が送信されます。この値は、Service層で Sort.Direction.ASC と連携します。
    • th:selected="${param.sortOrder?.toString() == 'asc'}": Thymeleafの機能で、もし現在のリクエストの sortOrder パラメータが "asc" であれば、このオプションが初期状態で選択された状態になります。
    • 昇順: ユーザーに表示される選択肢のテキストです。
  • <option value="desc" th:selected="${param.sortOrder?.toString() == 'desc'}">降順</option>:

    • value="desc": このオプションが選択されたときに、HTTPリクエストの sortOrder パラメータとして "desc" という値が送信されます。この値は、Service層で Sort.Direction.DESC と連携します。
    • th:selected は「昇順」のオプションと同様に、現在の選択状態を保持します。
    • 降順: ユーザーに表示される選択肢のテキストです。

このHTMLの追加により、ユーザーは「昇順」または「降順」を並び順として選択できるようになり、選択した値がバックエンドに送信されることで、動的なソートが可能になります。

変更点: HTML検索フォームのレイアウト調整

/src/main/resources/templates/posts/list.html
1     <!-- 検索フォーム -->
2     <form th:action="@{/posts}" method="get" class="mb-3">
3         <div class="row">
4-            <div class="col-md-6">
5+            <!-- 検索キーワード -->
6+            <div class="col-md-4">
7                 <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>
8             </div>
9+            <!-- マッチタイプ -->
10             <div class="col-md-2">
11                 <select name="matchType" class="form-control">
12                     <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>
13                     <option value="endswith" th:selected="${param.matchType?.toString() == 'endswith'}">後方一致</option>
14                 </select>
15             </div>
16+            <!-- ソート基準 -->
17+            <div class="col-md-2">
18+                <select name="sortBy" class="form-control">
19+                    <option value="createdAt" th:selected="${param.sortBy?.toString() == 'createdAt'}">作成日</option>
20+                    <option value="title" th:selected="${param.sortBy?.toString() == 'title'}">タイトル</option>
21+                </select>
22+            </div>
23+            <!-- 並び順 -->
24+            <div class="col-md-2">
25+                <select name="sortOrder" class="form-control">
26+                    <option value="asc" th:selected="${param.sortOrder?.toString() == 'asc'}">昇順</option>
27+                    <option value="desc" th:selected="${param.sortOrder?.toString() == 'desc'}">降順</option>
28+                </select>
29+            </div>
30+            <!-- 検索ボタン -->
31             <div class="col-md-2">
32                 <button type="submit" class="btn btn-primary">検索</button>
33             </div>
34         </div>
35     </form>
36-
37     <h2>掲示板一覧</h2>
38     <table class="table table-striped">
39         <thead>

この変更では、HTMLの検索フォームのレイアウトを調整しました。

  • 検索キーワードの幅を変更:

    • これまでの <div class="col-md-6"> (横幅の6/12)から <div class="col-md-4"> (横幅の4/12)に変更しました。これにより、検索キーワードの入力欄が少し狭くなり、その分他の要素を横に並べるスペースを確保しています。
  • 新しいソート関連の要素を追加:

    • ソート基準 (sortBy) と並び順 (sortOrder) を選択するドロップダウンメニューがそれぞれ <div class="col-md-2"> で追加されました。これにより、検索キーワード、マッチタイプ、ソート基準、並び順、検索ボタンの各要素が横一列に並ぶようなレイアウトになります。

Bootstrapのグリッドシステム (col-md-*) は、画面の横幅を12分割して要素の幅を指定するものです。今回の変更では、新しい要素を追加しつつ、全体のレイアウトが崩れないように各要素の幅を調整しています。これにより、フォームの見た目が整理され、使いやすさが向上しています。

おわりに

この記事では、Spring Bootアプリケーションに並び替え(ソート)機能を実装する一連の流れを学習しました。具体的には、Controllerでソート条件をリクエストパラメータとして受け取り、Service層でSpring Data JPAのSortオブジェクトを動的に生成しました。そして、Repository層のメソッドにこのSortオブジェクトを渡すことで、データベースから指定された基準と順序でデータを取得できることを確認しました。また、HTMLフォームにソート基準と並び順の選択肢を追加し、ユーザーが画面から直感的に操作できるUIの実装も行いました。これらの実装を通じて、検索機能と連携した柔軟なデータ表示の実現方法を体系的に学ぶことができました。

関連コンテンツ