JavaのWebアプリとデータベースはどうつながる?Servlet・Model・DAOの役割と処理の流れ

画面の「登録」を押すと、入力した内容がデータベースに保存される。この仕組みを作るとき、HTMLのフォームとSQLの書き方は分かっても、その間をどのクラスでつなげればよいか迷うことがあります。
この記事の構成では、Servletが入力を受け取り、Modelが内容を確認して、DAOに保存を依頼します。DAOがJDBCを使ってSQLを実行し、処理結果はModel、Servletへ戻ります。最後にServletがJSPへ結果を渡し、JSPがブラウザに表示するHTMLを作ります。
役割を分けると、入力ルールを変えるときはModel、SQLを変えるときはDAO、画面の見た目を変えるときはJSPというように、変更する場所を判断しやすくなります。今回はタスクを1件登録する例で、値と呼び出し先を順番に追います。
本記事は、Schoo「Java入門 中級」第7回「データベースと連携したWebアプリケーションの実装」を学んだあとに理解を深めるステップアップ学習記事です。未受講の方も、入力例・コード・保存後のDB・表示結果を記事内で追えるようにしています。コードは各役割を説明するための抜粋です。授業の配布ファイルや実行環境を用意する必要はありません。
まず、画面で何が起きれば完成なのかを決める
- 入力画面でタスク名に「資料を確認する」と入力し、登録ボタンを押す。
- DBのtasksテーブルに、入力したタスク名が1件保存される。
- ブラウザに「1件登録しました。」と表示される。
タスク名が未入力・空白だけ・50文字超の場合は保存せず、入力エラーを表示します。この例の文字数はJavaのString.length()で判定します。同じ名前での登録は許可します。
正常に登録できたあとのDBは、たとえば次の状態です。これは空のテーブルへ最初に登録した場合の例で、IDはDBが割り当てます。
tasksテーブル id title 1 資料を確認する
画面に表示する「1件」は今回のSQLで登録した件数です。IDが2や3のタスクを登録した場合も、その操作で1件登録したなら結果は「1件」です。タスク名そのものは結果画面に再表示しません。
イラスト:フォームの入力がDBに保存されるまで

