Javaで検索機能を作るには?入力したキーワードでデータを絞り込む仕組み

タスク名に「資料」と入力すると、「資料を確認する」と「会議資料を作る」だけが表示される。検索機能では、入力した文字をDBの検索条件に使い、一致したデータを一覧へ返します。画面に全件を表示してから、不要な行を隠しているわけではありません。
この記事では、入力欄 → Servlet → Model → DAO → DBへキーワードが届き、取得したデータがList<Task> → JSP → HTMLとして一覧になるまでを追います。入力が空の場合と、検索した結果が0件の場合の違いも整理します。
本記事は、Schoo「Java入門 中級」第7回「データベースと連携したWebアプリケーションの実装」に対応する学習記事です。未受講の方も、以下のデータとコードを読み比べて理解できる構成です。
「資料」を含むタスクだけを表示したい
SQLiteのtasksテーブルに、次の3件が入っているものとします。idは各行を識別する番号、titleはタスク名です。この例では、あらかじめ用意した固定データを検索します。
| id | title |
|---|---|
| 1 | 資料を確認する |
| 2 | 議事録を作る |
| 3 | 会議資料を作る |
「資料」で検索した結果は、ID=1とID=3の2件です。ID=3のように途中に「資料」がある場合も対象にします。このような文字列の一部分が一致する検索を、部分一致検索と呼びます。
| 入力するキーワード | 期待する検索結果 |
|---|---|
| 資料 | ID=1、ID=3の2件 |
| 議事録 | ID=2の1件 |
| 買い物 | 0件。「該当するタスクはありません。」と表示 |
イラスト:入力したキーワードで絞り込み、一覧へ返す

