Javaで削除機能を作るには?選んだデータを削除する仕組み

最終更新日

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

一覧で選んだタスクだけを削除したい。そのためにサーバーへ送るのは、画面の「2行目」ではなく、そのタスクのIDです。サーバーは受け取ったIDをSQLの条件に指定し、DBから対象の行を削除します。

この記事では、ID=2の「議事録を作る」を例に、対象を確認する画面 → 削除ボタン → Servlet・Model・DAO → 削除後の一覧をつなげます。ボタンを押す前と後で、どの値が送られ、何が変わるのかを見ていきましょう。

本記事は、Schoo「Java入門 中級」第7回「データベースと連携したWebアプリケーションの実装」を学んだあとに、理解を深めたい方向けの学習記事です。未受講の方も、以下のデータと処理ごとのコードを読み比べられます。

削除では「どのデータか」をIDで指定する

SQLiteのtasksテーブルに、次の3件が入っている例です。idは各タスクを区別する主キー、titleはタスク名、statusは状態です。この記事では、この固定データを使います。

idtitlestatus
1資料を確認する未着手
2議事録を作る未着手
3会議資料を作る進行中

ID=2を削除すると、その行全体がなくなり、3件から2件になります。statusを「完了」に変える更新とは違います。ID=1とID=3の行は、そのまま残ります。

画面で並び替えや検索をすると、表示される順番は変わります。また、タスク名が同じデータが複数あることも考えられます。行の位置やタスク名ではなく、一意に決まるIDで削除対象を指定します。

イラスト:確認したIDを送って、その行だけを削除する

GETでID2の削除確認画面を開く。この時点では削除しない。削除するボタンからid=2をPOSTでServletに送り、Model、DAOへ渡す。DELETE文のWHERE idの条件に2を設定し、ID2の行だけを削除する。一覧をGETで再取得すると、ID1とID3の2件が残る。
画面で確認するID、POSTで送るID、WHEREに指定するIDを一致させます。

図は、確認画面を表示したところから、削除後に一覧を読み直すまでを示しています。ブラウザがSQLを送ってDBへ直接接続するわけではありません。Servletがリクエストを受け、ModelがIDを確認し、DAOがDBを操作します。図のURLはアプリのパスを省略しています。

操作リクエストDBへの処理
削除確認のリンクを押すGET task-delete?id=2SELECTで対象を読む。まだ削除しない。
確認画面でキャンセルするGET task-list一覧を読む。削除しない。
「削除する」を押すPOST task-delete、id=2を送信DELETEで指定した行を削除する。
結果画面から一覧に戻るGET task-listSELECTで削除後のデータを読み直す。

1.GETで削除する前のデータを確認する

一覧のID=2の行に、次のリンクを付けます。ここで行うのは削除ではなく、確認画面を開くことです。

<a href="task-delete?id=2">削除の確認</a>

実際に一覧を繰り返し表示するJSPでは、固定の2ではなく、その行のTaskからIDを取り出します。

<a href="task-delete?id=<%= task.getId() %>">削除の確認</a>

TaskDeleteServletは@WebServlet("/task-delete")で対応付けます。アプリのパスが/tasks-appなら、リンク先は/tasks-app/task-delete?id=2です。

TaskDeleteServlet.java:doGet

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    String idText = request.getParameter("id");
    String dbPath = getServletContext().getRealPath("/WEB-INF/db/tasks.db");
    TaskDeleteModel model = new TaskDeleteModel(dbPath);
    try {
        Task task = model.findTask(idText);
        if (task == null) {
            response.sendError(404);
            return;
        }
        request.setAttribute("task", task);
        request.getRequestDispatcher("/task-delete.jsp").forward(request, response);
    } catch (IllegalArgumentException e) {
        response.sendError(400);
    } catch (SQLException e) {
        getServletContext().log("Task load failed", e);
        response.sendError(500);
    }
}

取得したTaskをtask属性へ保存し、JSPへforwardします。DBに対象がなければ404で終了します。このdoGetから削除用のメソッドは呼びません。GETは表示や取得に使い、リンクを開いただけでデータが消える処理にはしないためです。

この例は、展開されたWebアプリのWEB-INF/db/tasks.dbをSQLiteのDBファイルとして使います。getRealPathはその実際の場所を得るメソッドです。配備方式によってはnullになるため、運用環境では接続先・保存場所を別途設定します。JSPはWEB-INFではなく、アプリ直下に置きます。