上から同じタスク名を追ってください。途中でStringからTaskオブジェクトにまとめていますが、newした時点では、まだDBには保存されていません。この例ではDAOがINSERT文を実行することで保存されます。
ServletからModelへ、ModelからDAOへ進む部分は、同じWebアプリ内のJavaのメソッド呼び出しです。クラスを一つ呼ぶたびに、ブラウザが別のリクエストを送るわけではありません。DAOからDBへの操作にはJDBCを使います。
Servlet・Model・DAO・DTO・JSPは、それぞれ何をする?
- Servlet(TaskServlet):リクエストからタスク名を取り出し、Modelに登録を依頼する。成功・失敗の結果をJSPへ渡す。
- Model(TaskModel):タスク名の入力ルールを確認する。登録に使うデータをDTOにまとめ、DAOへ依頼する。
- DAO(TaskDao):DBに接続し、INSERT文を実行する。今回登録した件数を返す。
- DTO(Task):タスク名をフィールドとして持ち、ModelからDAOへデータを渡す。SQLは実行しない。
- JSP(task-result.jsp):Servletから渡された結果を使い、ブラウザへ返すHTMLを作る。DBには接続しない。
ここでの「Model」は、アプリケーションの処理を担当するTaskModelクラスの呼び方です。MVC全体でいうModelはもっと広い概念で、DAOによるデータアクセスなども含めて考えます。「MVCのModelは必ず1クラス」「DAOはMVCと別の仕組み」という意味ではありません。
Task.java:今回、ModelからDAOへ渡すデータ
public class Task {
private String title;
public Task(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
new Task(“資料を確認する”)で、titleフィールドにその文字列を持つオブジェクトができます。getTitle()は同じ文字列を返します。このクラスにはDBへ保存する処理はありません。
今回は受け渡す項目がタスク名だけなので、DAOへStringを直接渡す設計もできます。ここでは、項目が増えた場合にもデータをひとまとまりで渡すDTOの使い方を確認します。DTOを使うこと自体が、DB保存の条件ではありません。
1.フォームからServletへタスク名を送る
task-form.htmlのフォーム部分
<form action="tasks" method="post" accept-charset="UTF-8">
<label for="taskTitle">タスク名</label>
<input id="taskTitle" name="title" type="text" required maxlength="50">
<button type="submit">登録</button>
</form>
登録ボタンを押すと、titleという名前で入力値を送ります。送信先のtasksは、このフォームと同じWebアプリの@WebServlet("/tasks")に対応します。POSTなので、ServletのdoPostが実行されます。
TaskServlet.javaの受け取り部分
request.setCharacterEncoding("UTF-8");
String title = request.getParameter("title");
HTMLのname="title"と、getParameter("title")の名前を一致させます。ここで取得するのは文字列で、この段階ではSQLは動いていません。
2.Modelが入力を確認し、DAOへ登録を依頼する
Servletはmodel.register(title)を呼びます。TaskModelのregisterメソッドは、次の順序で処理します。
TaskModel.javaのregisterメソッド
public int register(String title) throws SQLException {
if (title == null || title.isBlank() || title.length() > 50) {
throw new IllegalArgumentException("タスク名を1〜50文字で入力してください。");
}
Task task = new Task(title);
TaskDao dao = new TaskDao(dbPath);
int count = dao.insert(task);
return count;
}
- 入力ルールに合っているか確認する。合わなければ例外で呼び出し元へ通知する。
- タスク名を持つTaskオブジェクトを作る。
- DAOのinsertメソッドを呼び、Taskを引数として渡す。
- DAOから返った登録件数を、呼び出し元のServletへ返す。
dbPathは、この例で使うSQLiteのDBファイルの場所を表す文字列です。ModelとDAOのコンストラクタは、この値をフィールドに保持するものとします。Taskオブジェクトを作ること、DAOオブジェクトを作ること、DBへ登録することは別の処理です。new TaskDaoだけでINSERT文が実行されるわけではありません。
HTMLにもrequiredなどの入力チェックがありますが、ブラウザのチェックを通らない送信もできます。Model側でもルールを確認するため、不正な値ならDAOを呼ぶ前に処理が止まります。
3.DAOがSQLを実行し、DBに1行を追加する
DBにはtasksというテーブルがあり、IDを持つid列と、タスク名を持つtitle列がある例です。IDはDBが割り当てるため、次のINSERTではtitleだけを指定します。接続先のDBやテーブルは用意されているものとして、何を渡すと、どの行が保存されるかを読みます。
TaskDao.javaのSQL実行部分
String sql = "INSERT INTO tasks (title) VALUES (?)";
try (
Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)
) {
stmt.setString(1, task.getTitle());
int count = stmt.executeUpdate();
return count;
}
task.getTitle()でDTOからタスク名を取り出し、setString(1, ...)でSQLの1番目の?に値を設定します。タスク名をSQL文字列へ直接つなげず、SQLと値を分けて渡します。
executeUpdate()を呼ぶと、JDBCドライバを通じてDBがINSERT文を実行します。このINSERTで1行追加されると、戻り値はint型の1です。setStringだけではSQLは実行されません。
この例は、新しい接続の標準状態である自動コミットを使い、1回のINSERTを行います。処理が正常に完了すると、その変更が確定します。複数のSQLをまとめて成功・失敗させるトランザクション制御は、ここには含めていません。
try-with-resourcesにより、PreparedStatementとConnectionは使い終わると閉じられます。接続を閉じても、保存済みの行を消すわけではありません。DBファイルに保存したデータは、このリクエストの処理が終わったあとも残ります。
イラスト:登録件数が戻り、結果画面が表示されるまで

先ほどの図とは、追っているデータが違います。保存するときに渡したのはタスク名を持つTask、ここで返しているのは登録件数のintです。DAOがJSPを直接呼んだり、DBがHTMLを作ったりしているわけではありません。
4.Servletが結果をJSPへ渡す
TaskServlet.javaの正常時の流れ
TaskModel model = new TaskModel(dbPath);
int count = model.register(title);
if (count == 1) {
request.setAttribute("count", count);
}
request.getRequestDispatcher("/task-result.jsp").forward(request, response);
model.register(title)の処理中に、ModelからDAOが呼ばれます。DBへの登録が終わって件数が返ると、Servletはその続きを実行します。Modelを呼んだ直後に、結果を待たずにJSPへ進むわけではありません。
登録件数は、リクエスト属性"count"に保存します。フォームから送られてきたリクエストパラメータ"title"と、サーバー側で追加するリクエスト属性"count"は別のものです。
task-result.jspの受け取り・表示部分
<%
Integer count = (Integer) request.getAttribute("count");
%>
<% if (count != null) { %>
<p><%= count %>件登録しました。</p>
<% } %>
setAttributeはオブジェクトを受け取るため、intの件数はIntegerに自動変換されて保存されます。JSPではIntegerとして取り出します。count属性がない場合もあるため、nullを確認しています。
このJSPもサーバー上で実行されます。ブラウザに届くのは、<p>1件登録しました。</p>というHTMLです。getAttributeやJavaのif文を、ブラウザが実行するわけではありません。
失敗したときは、成功の結果を返さない
- タスク名が不正:ModelがIllegalArgumentExceptionで通知する。DAOは呼ばず、Servletが入力エラーをJSPへ渡す。
- DB接続やSQLの実行で問題:SQLExceptionがDAOからModelを経てServletへ伝わる。Servletは成功件数ではなく、エラーメッセージをJSPへ渡す。
- JSPへ結果が届かない:setAttributeとgetAttributeの属性名、forward先のファイル名を確認する。DBへの保存が済んでいる可能性もあるため、確認せず何度も登録し直さない。
たとえばDBにtasksテーブルがなければ、SQL実行時にエラーになります。接続先のDBとテーブルを確認します。SQLExceptionを握りつぶして1を返すと、失敗したのに「1件登録しました」と表示されるため、そのような処理にはしません。
TaskServlet.java:例外を受け止める部分の例
try {
TaskModel model = new TaskModel(dbPath);
int count = model.register(title);
if (count == 1) {
request.setAttribute("count", count);
} else {
response.setStatus(500);
request.setAttribute("error", "登録結果を確認できませんでした。");
}
} catch (IllegalArgumentException e) {
response.setStatus(400);
request.setAttribute("error", "タスク名を1〜50文字で入力してください。");
} catch (SQLException e) {
getServletContext().log("タスク登録処理のDBエラー", e);
response.setStatus(500);
request.setAttribute("error", "登録処理を完了できませんでした。");
}
request.getRequestDispatcher("/task-result.jsp").forward(request, response);
正常時はcount属性に件数、失敗時はerror属性に固定メッセージを入れます。400は入力に問題がある場合、500はサーバー側の処理で問題があった場合のHTTPステータスです。詳しい例外の内容はサーバー側のログへ記録し、DBのパスやSQLなどをそのまま画面へ出しません。
task-result.jsp:成功と失敗を分けて表示する部分
<%
Integer count = (Integer) request.getAttribute("count");
String error = (String) request.getAttribute("error");
%>
<% if (error != null) { %>
<p><%= error %></p>
<% } else if (count != null) { %>
<p><%= count %>件登録しました。</p>
<% } else { %>
<p>入力画面からタスクを登録してください。</p>
<% } %>
error属性があれば、そのメッセージを表示します。ここに入れるのはServletで決めた固定メッセージであり、タスク名などの入力文字列はHTMLへ出力していません。
なぜServletやJSPに、全部まとめて書かないのか
ServletにSQLを書くこと自体がJavaの文法違反になるわけではありません。ただし、受け取り・入力ルール・SQL・画面表示を同じ場所に詰め込むと、何を直すべきか分かりにくくなります。今回の分担なら、次のように判断できます。
- タスク名の入力ルールを変える:まずModelを確認し、フォームの入力制約も合わせる。
- 登録先のテーブルやSQLを変える:DAOを確認する。項目自体が変わるならDTOや画面も見直す。
- 結果の文言やレイアウトを変える:JSPを確認する。
- フォームの送信先や結果画面の遷移先を変える:Servletとフォームの対応を確認する。
また、ModelとDAOの動作は、ブラウザを使わずJavaのテストから確認できます。画面がまだできていなくても、入力が正しければ保存されるか、不正なら保存されないかを確かめられます。役割を分けるのは、クラスの数を増やすためではなく、変更と確認をしやすくするためです。
2回登録したとき、DBと画面はそれぞれどう変わる?
空のtasksテーブルに「資料を確認する」、次に「議事録を作る」を1件ずつ登録したとします。どちらも成功した場合を、記事内の結果で比べてみましょう。
1回目の登録後
DBのtasksテーブル id title 1 資料を確認する ブラウザの表示 1件登録しました。
2回目の登録後
DBのtasksテーブル id title 1 資料を確認する 2 議事録を作る ブラウザの表示 1件登録しました。
DBの行数は1行から2行へ増えましたが、2回目に追加した行は1行なので、画面は今回も「1件」です。INSERTの戻り値は、その操作で登録した件数であって、DB全体の行数やタスク一覧ではありません。上の表は保存後の状態を説明したもので、JSPに一覧を表示しているわけではありません。
ブラウザを閉じて再び開いても、DBへ保存した行は残ります。一方、count属性はそのリクエストのための一時的なデータです。結果JSPだけを直接開くと、登録処理は実行されず、「入力画面からタスクを登録してください。」と表示されます。
この例は処理のつながりを示すため、登録後にforwardしています。結果画面の再読み込みでPOSTを再送信すると、同じ内容を再登録する可能性があります。実際のアプリでは、登録後のリダイレクトや二重登録防止、認証・権限制御・CSRF対策なども別途設計します。掲載した抜粋を、そのまま公開サービスの完成形として扱うものではありません。
確認問題
- 「タスク名が空白だけの場合は登録しない」というルールと、INSERT文を実行する処理は、それぞれ今回のどのクラスに書きますか。
- Modelで
new Task(title)とnew TaskDao(dbPath)まで実行しました。これだけでDBに行は追加されますか。今回のコードでSQLを実行するメソッド名も答えてください。 - 空のtasksテーブルに異なるタスクを2回、1件ずつ登録しました。どちらも成功しています。2回目の画面は「1件登録しました」「2件登録しました」のどちらですか。DB内の行数と区別して説明してください。
- DAOのSQL実行でSQLExceptionが通知されました。どのクラスが受け止めて画面用の結果を用意しますか。また、失敗したのに1を返してはいけない理由を答えてください。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- Servletが入力を受け取り、Modelがルールを確認し、DAOがDB操作を担当する。
- DTOをnewしただけでは保存されない。今回の保存はDAOのexecuteUpdateによるINSERTで行う。
- DAOの戻り値はModel、Servletへ戻る。Servletはリクエスト属性に入れてJSPへ渡す。
- JSPはHTMLを作り、ブラウザはそのHTMLを表示する。
- DBに残るデータと、リクエスト中だけのデータを区別して確認する。