【Spring Boot】検索機能を実装する|簡単な掲示板アプリの作成

この記事では、Spring Bootで作成した掲示板アプリに検索機能を追加する方法を学びます。投稿のタイトルや内容をキーワードで検索できるようにし、部分一致、前方一致、後方一致といった多様な検索方法を実装します。Spring Data JPAを活用したデータ検索と、Thymeleafを使った検索フォームの作成手順を具体的に解説します。

作成日: 更新日:

開発環境

  • 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 import com.example.bbs.service.PostService;
2 import com.example.bbs.service.UserService;
3 import com.example.bbs.service.CommentService;
4+
5+import java.util.List;
6+
7 import org.springframework.stereotype.Controller;
8 import org.springframework.ui.Model;
9 import org.springframework.web.bind.annotation.*;
10 
11     // 一覧表示
12     @GetMapping
13-    public String listPosts(Model model) {
14-        // ログインユーザーを取得
15-        User loggedInUser = userService.getCurrentUser();
16+    public String listPosts(
17+            @RequestParam(value = "keyword", required = false) String keyword,
18+            @RequestParam(value = "matchType", required = false) String matchType,
19+            Model model) {
20 
21-        // モデルに渡す
22+        // ログインユーザーを取得してモデルに渡す
23+        User loggedInUser = userService.getCurrentUser();
24         model.addAttribute("loggedInUserId", loggedInUser.getId());
25 
26-        model.addAttribute("posts", postService.findAll());
27+        // 検索フォームの入力値を保持
28+        List<Post> posts;
29+        if (keyword != null && !keyword.isEmpty() && matchType != null && !matchType.isEmpty()) {
30+            posts = postService.searchPosts(keyword, matchType);
31+        } else {
32+            posts = postService.findAll();
33+        }
34+
35+        model.addAttribute("posts", posts);
36+
37         return "posts/list";
38     }
39 
40

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

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

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

/src/main/java/com/example/bbs/service/PostService.java
1         return postRepository.findAll();
2     }
3 
4+    public List<Post> searchPosts(String keyword, String matchType) {
5+        switch (matchType) {
6+            case "startswith": // 前方一致
7+                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword);
8+            case "endswith": // 後方一致
9+                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword);
10+            case "contains": // 部分一致(デフォルト)
11+            default:
12+                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword);
13+        }
14+    }
15+
16     public Optional<Post> findById(Long id) {
17         return postRepository.findById(id);
18     }
19

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

/src/main/resources/templates/posts/list.html
1     <h1>Post List</h1>
2     <h2>新規投稿</h2>
3     <a href="/posts/new" class="btn btn-primary mb-3">New Post</a>
4+
5+    <!-- 検索フォーム -->
6+    <form th:action="@{/posts}" method="get" class="mb-3">
7+        <div class="row">
8+            <div class="col-md-6">
9+                <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>
10+            </div>
11+            <div class="col-md-2">
12+                <select name="matchType" class="form-control">
13+                    <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>
14+                    <option value="startswith" th:selected="${param.matchType?.toString() == 'startswith'}">前方一致</option>
15+                    <option value="endswith" th:selected="${param.matchType?.toString() == 'endswith'}">後方一致</option>
16+                </select>
17+            </div>
18+            <div class="col-md-2">
19+                <button type="submit" class="btn btn-primary">検索</button>
20+            </div>
21+        </div>
22+    </form>
23+
24     <h2>掲示板一覧</h2>
25     <table class="table table-striped">
26         <thead>
27

コード解説

変更点: PostRepositoryに検索用メソッドを追加

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

PostRepositoryは、データベースから投稿(Post)のデータを取得したり保存したりする役割を担うインターフェースです。Spring Data JPAという機能を使うと、ここに特定の命名規則に従ってメソッドを宣言するだけで、Spring Bootが自動的にデータベースへの検索処理を生成してくれます。

今回追加されたメソッドは、以下の3種類です。

  1. findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword)

    • このメソッドは、投稿のタイトル(Title)または内容(Content)に、指定されたキーワード(Containing)が含まれる(部分一致)投稿を検索します。Orが入っているため、「タイトルにキーワードが含まれるか、内容にキーワードが含まれるか、どちらか一方でも当てはまれば」検索結果に含めます。
  2. findByTitleStartingWithOrContentStartingWith(String titlePrefix, String contentPrefix)

    • このメソッドは、投稿のタイトルまたは内容が、指定された文字列(StartingWith)で始まる(前方一致)投稿を検索します。
  3. findByTitleEndingWithOrContentEndingWith(String titleSuffix, String contentSuffix)

    • このメソッドは、投稿のタイトルまたは内容が、指定された文字列(EndingWith)で終わる(後方一致)投稿を検索します。