TaskDeleteModel.java:findTask

public Task findTask(String idText) throws SQLException {
    int id = convertId(idText);
    TaskDeleteDao dao = new TaskDeleteDao(dbPath);
    return dao.findById(id);
}

convertIdはIDを正の整数に変換するメソッドです。コードは後で示します。DAOは同じIDで検索し、確認画面に出す1件を返します。

TaskDeleteDao.java:findById

public Task findById(int id) throws SQLException {
    String sql = "SELECT id, title, status FROM tasks WHERE id = ?";
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
         PreparedStatement stmt = conn.prepareStatement(sql)) {
        stmt.setInt(1, id);
        try (ResultSet rs = stmt.executeQuery()) {
            if (rs.next()) {
                return new Task(rs.getInt("id"), rs.getString("title"),
                        rs.getString("status"));
            }
        }
    }
    return null;
}

このTaskはid・title・statusを保持し、getId()・getTitle()・getStatus()で取り出せるDTOです。確認画面ではIDだけでなく名前も表示し、利用者が対象を判断できるようにします。ModelとDAOは、コンストラクタで受け取ったdbPathをフィールドへ保存します。次はModelの例です。

private final String dbPath;

public TaskDeleteModel(String dbPath) {
    this.dbPath = dbPath;
}

DAOも同じ形で、コンストラクタ名をTaskDeleteDaoにします。この記事のクラスはexampleパッケージに置く例です。

2.確認画面からhiddenのIDをPOSTで送る

確認画面には、対象のIDとタスク名、「削除する」と「キャンセル」を用意します。表示だけでは削除せず、利用者が「削除する」を押したときに送信します。

ここで表示するタスク名は、冒頭の固定データです。JSPの<%= ... %>は自動でHTMLエスケープされません。利用者が登録した名前などを表示するアプリでは、出力時のHTMLエスケープが必要です。

task-delete.jsp:削除確認画面

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="example.Task" %>
<%
Task task = (Task) request.getAttribute("task");
if (task == null) {
    response.sendError(404);
    return;
}
%>
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>削除の確認</title></head>
<body>
<h1>削除の確認</h1>
<p>ID:<%= task.getId() %></p>
<p><%= task.getTitle() %></p>
<p>このタスクを削除しますか?</p>
<form action="task-delete" method="post">
    <input type="hidden" name="id" value="<%= task.getId() %>">
    <button type="submit">削除する</button>
</form>
<p><a href="task-list">キャンセル</a></p>
</body>
</html>

Servletが設定したtask属性をJSPが取り出し、画面を作ります。JSPを直接開いてtaskがない場合は404です。ID=2のとき、hiddenの部分は次のHTMLになります。

<input type="hidden" name="id" value="2">

name="id"がパラメータ名、value="2"が送る値です。「ID:2」と文字を表示するだけでは送信されません。hiddenは入力欄を見せずに値をフォームに含めるために使います。今回のフォームはタスク名や状態を送らず、idだけを送ります。

キャンセルは一覧へのリンクです。POSTせずに確認画面を離れるため、削除処理は動きません。ただし、確認画面は利用者の誤操作を減らすための画面であり、不正なリクエストを防ぐ仕組みではありません。hiddenのIDも変更できるため、削除してよい利用者かどうかなどはサーバー側で確認する必要があります。

3.ServletでPOSTを受け、削除件数を確認する

GETで確認画面を開いたリクエストと、削除ボタンで送るPOSTは別です。GETで使ったtask属性が、そのままPOSTへ残るわけではありません。POSTでもhiddenから送られたidを改めて受け取ります。

TaskDeleteServlet.java:doPost

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");
    String idText = request.getParameter("id");
    String dbPath = getServletContext().getRealPath("/WEB-INF/db/tasks.db");
    TaskDeleteModel model = new TaskDeleteModel(dbPath);
    try {
        int count = model.deleteTask(idText);
        if (count == 0) {
            response.sendError(404);
            return;
        }
        request.setAttribute("count", count);
        request.getRequestDispatcher("/task-delete-result.jsp").forward(request, response);
    } catch (IllegalArgumentException e) {
        response.sendError(400);
    } catch (SQLException e) {
        getServletContext().log("Task delete failed", e);
        response.sendError(500);
    }
}