上下の図は、同じ1回の検索を前半と後半に分けたものです。DBは条件に一致する行を返し、DAOが各行の値からTaskを作ってListへ追加します。DBからList<Task>そのものが送られるわけではありません。
1.フォームのnameを使って、入力した文字を受け取る
task-search.html:検索画面。Servletと同じWebアプリの直下に置く例です。
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>タスク検索</title></head>
<body>
<h1>タスク検索</h1>
<form action="task-search" method="get">
<label for="keyword">タスク名</label>
<input type="text" id="keyword" name="keyword">
<button type="submit">検索</button>
</form>
</body>
</html>
method="get"なので、検索ボタンを押すとGETリクエストが送られます。actionのtask-searchはServletのURLです。入力欄のname=”keyword”が、入力値を送るときの名前になります。idはlabelとの対応に使っており、送信するパラメータ名を決めるものではありません。
TaskSearchServletのクラス宣言の直前に、対応するURLを指定します。
@WebServlet("/task-search")
そのServletのdoGet内で、次のように入力を受け取ります。
String keyword = request.getParameter("keyword");
「資料」を入力すると、keywordにはStringの"資料"が入ります。今回は文字を検索するため、整数への変換は不要です。
アプリのパスが/tasks-appなら、送信先は/tasks-app/task-searchです。URLにはkeywordの値も付きます。日本語は通信時にURLエンコードされるため、アドレス欄やログでは%で始まる表記になることがあります。検索はデータを変更しない操作なので、この例ではGETを使っています。
2.Modelで空欄を確認してから、DAOに検索を依頼する
Modelは、この検索処理の窓口です。「キーワードなしでは検索しない」というルールをここに置きます。
TaskSearchModel.java:searchメソッド
public List<Task> search(String keyword) throws SQLException {
if (keyword == null || keyword.isBlank()) {
throw new IllegalArgumentException("検索キーワードを入力してください。");
}
TaskSearchDao dao = new TaskSearchDao(dbPath);
return dao.findByTitle(keyword);
}
keyword == nullはkeywordというパラメータがない場合、isBlank()は空文字や空白だけの場合を調べます。条件に合わない入力では例外を通知し、DAOを呼びません。Java 11以降のisBlankを使っています。
dbPathはコンストラクタで受け取って保持するDBファイルの場所です。ModelとDAOはそれぞれ次の形で保持します。クラス名は、それぞれTaskSearchModel・TaskSearchDaoになります。
private final String dbPath;
public TaskSearchModel(String dbPath) {
this.dbPath = dbPath;
}
この例では、空欄で全件検索する仕様にはしていません。また、前後の空白を取り除く処理もしていないため、文字と一緒に入力した空白は検索条件の一部になります。こうした入力の扱いは、画面と処理の両方で決めておく必要があります。
3.LIKEと「%」で、部分一致の条件を作る
SELECT id, title FROM tasks WHERE title LIKE ? ORDER BY id
LIKEは、文字列が指定したパターンに一致するかを調べます。%は「0文字以上の任意の文字列」です。キーワードの前後に%を付けることで、前後に文字があっても一致する条件になります。
stmt.setString(1, "%" + keyword + "%");
| パターン | 一致する条件 | 今回の3件での結果 |
|---|---|---|
| 資料 | 「資料」と同じ文字列 | 0件 |
| 資料% | 「資料」で始まる | ID=1 |
| %資料 | 「資料」で終わる | 0件 |
| %資料% | 途中を含め、どこかに「資料」がある | ID=1、ID=3 |
keywordが「資料」なら、Javaで作る文字列は"%資料%"です。setStringの第1引数の1は、最初の?の番号であり、ID=1を検索する指定ではありません。
SQLはLIKE ?と書き、?をシングルクォートで囲みません。%を付けた検索値をsetStringで渡します。入力値をSQL文へ直接つなげるのではなく、SQLの形と、条件として渡す値を分ける書き方です。
注意:入力した%や_も、LIKEの特殊文字として働きます。%は任意の0文字以上、_は任意の1文字です。PreparedStatementを使っても、この意味はなくなりません。例えば今回のデータで%を入力すると3件すべてに一致します。「%という文字そのもの」を検索したい仕様では、LIKEのエスケープを別途設計する必要があります。ここでは授業と同じ、LIKEのパターンによる検索を扱います。
4.一致した行を、一つずつTaskにしてListへ追加する
TaskSearchDao.java:findByTitleメソッド
public List<Task> findByTitle(String keyword) throws SQLException {
String sql = "SELECT id, title FROM tasks WHERE title LIKE ? ORDER BY id";
List<Task> tasks = new ArrayList<>();
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, "%" + keyword + "%");
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
Task task = new Task(rs.getInt("id"), rs.getString("title"));
tasks.add(task);
}
}
}
return tasks;
}
DBの検索結果は、ここでは0件・1件・複数件のどれもあり得ます。そこでwhile (rs.next())で1行ずつ読み、毎回new TaskしてListへ追加します。IDの詳細検索のようにifだけで読むと、2件目以降を取りこぼします。
// 「資料」で検索した場合の、Listに追加する値のイメージ
tasks.add(new Task(1, "資料を確認する"));
tasks.add(new Task(3, "会議資料を作る"));
この2行は結果のイメージです。実装では上のDAOのとおり、ResultSetから取得した値を使います。検索する文字が変わるたびに、addのコードを書き換えるわけではありません。
Task.java:1件分のIDとタスク名を保持するDTO
public class Task {
private final int id;
private final String title;
public Task(int id, String title) {
this.id = id;
this.title = title;
}
public int getId() { return id; }
public String getTitle() { return title; }
}
一致する行がなくても、最初に作った空のListが返ります。0件はnullではなく、要素数が0のListです。一方、SQL処理に失敗した場合はSQLExceptionが通知されます。失敗を空のListに置き換えてはいけません。
ResultSetなどはtry-with-resourcesで閉じますが、すでに値を取り出して作ったTaskとListは、その後も利用できます。JSPにはResultSetを渡さず、Javaのオブジェクトにした結果を渡します。
5.Servletで検索結果をtasks属性に保存し、JSPへ渡す
TaskSearchServlet.java:doGet全体。このServletには前述の@WebServlet("/task-search")を付けます。
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String keyword = request.getParameter("keyword");
String dbPath = getServletContext().getRealPath("/WEB-INF/db/tasks.db");
TaskSearchModel model = new TaskSearchModel(dbPath);
try {
List<Task> tasks = model.search(keyword);
request.setAttribute("tasks", tasks);
} catch (IllegalArgumentException e) {
response.setStatus(400);
request.setAttribute("error", "検索キーワードを入力してください。");
} catch (SQLException e) {
getServletContext().log("Task search failed", e);
response.setStatus(500);
request.setAttribute("error", "検索できませんでした。時間をおいて再度お試しください。");
}
request.getRequestDispatcher("/task-search.jsp").forward(request, response);
}
この例は、展開されたWebアプリのWEB-INF/db/tasks.dbを使う構成です。getRealPathはサーバー上の実際の場所を取得します。配備方式によってはnullになるため、実際の運用では接続先や保存場所を環境に合わせて設定します。JSPはWEB-INFではなく、アプリ直下に置きます。
| 名前 | 使う場所と役割 |
|---|---|
| keyword | フォームのnameとgetParameterに使う。ブラウザから送られたString。 |
| tasks | setAttributeとgetAttributeに使う。サーバー内でJSPへ渡すList<Task>。 |
| error | 入力不正やDB処理の失敗を知らせる、サーバー側で決めたメッセージ。 |
keywordはリクエストパラメータ、tasksはリクエスト属性です。getParameter(“tasks”)ではListを取り出せません。Servletが設定する属性名と、JSPで取り出す属性名を一致させます。
forwardは同じリクエストを使ってJSPへ処理を引き継ぐため、tasks属性を読むことができます。URLは検索用Servletのままです。例外の詳細はログへ記録し、DBの場所やSQLを画面へ出さないようにしています。
6.JSPで、検索結果の件数と一覧を表示する
task-search.jsp:アプリ直下の表示用ファイル
以下は冒頭の固定データを表示する学習用の例です。<%= ... %>は自動でHTMLエスケープされません。利用者が登録したタスク名などを表示するアプリに広げる場合は、出力時のHTMLエスケープが必要です。この例では、検索キーワードや例外の詳細を画面へそのまま出力しません。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List,example.Task" %>
<%
List<Task> tasks = (List<Task>) request.getAttribute("tasks");
String error = (String) request.getAttribute("error");
%>
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>タスク検索結果</title></head>
<body>
<h1>タスク検索結果</h1>
<% if (error != null) { %>
<p><%= error %></p>
<% } else if (tasks == null) { %>
<p>検索画面からキーワードを入力してください。</p>
<% } else if (tasks.isEmpty()) { %>
<p>該当するタスクはありません。</p>
<% } else { %>
<p><%= tasks.size() %>件</p>
<table>
<tr><th>ID</th><th>タスク名</th></tr>
<% for (Task task : tasks) { %>
<tr>
<td><%= task.getId() %></td>
<td><%= task.getTitle() %></td>
</tr>
<% } %>
</table>
<% } %>
<p><a href="task-search.html">検索画面に戻る</a></p>
</body>
</html>
(List<Task>)は、Object型で取り出した属性を、このServletが保存したList<Task>として扱うためのキャストです。Taskはexampleパッケージのクラスなので、pageディレクティブでListとともに読み込んでいます。
tasks.size()で件数を出し、拡張for文でTaskを1件ずつ表示します。「資料」の検索なら、ブラウザには2件とID=1・ID=3の表が表示されます。JSPが生成したHTMLがブラウザへ届き、JavaのListそのものが送られるわけではありません。
ここには2種類の繰り返しがあります。DAOのwhileはDBの結果をJavaのListにする処理、JSPのforはそのListをHTMLにする処理です。JSPで再びSQLを実行しているのではありません。
空欄・0件・DBエラーは、別々に扱う
| 状況 | 判明する場所 | 今回の表示・応答 |
|---|---|---|
| キーワードがない、空文字、空白だけ | Modelの入力チェック | 検索せず、入力を促す。HTTP 400。 |
| 買い物で検索して一致なし | DAOで0行。空のListが返る | 該当するタスクはありません。HTTP 200。 |
| tasksテーブルがないなど、検索失敗 | DAOからSQLException | 検索できませんでした。HTTP 500。 |
| JSPを直接開いた | tasksもerrorもない | 検索画面からの入力を促す。DB検索はしない。 |
検索結果が0件でも、検索処理自体は正常に完了しています。詳細画面で指定した1件が存在しない場合とは違い、この検索一覧では404にしません。逆に、DBの故障を「0件でした」と表示すると、利用者は本当にデータがないと誤解してしまいます。
入力を変えても検索結果が変わらないとき
- フォーム:name=”keyword”とgetParameter(“keyword”)の名前が一致しているか。
- Servlet・Model:受け取ったkeywordを渡しているか。search(“資料”)のような固定値になっていないか。
- DAO:SQLにWHERE title LIKE ?があり、setStringで今回の入力を設定しているか。
- DAO:whileの中でTaskを作って追加しているか。最初の1件しか読んでいない状態ではないか。
- JSP:getAttribute(“tasks”)のListを表示しているか。固定の一覧を書いていないか。
確認問題
- 「資料」を入力した場合、setStringで渡す文字列は何ですか。冒頭の3件のうち、どのIDが検索結果になりますか。
- 検索結果が2件あるとき、DAOでif (rs.next())とするのではなくwhileを使う理由と、new Taskをwhileの中に置く理由を説明してください。
- 入力が空白だけの場合、「買い物」で検索して0件の場合、tasksテーブルがなく検索できない場合について、どこで判明し、何を表示するかを整理してください。
- フォームのnameがkeywordなのにgetParameter(“title”)と書いた場合と、ServletでsetAttribute(“tasks”, tasks)したのにJSPでgetAttribute(“task”)と書いた場合は、それぞれどうなりますか。正しい名前も答えてください。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- フォームのnameを使って入力を受け取り、Model・DAOへ検索条件を渡す。
- LIKE ?に%キーワード%を設定すると、前後に文字があっても一致する。
- DAOは一致した各行をTaskにし、Listとして返す。0件なら空のList。
- Servletがtasks属性に保存し、JSPがListから結果一覧のHTMLを作る。
- 入力不正、検索結果0件、DB処理の失敗を区別する。