これらのメソッドは、それぞれタイトルと内容の両方に対して検索を行えるように、titleKeyword(またはtitlePrefix, titleSuffix)とcontentKeyword(またはcontentPrefix, contentSuffix)という2つの引数を受け取ります。

変更点: PostServiceに検索ロジックを追加

1         return postRepository.findAll();
2     }
3
4+    public List<Post> searchPosts(String keyword, String matchType) {
5+        switch (matchType) {
6+            case "startswith": // 前方一致
7+                return postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword);
8+            case "endswith": // 後方一致
9+                return postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword);
10+            case "contains": // 部分一致(デフォルト)
11+            default:
12+                return postRepository.findByTitleContainingOrContentContaining(keyword, keyword);
13+        }
14+    }
15+
16     public Optional<Post> findById(Long id) {
17         return postRepository.findById(id);
18     }

PostServiceは、アプリケーションのビジネスロジック、つまり「何をするか」という処理を担当するクラスです。ここでは、新しくsearchPostsというメソッドが追加されました。

このsearchPostsメソッドは、ユーザーが入力した検索キーワード(keyword)と、どの種類の検索を行うかを示す検索タイプ(matchType)の2つの情報を受け取ります。

メソッドの中ではswitch文が使われています。これは、matchTypeの値に応じて異なる処理を実行するためのものです。

  • case "startswith": matchTypeが"startswith"(前方一致)の場合、postRepository.findByTitleStartingWithOrContentStartingWith(keyword, keyword)を呼び出します。これは、タイトルまたは内容がキーワードで始まる投稿を検索する処理です。
  • case "endswith": matchTypeが"endswith"(後方一致)の場合、postRepository.findByTitleEndingWithOrContentEndingWith(keyword, keyword)を呼び出します。これは、タイトルまたは内容がキーワードで終わる投稿を検索する処理です。
  • case "contains" または default: matchTypeが"contains"(部分一致)の場合、または上記以外の値の場合(デフォルト処理)には、postRepository.findByTitleContainingOrContentContaining(keyword, keyword)を呼び出します。これは、タイトルまたは内容にキーワードが含まれる投稿を検索する処理です。

このように、PostServiceは受け取った検索タイプに応じて、PostRepositoryに定義した適切な検索メソッドを呼び出し、その結果を返します。

変更点: PostControllerで検索パラメータを受け取り、検索処理を呼び出す