getParameter("id")で得る値は文字列の”2″です。ModelのdeleteTaskへ渡すと、最終的にDAOが削除した件数を返します。対象がなければ0、今回の主キー指定で1行を削除できれば1です。0件を「削除しました」とは表示せず、この例では404を返します。

処理のつながりを追うため、成功時はcount属性を設定して結果JSPへforwardしています。入力が不正なら400、SQL処理に失敗すれば500です。例外の詳細はログへ残し、DBの場所やSQLのエラー内容を画面へ表示しません。

4.ModelでIDを確認してからDAOへ渡す

TaskDeleteModel.java:deleteTask

public int deleteTask(String idText) throws SQLException {
    int id = convertId(idText);
    TaskDeleteDao dao = new TaskDeleteDao(dbPath);
    return dao.deleteById(id);
}

削除に必要なのは対象のIDです。更新のように新しい状態を渡したり、削除用にタスク名を組み立てたりする必要はありません。

TaskDeleteModel.java:convertId。確認用のGETと削除用のPOSTで共通に使います。

private int convertId(String idText) {
    int id;
    try {
        id = Integer.parseInt(idText);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("IDが正しくありません。");
    }
    if (id <= 0) {
        throw new IllegalArgumentException("IDが正しくありません。");
    }
    return id;
}

IDが送られていない、数字ではない、intの範囲を超える、0以下といった場合は、DAOへ進みません。正の整数に変換できても、そのIDが存在するとは限りません。入力の形式を確認する処理と、DBに対象があるかを確認する処理は別です。

5.WHEREにIDを指定して、その1行を削除する

TaskDeleteDao.java:deleteById

public int deleteById(int id) throws SQLException {
    String sql = "DELETE FROM tasks WHERE id = ?";
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
         PreparedStatement stmt = conn.prepareStatement(sql)) {
        stmt.setInt(1, id);
        return stmt.executeUpdate();
    }
}
コード意味
DELETE FROM taskstasksテーブルの行を削除する
WHERE id = ?削除する行をIDで限定する
stmt.setInt(1, id)1番目の?へ、対象のIDを設定する
stmt.executeUpdate()DELETEを実行し、削除した件数を返す

ID=2なら、設定するコードはstmt.setInt(1, 2)です。最初の1は?の番号であり、削除するIDではありません。ID=3を削除する場合はstmt.setInt(1, 3)になります。

setIntは値を設定するだけです。DBの行が削除されるのはexecuteUpdateでSQLを実行したときです。DELETEというSQLでも、JDBCではexecuteUpdateを使います。戻り値は削除したIDでも残り件数でもありません。

WHEREを省くと、テーブルの全行が削除対象になります。PreparedStatementを使うだけで「選んだ1件だけ」になるわけではありません。値の渡し方に加えて、SQLの条件自体が必要です。

この例は、新しい接続の標準状態である自動コミットで1回のDELETEを実行します。削除後にブラウザの「戻る」を押しても、DBの行は復元されません。業務によっては完全に消さず、削除済みの印を付けて残す設計もありますが、この記事は行そのものを削除する例です。

6.結果を表示し、一覧をDBから読み直す

task-delete-result.jsp:削除結果

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
Integer count = (Integer) request.getAttribute("count");
if (count == null) {
    response.sendError(404);
    return;
}
%>
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>削除結果</title></head>
<body>
<h1>削除結果</h1>
<p><%= count %>件削除しました。</p>
<p><a href="task-list">一覧に戻る</a></p>
</body>
</html>

成功すると「1件削除しました。」と表示します。intのcountはsetAttributeで保存するときにIntegerへ自動変換されるため、JSPではIntegerとして受け取ります。「一覧に戻る」はtask-listへ新しいGETを送るリンクです。

一覧は、削除前に取得したListをそのまま表示せず、DAOから取り直します。DBでDELETEを実行しても、すでに作ってあるJavaのListや、ブラウザが表示中のHTMLから自動的に行が消えるわけではありません。

TaskListServlet.java:doGet。クラスには@WebServlet("/task-list")を付けます。

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    String dbPath = getServletContext().getRealPath("/WEB-INF/db/tasks.db");
    TaskDeleteModel model = new TaskDeleteModel(dbPath);
    try {
        List<Task> tasks = model.getTaskList();
        request.setAttribute("tasks", tasks);
        request.getRequestDispatcher("/task-list.jsp").forward(request, response);
    } catch (SQLException e) {
        getServletContext().log("Task list failed", e);
        response.sendError(500);
    }
}

