
前回の記事「【Javaの基礎知識】JDBCを使ったDB連携の基本と実装手順」で作成したTodoアプリに、更新処理を追加します。さらに、更新時のオーバーヘッドを抑えるために「コネクションプール」を導入し、データベース接続の効率を向上させます。本記事では、Servlet/JSPを用いた実装手順をわかりやすく解説します。
【Javaの基礎知識】JDBCを使ったDB連携の基本と実装手順
Todoアプリの更新制御とは?
Todoアプリの更新処理を適切に制御することで、データの整合性を保ち、複数ユーザーが同時に操作しても正しい情報が維持されるようになります。更新処理を誤ると、データが競合したり、意図しない情報が上書きされる可能性があります。
本記事では、更新時のデータ競合を防ぐための設計や実装方法を解説し、ロストアップデートの回避、楽観ロックと悲観ロックの使い分け、適切なトランザクション管理などの重要なポイントを押さえます。
更新処理の課題と考慮すべきポイント
Todoアプリの更新処理を適切に設計しないと、データ競合が発生する可能性があります。特に以下の点を考慮する必要があります。
- 更新時のデータ競合(ロストアップデート問題)
複数のユーザーが同時に同じデータを更新した場合、後から更新されたデータが上書きされ、前の変更が消えてしまう問題が発生します。 - 楽観ロックと悲観ロックの考え方
データ競合を防ぐ方法として「楽観ロック」と「悲観ロック」があります。
- 楽観ロック: 更新前のデータのバージョン番号をチェックし、一致している場合のみ更新を許可する方法。
- 悲観ロック: データを更新する前にロックをかけ、他のユーザーが変更できないようにする方法。 - ステートレスなWebアプリでの更新制御
Webアプリは基本的に「ステートレス(状態を持たない)」ため、更新の一貫性を保つためには適切な設計が必要です。
更新処理の実装方法
更新処理を実装するには、以下の手順を考慮する必要があります。
- UPDATE 文を使った基本的な更新処理
SQLの UPDATE 文を使用してデータを更新します。例えば、以下のようなSQLを実行します。UPDATE todo SET title = ?, description = ?, updated_at = NOW() WHERE id = ?;
- 楽観ロックを用いた更新手順(バージョン管理)
データのバージョンを管理し、更新時にバージョン番号が一致しているか確認します。UPDATE todo SET title = ?, description = ?, updated_at = NOW(), version = version + 1 WHERE id = ? AND version = ?;
- 例外処理とトランザクション管理
更新時にエラーが発生した場合は、ロールバックしてデータの整合性を維持する必要があります。try {
connection.setAutoCommit(false);
// UPDATE 処理を実行
connection.commit();
} catch (Exception e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
コネクションプールとは?

コネクションプールとは、データベース接続を効率的に管理し、パフォーマンスを向上させる技術です。あらかじめ一定数のデータベース接続(コネクション)を作成し、それを使い回すことで、新たな接続を都度確立するオーバーヘッドを削減できます。
コネクションプールの仕組みとメリット
コネクションプールを導入することで、以下のようなメリットが得られます。
- データベース接続の負荷軽減
各リクエストごとに新しいコネクションを確立するのではなく、既存のコネクションを再利用することで、接続オーバーヘッドを削減できます。 - コネクションの再利用によるパフォーマンス向上
コネクションの確立と解放にかかる時間を削減し、アプリケーションのレスポンス速度を向上させます。 - 主要なコネクションプールライブラリ
現在、主に使用されるコネクションプールライブラリには以下のものがあります。- Apache Commons DBCP
- HikariCP(Spring Bootのデフォルト)
- C3P0
Apache DBCPを使ったコネクションプールの導入
Todoアプリの更新処理を実装しましたが、データベース接続の効率を考えると、まだ問題が残っています。
現在の実装では、SQLを実行するたびに新しくデータベース接続を作成し、処理が終わるたびに接続を閉じています。
なぜコネクションプールが必要なのか?
データベースに接続するたびに DriverManager.getConnection() を呼び出すと、以下の問題が発生します。
- 毎回新しい接続を確立するため、処理が遅くなる
- データベースサーバーの負荷が高くなり、パフォーマンスが低下する
- 大量のリクエストが発生すると、接続数の制限を超えてエラーが発生する可能性がある
特に、複数のユーザーが同時にアクセスするWebアプリケーションでは、**コネクションの作成・破棄を繰り返すことは非効率** です。
コネクションプールとは?
コネクションプールは、あらかじめ一定数のデータベース接続を作成し、それを使い回す仕組みです。
これにより、毎回新しい接続を作るオーバーヘッドを削減し、データベースの負荷を大幅に軽減できます。
Apache Commons DBCP を導入する理由
Java には複数のコネクションプール実装がありますが、今回は Apache Commons DBCP を利用します。
ライブラリ名 | 特徴 |
---|---|
Apache Commons DBCP | Tomcat などで広く使用されており、設定がシンプル |
HikariCP | パフォーマンスが高いが、設定がやや複雑 |
C3P0 | 設定の自由度が高いが、やや古い |
DBCP は設定が簡単で、Tomcat との相性が良いため、Servlet ベースのアプリケーションに適しています。
コネクションプールを動作させるために必要な JAR
Java でコネクションプールを導入するためには、以下の 4 つの JAR ファイルを「クラスパス」に追加する必要があります。
JAR ファイル | 役割 | ダウンロードリンク |
---|---|---|
commons-dbcp2-2.9.0.jar | コネクションプールの管理(Apache Commons DBCP) | ダウンロード |
commons-pool2-2.11.1.jar | DBCP 内部で使用するプール管理 | ダウンロード |
mysql-connector-j-8.0.32.jar | MySQL への接続ライブラリ | ダウンロード |
commons-logging-1.2.jar | DBCP 内部で使用するロギングライブラリ | ダウンロード |
Eclipse で JAR を「クラスパス」に追加する方法
JAR ファイルを正しく追加しないと、以下のようなエラーが発生します。
java.lang.ClassNotFoundException: org.apache.commons.dbcp2.BasicDataSource
このエラーを回避するために、以下の手順で「クラスパス(Classpath)」に JAR を追加してください。
- Eclipse のプロジェクトを右クリック → 「プロパティ」
- 「Java のビルド・パス」 → 「ライブラリー」タブを開く
- 「JAR の追加」ボタンをクリック
- 「WEB-INF/lib/」フォルダ内の JAR(`commons-dbcp2-2.9.0.jar` など)を選択
- 「クラスパス(Classpath)」を選択して追加(モジュールパスではない!)
- 「適用して閉じる」
- 「プロジェクト」→「ビルド・プロジェクト」を実行
- Tomcat をクリーン&再起動
JAR の追加でよくあるエラーと解決策
エラー | 原因 | 解決策 |
---|---|---|
java.lang.ClassNotFoundException: org.apache.commons.dbcp2.BasicDataSource | 「commons-dbcp2-2.9.0.jar」を追加していない、または「モジュールパス」に追加してしまった | 「クラスパス(Classpath)」に追加し直す |
java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory | 「commons-logging-1.2.jar」が `WEB-INF/lib/` にない | JAR をダウンロードし、クラスパスに追加する |
java.sql.SQLException: No suitable driver found for jdbc:mysql://... | 「mysql-connector-j-8.x.x.jar」が `WEB-INF/lib/` にない | MySQL Connector J をダウンロードして追加する |
クラスパスとモジュールパスの違いとは?
Java のプロジェクトでライブラリを追加する際、Eclipse では「クラスパス(Classpath)」と「モジュールパス(Modulepath)」のどちらかを選択する必要があります。しかし、この違いを理解していないと、適切に設定できず、 ClassNotFoundException などのエラーに悩まされることになります。
特に、Webアプリ開発(Servlet, JSP, Spring Boot など)では **「クラスパス」に統一すべき** ですが、Java 9 以降のモジュールシステムでは **「モジュールパス」を活用する場合もあります。**
クラスパス(Classpath)とは?
クラスパスは、従来の Java アプリケーションで使用されるライブラリ管理方式です。JAR をクラスパスに追加することで、Java のクラスローダーがそれを読み込めるようになります。
特徴 | 説明 |
---|---|
Java 8 以前の標準 | Java 8 以前は、すべての JAR を「クラスパス」に追加して管理していた |
Webアプリ向き | Servlet, JSP, Spring Boot などのフレームワークは「クラスパス」に統一すべき |
JAR の配置 | 一般的に WEB-INF/lib/ に JAR を配置し、Eclipse で「クラスパス」に追加する |
モジュールパス(Modulepath)とは?
Java 9 以降では、モジュールシステムが導入され、 module-info.java を使用して依存関係を明示的に管理する方法が推奨されています。モジュールパスを利用すると、モジュールごとにクラスを分離でき、より厳格な依存関係管理が可能になります。
特徴 | 説明 |
---|---|
Java 9 以降で使用 | モジュールシステムを採用する場合に「モジュールパス」を使う |
module-info.java が必要 | 明示的に依存関係を定義しないと JAR が認識されない |
一部のライブラリが非対応 | Apache DBCP など、モジュールパスでは動作しないライブラリがある |
更新制御とコネクションプールを組み合わせた実装
Todoアプリの更新処理を適切に制御し、データベース接続の負荷を軽減するために、コネクションプールを組み合わせた実装を行います。ここでは、JDBCを使ったDAOクラスの実装、Servletによるリクエスト処理、トランザクション管理について詳しく解説します。
【今回の改修対象ファイルは下記】
1 2 3 4 5 6 7 8 9 | 📂 src/ ├── 📂 com.example.todo/ │ ├── MySQLConnection.java 【修正】コネクションプール対応 │ ├── Todo.java 【修正】update_at フィールド追加 │ ├── TodoDAO.java 【修正】getAllTodos() / updateTodo() の修正 │ ├── TodoServlet.java 【修正】更新リクエスト処理の追加 │ ├── 📂 webapp/ │ ├── index.jsp 【修正】update_at を送信する hidden フィールド追加 |
コネクションプールを利用したDB接続クラスの改修
前回「【Javaの基礎知識】DockerでMySQL環境を構築!」で作成した MySQLConnection.java クラスでは、データベースへの接続を DriverManager を使って行っていました。
しかし、このままでは SQLを実行するたびに新しくデータベース接続を作成してしまい、パフォーマンスが低下 します。
ここでは、既存の MySQLConnection.java を修正し、Apache Commons DBCP を利用したコネクションプールを導入 します。
この修正により、取得した update_at の値とデータベースの update_at を比較することで、他のユーザーによるデータの変更を検知できるようになります。
もし update_at の値が異なっていた場合、別のユーザーがすでにデータを更新しているため、上書きを防ぐことが可能 です。
updateTodo() で update_at(更新時刻)を利用し、更新前の update_at とデータベースの update_at を比較することで、データの競合を防ぐ排他制御を実装 します。
これにより、他のユーザーがすでにデータを更新していた場合、誤って上書きすることを防ぐことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package com.example.todo; import java.sql.Connection; import java.sql.SQLException; import javax.sql.DataSource; import org.apache.commons.dbcp2.BasicDataSource; public class MySQLConnection { private static BasicDataSource dataSource = new BasicDataSource(); static { // データベース接続情報を設定 dataSource.setUrl("jdbc:mysql://localhost:3306/todo_db?serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("root"); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); // コネクションプールの設定 dataSource.setInitialSize(5); // 起動時に確保するコネクション数 dataSource.setMaxTotal(50); // プール内の最大コネクション数 dataSource.setMaxIdle(10); // アイドル状態で保持する最大コネクション数 dataSource.setMinIdle(2); // 最低限保持するアイドルコネクション数(追加) dataSource.setMaxWaitMillis(5000); // コネクションが不足した場合の最大待機時間(追加) } // DataSource を取得するメソッド public static DataSource getDataSource() { return dataSource; } // コネクションを取得するメソッド(追加) public static Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { // エラーメッセージをログに記録 e.printStackTrace(); // RuntimeException に変換して、Servlet 側でエラーメッセージを取得できるようにする throw new RuntimeException("データベース接続エラー: " + e.getMessage()); } } } |
1. MySQLConnection.javaの修正ポイント
- コネクションプールの導入
Apache Commons DBCP を使用し、コネクションプールを導入。
getConnection() メソッドでプールされたコネクションを取得し、効率的にデータベース接続を管理。 - 設定パラメータ
setInitialSize(5) → 起動時に確保するコネクション数を5に設定。
setMaxTotal(50) → プール内の最大コネクション数を50に設定。
setMaxIdle(10) → アイドル状態で保持する最大コネクション数を10に設定。
setMinIdle(2) → 最低限保持するアイドルコネクション数を2に設定。
setMaxWaitMillis(5000) → コネクション不足時の最大待機時間を5000ms(5秒)に設定。
設定パラメータの解説
- setInitialSize(5) → 起動時に確保するコネクション数
- setMaxTotal(50) → プール内の最大コネクション数
- setMaxIdle(10) → アイドル状態で保持する最大コネクション数
- setMinIdle(2) → 最低限保持するアイドルコネクション数
- setMaxWaitMillis(5000) → コネクションが不足した場合の最大待機時間
コネクションリークを防ぐ close() の重要性
コネクションプールを適切に運用するには、データベース接続を使用した後に close() を呼び出すことが重要です。
ただし、 MySQLConnection.getConnection() では close() を呼びません。なぜなら、接続を開く役割を持つため、ここで try-with-resources を使うとすぐに接続が閉じてしまうからです。
どこで try-with-resources を使うべきか?
- データベース接続を取得する MySQLConnection.java では使わない
- 実際に SQL を実行する DAOクラス(TodoDAO.java など)で使う
DAOクラスでの適切な使用例
public List<Todo> getAllTodos() {
List<Todo> todos = new ArrayList<>();
String sql = "SELECT id, title FROM todo_items";
try (Connection conn = MySQLConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
todos.add(new Todo(rs.getInt("id"), rs.getString("title")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return todos;
}
間違った使い方
以下のように try-with-resources を MySQLConnection.java で使ってしまうと、接続がすぐに閉じてしまい、DAOやServletで利用できなくなります。
public static Connection getConnection() {
try (BasicDataSource dataSource = new BasicDataSource()) {
dataSource.setUrl("jdbc:mysql://localhost:3306/todo_db");
return dataSource.getConnection(); // すぐに閉じられる
} catch (SQLException e) {
throw new RuntimeException("データベース接続エラー", e);
}
}
この書き方だと、接続がすぐに閉じてしまい、DAOで取得したコネクションが使えなくなるため、上記の使い方は間違いです。
コネクションプールのデメリットと対策
デメリット | 対策 |
---|---|
プール内に使われない Connection が増えると、メモリを圧迫する | setMaxIdle() や setMinIdle() を適切に設定 |
コネクションが破損した場合、異常な Connection がプールに残る | testOnBorrow=true で取得時に接続をチェック |
アクセスが少ないアプリでは、毎回 getConnection() する方が効率的 | setInitialSize() を低く設定 |
Todo クラスの改修(version フィールド追加)
Todoアプリの更新制御を実装するために、データの競合を防ぐ 楽観ロック を適用します。そのためには、データの変更履歴を管理するための version フィールドを追加する必要があります。
まず、 Todo クラスに version フィールドを追加します。これにより、データが変更されるたびに version の値がインクリメントされ、競合が発生した場合にエラーを検出できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package com.example.todo; import java.sql.Timestamp; public class Todo { private int id; private String title; private Timestamp updateAt; //【追加】更新時刻を管理 //【修正】コンストラクタ(update_at なし) public Todo(int id, String title) { this.id = id; this.title = title; } //【修正】コンストラクタ(update_at あり) public Todo(int id, String title, Timestamp updateAt) { this.id = id; this.title = title; this.updateAt = updateAt; } //【修正】ゲッター追加 public int getId() { return id; } public String getTitle() { return title; } public Timestamp getUpdateAt() { //【追加】更新時刻を取得 return updateAt; } //【修正】セッター追加 public void setUpdateAt(Timestamp updateAt) { //【追加】更新時刻をセット this.updateAt = updateAt; } } |
Todo.javaの修正ポイント
- update_at フィールドの追加
update_at(更新時刻)を Timestamp 型で追加。
新規作成時に CURRENT_TIMESTAMP を設定。
更新時に変更されることを反映。 - コンストラクタとゲッター・セッター
update_at を含む新しいコンストラクタを追加。
update_at にアクセスするためのゲッター・セッターを追加。
TodoアプリのDAOクラスの改修(更新メソッドの追加)
前回のプログラムでは、TodoDAO にデータ取得(getAllTodos())、追加(addTodo())、削除(deleteTodo())の基本的な機能を実装しました。しかし、更新処理(updateTodo())はまだ実装されておらず、複数のユーザーが同時にデータを編集した際の競合を防ぐ仕組みもありません。
そこで、本記事では データの競合を防ぐために update_at(更新時刻)を活用した排他制御を導入 し、updateTodo() を実装 していきます。具体的には、getAllTodos() に update_at を取得する処理を追加 し、updateTodo() では update_at の一致を条件に UPDATE を実行することで、他のユーザーによる変更があった場合に更新を防ぐ 仕組みを実装します。
JDBCを用いたデータ操作
以下のDAOクラスでは、データベースの更新処理を行うために updateTodo() メソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | package com.example.todo; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; public class TodoDAO { public List<Todo> getAllTodos() { List<Todo> todos = new ArrayList<>(); String sql = "SELECT id, title, update_at FROM todo_items ORDER BY id ASC"; try (Connection conn = MySQLConnection.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { int id = rs.getInt("id"); String title = rs.getString("title"); Timestamp updateAt = rs.getTimestamp("update_at"); todos.add(new Todo(id, title, updateAt)); } // デバッグ用: 取得件数を出力 System.out.println("デバッグ: 取得したTodo件数 = " + todos.size()); } catch (SQLException e) { e.printStackTrace(); } return todos; } public int updateTodo(int id, String title, Timestamp oldUpdateAt) { String sql = "UPDATE todo_items SET title = ?, update_at = NOW() WHERE id = ? AND update_at = ?"; try (Connection conn = MySQLConnection.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, title); pstmt.setInt(2, id); pstmt.setTimestamp(3, oldUpdateAt); // 更新前の `update_at` を条件にする int affectedRows = pstmt.executeUpdate(); // 更新成功時は 1, 失敗時は 0 を返す return affectedRows; } catch (SQLException e) { e.printStackTrace(); return 0; // エラー時は更新失敗(排他制御エラーを識別するため) } } public void addTodo(String title) { String sql = "INSERT INTO todo_items (title, update_at) VALUES (?, NOW())"; try (Connection conn = MySQLConnection.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, title); pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } public void deleteTodo(int id) { String sql = "DELETE FROM todo_items WHERE id = ?"; try (Connection conn = MySQLConnection.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } } |
3. TodoDAO.java
- getAllTodos() の修正
SELECT id, title, update_at FROM todo_items に変更し、update_at(更新時刻)も取得。
Todo オブジェクトに update_at をセットするように修正。 - updateTodo() の修正
更新前の update_at とデータベース内の update_at を比較し、一致した場合のみ更新を実行(排他制御)。
UPDATE 文に update_at = NOW() を追加し、更新時刻を更新。
競合した場合(update_at が異なる場合)は更新を拒否し、0 を返すように修正。 - addTodo() の修正
新規タスク追加時に update_at に CURRENT_TIMESTAMP を設定。 - deleteTodo() の修正
特に変更なし。update_at には影響しない。
TodoアプリのServletクラスの改修(更新処理の追加)
前回までのTodoServletクラスには、 doPost() メソッド内に、「追加」と「削除」処理処理しか実装されていませんでした。
今回は、クライアント(ブラウザやフロントエンドアプリ)から送信された「更新」リクエストを処理するために、TodoServletクラスへ更新処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | package com.example.todo; import java.io.IOException; import java.sql.Timestamp; import java.util.List; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; @WebServlet("/todo") public class TodoServlet extends HttpServlet { private TodoDAO todoDAO = new TodoDAO(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Todoリストを取得 List<Todo> todoList = todoDAO.getAllTodos(); // デバッグ: 取得できた件数を表示 System.out.println("デバッグ: 取得したTodo件数 = " + (todoList != null ? todoList.size() : "null")); // リストをリクエストスコープに設定 request.setAttribute("todos", todoList); // index.jsp にフォワード RequestDispatcher dispatcher = request.getRequestDispatcher("/index.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); HttpSession session = request.getSession(); // セッション取得 String action = request.getParameter("action"); String title = request.getParameter("title"); String updateIdStr = request.getParameter("update_id"); String updateAtStr = request.getParameter("update_at"); String deleteIdStr = request.getParameter("delete_id"); // 削除処理 if (deleteIdStr != null && !deleteIdStr.isEmpty()) { int deleteId = Integer.parseInt(deleteIdStr); todoDAO.deleteTodo(deleteId); session.setAttribute("message", "🗑️ 削除が完了しました。"); // 削除メッセージをセット response.sendRedirect(request.getContextPath() + "/todo"); return; } // 追加処理 if ("add".equals(action) && title != null && !title.trim().isEmpty()) { todoDAO.addTodo(title); session.setAttribute("message", "✅ 追加が完了しました。"); // 追加メッセージをセット response.sendRedirect(request.getContextPath() + "/todo"); return; } // 更新処理 if ("update".equals(action) && updateIdStr != null && !updateIdStr.isEmpty()) { int updateId = Integer.parseInt(updateIdStr); Timestamp updateAt = Timestamp.valueOf(updateAtStr); // update_at を比較 int result = todoDAO.updateTodo(updateId, title, updateAt); if (result == 0) { // 🛠 排他エラー → 成功メッセージをクリア session.setAttribute("errorMessage", "⚠️ 他のユーザーがデータを更新しました。最新の情報を取得してください。"); session.removeAttribute("successMessage"); // 成功メッセージを削除 } else { // 🛠 正常更新 → エラーメッセージをクリア session.setAttribute("message", "✅ 更新が完了しました。"); session.removeAttribute("errorMessage"); // エラーメッセージを削除 } response.sendRedirect(request.getContextPath() + "/todo"); return; } // 追加処理 if ("add".equals(action) && title != null && !title.trim().isEmpty()) { todoDAO.addTodo(title); response.sendRedirect(request.getContextPath() + "/todo"); return; } // エラー処理 session.setAttribute("errorMessage", "⚠️ 入力値が不正です。"); session.removeAttribute("successMessage"); // 成功メッセージを削除 response.sendRedirect(request.getContextPath() + "/todo"); } private void forwardToErrorPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher("/index.jsp"); dispatcher.forward(request, response); } } |
TodoServlet.javaの修正ポイント
- doPost() メソッドの修正
updateId, title, update_at をリクエストパラメータとして受け取り、updateTodo() を呼び出すように修正。
update_at を Timestamp に変換し、updateTodo() メソッドに渡して、排他制御を適用。
更新に失敗した場合、エラーメッセージをリクエスト属性にセットし、/index.jsp に表示。
削除処理も追加。deleteId を受け取り、deleteTodo() を呼び出してタスクを削除。
index.jsp の改修(更新時に update_at を送信)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | <%@ page contentType="text/html; charset=UTF-8" language="java" %> <%@ page import="java.util.List" %> <%@ page import="com.example.todo.Todo" %> <html> <head> <title>Todoリスト</title> <style> .error { color: red; font-weight: bold; } .success { color: green; font-weight: bold; } </style> </head> <body> <h1>Todoリスト</h1> <%-- メッセージの表示処理 --%> <% String errorMessage = (String) session.getAttribute("errorMessage"); String successMessage = (String) session.getAttribute("message"); if (errorMessage != null) { %> <p class="error">⚠<%= errorMessage %></p> <% session.removeAttribute("errorMessage"); // 表示後に削除 } if (successMessage != null && errorMessage == null) { // エラーメッセージがない場合のみ表示 %> <p class="success"><%= successMessage %></p> <% session.removeAttribute("message"); // 表示後に削除 } %> <%-- Todoの追加フォーム --%> <form action="todo" method="post"> <input type="hidden" name="action" value="add"> <input type="text" name="title" required> <button type="submit">追加</button> </form> <%-- Todoリストの表示 --%> <ul> <% List<Todo> todos = (List<Todo>) request.getAttribute("todos"); if (todos != null && !todos.isEmpty()) { for (Todo todo : todos) { %> <li> <form action="todo" method="post" style="display:inline;"> <input type="hidden" name="action" value="update"> <input type="hidden" name="update_id" value="<%= todo.getId() %>"> <input type="text" name="title" value="<%= todo.getTitle() %>" required> <input type="hidden" name="update_at" value="<%= todo.getUpdateAt() %>"> <button type="submit">更新</button> </form> <form action="todo" method="post" style="display:inline;"> <input type="hidden" name="delete_id" value="<%= todo.getId() %>"> <button type="submit">削除</button> </form> </li> <% } } else { %> <p>現在、登録されているTodoはありません。</p> <% } %> </ul> </body> </html> |
修正のポイント
index.jsp に update_at を送信する hidden フィールドを追加しました。
これにより、TodoServlet.java に update_at を渡し、更新時に排他制御を適用 できます。
- update_at を hidden フィールドとして追加し、送信
- 更新フォームを用意し、既存の title を編集可能に
- 削除・追加フォームはそのまま
MySQLの todo_items テーブルに update_at を追加
データの競合を防ぐために、 update_at カラムを追加し、更新時刻を記録できるようにします。 これにより、複数のユーザーが同時にデータを更新した際の排他制御を行うことが可能になります。
update_at カラムの追加
以下の SQL を実行し、 update_at カラムを追加します。
ALTER TABLE todo_items ADD COLUMN update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
解説
- update_at TIMESTAMP → 更新時刻を記録するカラム
- DEFAULT CURRENT_TIMESTAMP → 新しいデータが追加されたとき、現在の時刻を自動設定
- ON UPDATE CURRENT_TIMESTAMP → UPDATE 文が実行されたとき、自動的に最新の時刻に更新
追加後のテーブル構造
カラムが正しく追加されたか、以下の SQL で確認します。
DESC todo_items;
想定される出力:
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | int(11) | NO | PRI | NULL | auto_increment |
title | varchar(255) | NO | NULL | ||
completed | tinyint(1) | YES | 0 | ||
update_at | timestamp | YES | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
既存データの更新
既存のレコードに NULL がある場合、以下の SQL で update_at を更新します。
UPDATE todo_items SET update_at = CURRENT_TIMESTAMP WHERE update_at IS NULL;
- update_at カラムを追加し、データの競合を防ぐ仕組みを導入
- 新規追加時は CURRENT_TIMESTAMP を自動設定
- 更新時は ON UPDATE CURRENT_TIMESTAMP により自動更新
- 既存データの update_at が NULL の場合、 UPDATE 文で修正可能
Todoアプリの仕様について
本セクションでは、今回開発したTodoアプリの仕様について詳しく解説します。本アプリは、シンプルながらも実用的なタスク管理を目的とし、排他制御(楽観ロック)やエラーメッセージの表示、ユーザーフィードバックの強化など、利便性を向上させるための機能を実装しています。
以下の項目ごとに、プロジェクト構成、アプリの主な機能、仕様の詳細、削除・追加時の動作、そして実際の実行イメージを紹介していきます。これにより、アプリの設計や動作の流れを明確に把握できるようになります。
プロジェクト構成
前回の「【Javaの基礎知識】Servlet/JSPでTodoアプリを作ろう」から更新処理の追加を行いました。
改修したファイルは下記のとおりです。。
1 2 3 4 5 6 7 8 9 | 📂 src/ ├── 📂 com.example.todo/ │ ├── MySQLConnection.java 【修正】コネクションプール対応 │ ├── Todo.java 【修正】update_at フィールド追加 │ ├── TodoDAO.java 【修正】getAllTodos() / updateTodo() の修正 │ ├── TodoServlet.java 【修正】更新リクエスト処理の追加 │ ├── 📂 webapp/ │ ├── index.jsp 【修正】update_at を送信する hidden フィールド追加 |
Todoアプリの機能一覧
機能 | 概要 |
---|---|
📋 一覧表示 | データベースの todo_items テーブルから Todo リストを取得し、表示する |
➕ 新規追加 | フォームで新しい Todo を入力し、リストへ追加する |
✏️ 更新(編集) | 各Todoアイテムの「タイトル」を直接編集し、「更新」ボタンを押すと変更が反映される |
❌ 削除 | 各Todoアイテムの「削除」ボタンを押すと削除される |
🔒 排他制御(楽観ロック) | 複数ユーザーが同じデータを編集した際に「他のユーザーが更新しました」というエラーメッセージを表示し、データの競合を防ぐ |
Todoアプリの仕様
Todoアプリの最終的な仕様を下記に示します。改修している間にあれも欲しいな、これも欲しいなとなるのをグッと堪えてJava言語の理解に必要最低限の使用し絞っています。
🔒 排他制御(楽観ロック)の導入
- update_at(タイムスタンプ) を用いて更新チェックを実施
- 他のユーザーが先にデータを更新していた場合、処理をキャンセル
- 排他エラーが発生した場合はエラーメッセージを表示し、リストを最新化
⚠️ エラーメッセージの表示
- 処理失敗時は画面上部にエラーメッセージを表示
- エラーが発生してもTodoリストを再取得し、最新の情報を表示
📢 ユーザーフィードバックの強化
- 成功時・失敗時のメッセージをセッション管理
- session.setAttribute() でメッセージを保存し、 session.removeAttribute() で一度のみ表示
🎨 UIの改善
- 各Todo項目に「更新」ボタンを追加し、直接編集が可能
- メッセージの種類を統一し、視認性を向上
💬 メッセージ一覧
メッセージ種別 | 内容 |
---|---|
⚠️ 他のユーザーがデータを更新しました | 排他エラー時に表示 |
✅ 更新が完了しました | 正常に更新された場合 |
✅ 追加が完了しました | 新規追加が成功した場合 |
🗑 削除が完了しました | 削除が成功した場合 |
⚠️ 入力値が不正です | 不正データ入力時 |
削除・追加時の挙動
- 削除成功時に「削除が完了しました」と表示
- 追加成功時に「追加が完了しました」と表示
- エラー時はエラーメッセージを表示し、リストの最新状態を維持
この仕様により、データの整合性を確保しながら直感的に操作できるTodoアプリ を実現しました。 🚀
今回のTodoアプリの改修では、更新処理が実装されました。また、複数のユーザーが同時に更新を行った際に矛盾が生じないよう、排他処理も実装しました。処理の画面キャプチャを下記に貼り付けておきます。
データ更新処理のイメージ
- 初期画面表示
ブラウザから http://localhost:8080/todo にアクセスして、以下のようにTodoリストを表示する。 - 既存タイトルの更新
リスト表示のタイトルへ更新タイトルを入力して「更新」ボタンをクリック - リストデータ更新確認
リスト内のタイトル名が変更されている
排他制御エラーの確認
排他エラー操作手順
- ブラウザA で index.jsp を開き、Todo の編集フォーム を開く
- ブラウザB で 同じTodoを開き、編集フォームを開く
- ブラウザB で タイトル を変更し、「更新」ボタンを押す(正常に更新)
- ブラウザA で 何も変更せず「更新」ボタンを押す
- 排他制御が機能すれば、「他のユーザーが更新しました」とエラーになる

まとめ
今回の Todo アプリは Java の基本技術 を用いて作成されており、 プロジェクトで必須とされる 3 層システム(プレゼンテーション層、ビジネスロジック層、データアクセス層) の構成を採用しています。
また、本アプリの設計は他のプログラムでも 共通する開発方針 に沿っており、基本となる実装の上に バリデーションやチェック機能 を付加する形で応用されています。つまり、今回の実装を理解することで、今後の開発においても 一貫した設計・実装の方針を持つプログラム を構築できるようになります。
本記事では 更新処理の実装と排他制御の導入 を中心に解説しましたが、こうした基礎技術の積み重ねが、安定した Web アプリケーションの開発につながります。