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

最終更新日

Schoo Java入門 中級 第7回 データベースと連携したWebアプリケーションの実装

画面の「登録」を押すと、入力した内容がデータベースに保存される。この仕組みを作るとき、HTMLのフォームとSQLの書き方は分かっても、その間をどのクラスでつなげればよいか迷うことがあります。

この記事の構成では、Servletが入力を受け取り、Modelが内容を確認して、DAOに保存を依頼します。DAOがJDBCを使ってSQLを実行し、処理結果はModel、Servletへ戻ります。最後にServletがJSPへ結果を渡し、JSPがブラウザに表示するHTMLを作ります。

役割を分けると、入力ルールを変えるときはModel、SQLを変えるときはDAO、画面の見た目を変えるときはJSPというように、変更する場所を判断しやすくなります。今回はタスクを1件登録する例で、値と呼び出し先を順番に追います。

本記事は、Schoo「Java入門 中級」第7回「データベースと連携したWebアプリケーションの実装」を学んだあとに理解を深めるステップアップ学習記事です。未受講の方も、入力例・コード・保存後のDB・表示結果を記事内で追えるようにしています。コードは各役割を説明するための抜粋です。授業の配布ファイルや実行環境を用意する必要はありません。

まず、画面で何が起きれば完成なのかを決める

  1. 入力画面でタスク名に「資料を確認する」と入力し、登録ボタンを押す。
  2. DBのtasksテーブルに、入力したタスク名が1件保存される。
  3. ブラウザに「1件登録しました。」と表示される。

タスク名が未入力・空白だけ・50文字超の場合は保存せず、入力エラーを表示します。この例の文字数はJavaのString.length()で判定します。同じ名前での登録は許可します。

正常に登録できたあとのDBは、たとえば次の状態です。これは空のテーブルへ最初に登録した場合の例で、IDはDBが割り当てます。

tasksテーブル
id  title
1   資料を確認する

画面に表示する「1件」は今回のSQLで登録した件数です。IDが2や3のタスクを登録した場合も、その操作で1件登録したなら結果は「1件」です。タスク名そのものは結果画面に再表示しません。

イラスト:フォームの入力がDBに保存されるまで

ブラウザがタスク名をPOSTで送る。Servletがtitleを取得し、Modelが確認してTaskオブジェクトを作る。DAOがTaskのtitleをSQLのパラメータに設定し、JDBC経由でDBのtasksテーブルに保存する。
入力値がServlet・Model・DAOを通って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;
}
  1. 入力ルールに合っているか確認する。合わなければ例外で呼び出し元へ通知する。
  2. タスク名を持つTaskオブジェクトを作る。
  3. DAOのinsertメソッドを呼び、Taskを引数として渡す。
  4. 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ファイルに保存したデータは、このリクエストの処理が終わったあとも残ります。

イラスト:登録件数が戻り、結果画面が表示されるまで

登録成功後、DAOのexecuteUpdateが返した件数1をModel、Servletへ返す。Servletがcount属性に保存してJSPへforwardし、JSPがHTMLを作る。ブラウザには1件登録しましたと表示される。
登録件数がDAO・Model・Servletへ戻り、JSPで表示される流れ

先ほどの図とは、追っているデータが違います。保存するときに渡したのはタスク名を持つ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対策なども別途設計します。掲載した抜粋を、そのまま公開サービスの完成形として扱うものではありません。

確認問題

  1. 「タスク名が空白だけの場合は登録しない」というルールと、INSERT文を実行する処理は、それぞれ今回のどのクラスに書きますか。
  2. Modelでnew Task(title)とnew TaskDao(dbPath)まで実行しました。これだけでDBに行は追加されますか。今回のコードでSQLを実行するメソッド名も答えてください。
  3. 空のtasksテーブルに異なるタスクを2回、1件ずつ登録しました。どちらも成功しています。2回目の画面は「1件登録しました」「2件登録しました」のどちらですか。DB内の行数と区別して説明してください。
  4. DAOのSQL実行でSQLExceptionが通知されました。どのクラスが受け止めて画面用の結果を用意しますか。また、失敗したのに1を返してはいけない理由を答えてください。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • Servletが入力を受け取り、Modelがルールを確認し、DAOがDB操作を担当する。
  • DTOをnewしただけでは保存されない。今回の保存はDAOのexecuteUpdateによるINSERTで行う。
  • DAOの戻り値はModel、Servletへ戻る。Servletはリクエスト属性に入れてJSPへ渡す。
  • JSPはHTMLを作り、ブラウザはそのHTMLを表示する。
  • DBに残るデータと、リクエスト中だけのデータを区別して確認する。

関連記事

シェアする