TaskDeleteModel.java:getTaskList

public List<Task> getTaskList() throws SQLException {
    TaskDeleteDao dao = new TaskDeleteDao(dbPath);
    return dao.findAll();
}

TaskDeleteDao.java:findAll

public List<Task> findAll() throws SQLException {
    String sql = "SELECT id, title, status FROM tasks ORDER BY id";
    List<Task> tasks = new ArrayList<>();
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
         PreparedStatement stmt = conn.prepareStatement(sql);
         ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            tasks.add(new Task(rs.getInt("id"), rs.getString("title"),
                    rs.getString("status")));
        }
    }
    return tasks;
}

一覧JSPはtasks属性のListを受け取り、繰り返し表示します。ID=2を削除した後に取得し直すと、表示するデータは次の2件です。

idtitlestatus
1資料を確認する未着手
3会議資料を作る進行中

ID=3は、2に詰め直しません。IDは表示順の番号ではなく、そのデータを区別する値です。確認するときは「画面が2件になった」だけでなく、ID=2がなく、ID=1とID=3の内容が変わっていないことまで見ます。

確認後に対象が消えた場合と、DBエラーを区別する

確認画面を開いた後に、別の操作でID=2が削除される場合があります。画面に名前が残っていても、削除実行時にはもう対象がありません。GETのSELECTで見つかったからといって、後のPOSTで必ず削除できるわけではありません。

状況処理結果今回の応答
IDがない・不正なID整数への変換・正の値の確認で拒否400。DELETEは実行しない。
確認画面を開く時点で対象なしDAOのfindByIdがnullを返す404。確認画面を出さない。
削除する時点で対象なしDELETEの件数が0404。削除成功とは表示しない。
SQL処理そのものに失敗SQLException500。詳細をログに記録する。

すでに削除済みの場合に「対象は存在しません」と案内するなど、画面の仕様はアプリごとに決めます。大切なのは、0件とDB障害を同じ結果として扱わないことです。SQLExceptionをcatchして0を返してしまうと、その区別ができなくなります。

この結果画面はforwardなので、再読み込みでPOSTが再送信される可能性があります。同じIDの削除を再送すると、1回目が1件、2回目は0件です。実際のアプリでは、削除成功後に一覧へリダイレクトして、結果表示をGETにする構成も使います。ただし、それだけですべての二重送信を防げるわけではありません。

確認画面やPOSTだけで、安全な削除になるわけではない

このコードは、ログインのない学習用データで削除の流れを追う例です。公開する業務アプリへそのまま転用するものではありません。確認画面を経ずにPOSTすることも、hiddenのIDを変更することもできます。

  • 削除権限:ログイン中の利用者が、そのIDのデータを削除できるかをサーバー側で確認する。IDが正の整数というだけでは許可しない。
  • CSRF対策:別のサイトなどから利用者の意図しない削除リクエストを送られないよう対策する。POSTにするだけでは防げない。
  • 削除のルール:関連するデータも消してよいか、履歴を残す必要があるかを決める。確認画面を出すだけで解決する問題ではない。

確認問題

  1. 冒頭の3件からID=2を削除します。フォームで送るパラメータ名と値、DAOの戻り値、削除後に残るIDと件数を答えてください。
  2. 削除確認画面を開いてからキャンセルしました。どのHTTPメソッドで画面へ移動し、DBはどうなりますか。また、確認画面を開くリンクだけで削除する実装には、どんな問題がありますか。
  3. SQLがDELETE FROM tasks WHERE id = ?の場合、ID=3を削除するsetIntの1行を書いてください。第1引数の意味と、WHEREを省いた場合の影響も答えてください。
  4. ID=2の確認画面を開いた後、別の操作でその行が削除されました。「削除する」を押した場合と、tasksテーブル自体がなくSQL処理に失敗する場合について、DAOとServletの動きの違いを説明してください。

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

まとめ

  • 削除対象は、表示位置ではなくIDで指定する。
  • GETは確認画面の表示、POSTは削除の実行に分ける。
  • hiddenのIDをServletで受け取り、Modelで確認してDAOへ渡す。
  • DELETEのWHEREで対象を限定し、executeUpdateの件数を確認する。
  • 削除後は一覧を取得し直す。残ったデータのIDは詰め直さない。

関連記事

シェアする