1 import com.example.bbs.service.PostService;
2 import com.example.bbs.service.UserService;
3 import com.example.bbs.service.CommentService;
4+
5+import java.util.List;
6+
7 import org.springframework.stereotype.Controller;
8 import org.springframework.ui.Model;
9 import org.springframework.web.bind.annotation.*;
10
11     // 一覧表示
12     @GetMapping
13-    public String listPosts(Model model) {
14-        // ログインユーザーを取得
15-        User loggedInUser = userService.getCurrentUser();
16+    public String listPosts(
17+            @RequestParam(value = "keyword", required = false) String keyword,
18+            @RequestParam(value = "matchType", required = false) String matchType,
19+            Model model) {
20
21-        // モデルに渡す
22+        // ログインユーザーを取得してモデルに渡す
23        User loggedInUser = userService.getCurrentUser();
24        model.addAttribute("loggedInUserId", loggedInUser.getId());
25
26-        model.addAttribute("posts", postService.findAll());
27+        // 検索フォームの入力値を保持
28+        List<Post> posts;
29+        if (keyword != null && !keyword.isEmpty() && matchType != null && !matchType.isEmpty()) {
30+            posts = postService.searchPosts(keyword, matchType);
31+        } else {
32+            posts = postService.findAll();
33+        }
34+
35+        model.addAttribute("posts", posts);
36+
37         return "posts/list";
38     }

PostControllerは、Webブラウザからのリクエストを受け取り、適切な処理を行い、結果をWebブラウザに返す役割を担うクラスです。

変更点として、listPostsメソッドの引数に@RequestParamアノテーションが追加されました。

  • @RequestParam(value = "keyword", required = false) String keyword: これは、HTTPリクエストのURLに含まれるkeywordという名前のパラメータ(例: ?keyword=テスト)をkeyword変数に受け取ることを意味します。required = falseは、このパラメータが必須ではないことを示しています。つまり、keywordがなくてもエラーにはなりません。
  • @RequestParam(value = "matchType", required = false) String matchType: こちらも同様に、matchTypeという名前のパラメータをmatchType変数に受け取ります。

メソッドの内部では、受け取ったkeywordmatchTypeが存在するかどうか(nullではなく、空ではないか)をチェックしています。

  • もしkeywordmatchTypeの両方が存在する場合、postService.searchPosts(keyword, matchType)を呼び出して、検索結果を取得します。
  • どちらか一方でも存在しない場合、または両方存在しない場合は、postService.findAll()を呼び出して、すべての投稿を取得します。

このようにして取得した投稿のリストは、model.addAttribute("posts", posts)を使って、表示用のThymeleafテンプレート(posts/list.html)に渡されます。これにより、テンプレートは検索結果または全投稿リストを表示できるようになります。

変更点: Thymeleafテンプレートに検索フォームを追加

1 <div class="container mt-4">
2     <h1>Post List</h1>
3     <h2>新規投稿</h2>
4     <a href="/posts/new" class="btn btn-primary mb-3">New Post</a>
5+
6+    <!-- 検索フォーム -->
7+    <form th:action="@{/posts}" method="get" class="mb-3">
8+        <div class="row">
9+            <div class="col-md-6">
10+                <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>
11+            </div>
12+            <div class="col-md-2">
13+                <select name="matchType" class="form-control">
14+                    <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>
15+                    <option value="startswith" th:selected="${param.matchType?.toString() == 'startswith'}">前方一致</option>
16+                    <option value="endswith" th:selected="${param.matchType?.toString() == 'endswith'}">後方一致</option>
17+                </select>
18+            </div>
19+            <div class="col-md-2">
20+                <button type="submit" class="btn btn-primary">検索</button>
21+            </div>
22+        </div>
23+    </form>
24+
25     <h2>掲示板一覧</h2>
26     <table class="table table-striped">
27         <thead>

posts/list.htmlは、Webブラウザに表示されるHTMLテンプレートです。Thymeleafというテンプレートエンジンを使って、Javaコードから受け取ったデータを表示します。

今回の変更で、このHTMLファイルに検索フォームが追加されました。

  • <form th:action="@{/posts}" method="get" class="mb-3">:

    • これは検索用のフォームです。th:action="@{/posts}"は、このフォームが/postsというURLに対してリクエストを送ることを指定しています。method="get"は、GETメソッドを使ってリクエストを送ることを意味し、これはPostController@GetMappingメソッドに対応します。
  • <input type="text" name="keyword" class="form-control" placeholder="Search" th:value="${param.keyword}" required>:

    • これは検索キーワードを入力するためのテキストボックスです。name="keyword"と指定することで、この入力値がControllerで@RequestParam("keyword")として受け取れるようになります。
    • th:value="${param.keyword}"はThymeleafの機能で、もしURLにkeywordパラメータがあれば、その値をテキストボックスの初期値として設定します。これにより、検索後にページを再表示しても、入力したキーワードが消えずに残るようになります。
    • required属性は、このフィールドが必須入力であることを意味します。
  • <select name="matchType" class="form-control"> ... </select>:

    • これは検索タイプ(部分一致、前方一致、後方一致)を選択するためのドロップダウンリストです。name="matchType"と指定することで、選択された値がControllerで@RequestParam("matchType")として受け取れます。
    • <option value="contains" th:selected="${param.matchType?.toString() == 'contains'}">部分一致</option>のように、各optionタグにはvalue属性で検索タイプを示す文字列(containsなど)が設定されています。
    • th:selected="${param.matchType?.toString() == 'contains'}"もThymeleafの機能で、もしURLにmatchTypeパラメータがあり、その値がこのoptionvalueと一致すれば、そのoptionが選択された状態になります。これにより、検索後に前回選択した検索タイプが保持されます。
  • <button type="submit" class="btn btn-primary">検索</button>:

    • これは検索を実行するためのボタンです。このボタンをクリックすると、フォームに入力されたデータが/postsにGETリクエストとして送信されます。

おわりに

今回の記事では、Spring Bootで作成した掲示板アプリに検索機能を追加する手順を学びました。Spring Data JPAの命名規則を活用することで、部分一致や前方一致など多様な検索条件でデータを効率的に取得するRepositoryメソッドを定義できました。また、Controllerで検索パラメータを受け取り、Serviceで検索ロジックを振り分け、Thymeleafでユーザーが使いやすい検索フォームを作成する一連の流れを理解できたことと思います。これらの知識は、Webアプリケーション開発における検索機能実装の基礎として、今後の開発に役立つでしょう。

関連コンテンツ