
前回の記事「【Javaの基礎知識】Todoアプリで学ぶ!JDBCを使ったDB連携と実装手順」で作成したTodoアプリに、更新処理を追加します。
さらに、更新時のオーバーヘッドを抑えるために「コネクションプール」を導入し、データベース接続の効率を向上させます。
【Javaの基礎知識】Todoアプリで学ぶ!JDBCを使ったDB連携と実装手順
Javaの基礎知識
Java の基礎知識(実践編)
📌 現場で使える力を。アプリ制作で学ぶ実践型トレーニング
└─【Javaの基礎知識(実践編)】現場で使えるWeb・DB・GUI開発の実践構築
├─【Javaの基礎知識】Javaとは? Javaの基本概要をわかりやすく解説!
├─【Javaの基礎知識】Eclipse+TomcatでWeb開発環境を構築!
├─【Javaの基礎知識】Todoアプリで学ぶ!ServletとJSPの基礎とWebアプリ開発
├─【Javaの基礎知識】DockerでMySQL環境構築|Javaから接続する手順
├─【Javaの基礎知識】Todoアプリで学ぶ!JDBCを使ったDB連携と実装手順
├─【Javaの基礎知識】Todoアプリで学ぶ!データ更新処理とコネクションプールの使い方
├─【Javaの基礎知識】設定地獄はもう嫌!シンプルな共通ログ出力クラスを作ってみた
├─【Javaの基礎知識】JavaFXでGUIアプリ作成入門!基本から実践まで!
├─【Javaの基礎知識】JavaFXで作るシンプルなエディタアプリ|保存・開く・編集
├─【Javaの基礎知識】Spring Boot環境構築&プロジェクトセットアップ完全ガイド!
├─【Javaの基礎知識】Spring BootでシンプルなMVC構造のWebアプリを作る
├─【Javaの基礎知識】Spring Boot × MySQL!DB接続からCRUD実装まで解説!|
├─【Javaの基礎知識】Spring Bootアプリの実行環境とデプロイ手順
|
└─共通DBアクセスクラス
📌 SQL記述を最小化、業務ロジックに集中できる共通基盤
├─ORMにはうんざり!第1回:シンプルなJava DBアクセスクラスを考えてみた
├─ORMにはうんざり!第2回:共通DBアクセスクラスでSQLを直感的に操作するJava設計
├─ORMにはうんざり!第3回:JavaでDB接続の最適化と共通プールの構築
├─ORMにはうんざり!第4回:Java共通ログ出力とsystem.xml設定の構成を解説
├─ORMにはうんざり!第5回:例外の闇を断つ 堅牢なJavaエラーハンドリングとログ設計
└─ORMにはうんざり!第6回:Java共通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 DBCPを使ったコネクションプールの導入
Todoアプリの更新処理を実装しましたが、データベース接続の効率を考えると、まだ問題が残っています。現在の実装では、SQLを実行するたびに新しくデータベース接続を作成し、処理が終わるたびに接続を閉じています。
なぜコネクションプールが必要なのか?
Webアプリケーションで複数ユーザーが同時にデータベースへアクセスする場合、毎回 DriverManager.getConnection() を使って新規接続を確立し、その都度切断する方法では大きな問題が発生します。
接続と切断の処理には時間がかかり、アクセスが集中すると処理が遅くなるだけでなく、データベースサーバーに余計な負荷がかかります。また、大量のリクエストが発生したときには接続数の制限を超えてエラーになる可能性もあります。
コネクションプールは、あらかじめ確立した複数のデータベース接続をプール(在庫)として保持し、必要なときにすぐ貸し出し、処理後は接続を切断せずにプールに返却して再利用できる仕組みです。
この仕組みにより接続確立の時間を大幅に削減でき、パフォーマンスが向上し、データベースサーバーへの負荷を抑えつつ安定した接続管理が可能になります。
コネクションプールを使わない場合のデメリット
- ✅ 毎回新しい接続を確立するため、処理が遅くなる
→ DriverManager.getConnection() は接続確立に時間がかかるため、リクエストのたびに呼ぶとレスポンスが遅くなる。 - ✅ データベースサーバーの負荷が高くなり、パフォーマンスが低下する
→ 接続・切断を繰り返すことでDBサーバーに負荷がかかり、CPU・メモリを無駄に消費する。 - ✅ 大量のリクエストが発生すると、接続数の制限を超えてエラーが発生する可能性がある
→ 同時接続が多発するとDBの接続上限を超え、接続拒否やエラー (Too many connections) が発生する。
特に、複数のユーザーが同時にアクセスする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 を追加してください。
JAR の追加でよくあるエラーと解決策
- エラー: ClassNotFound: org.apache.commons.dbcp2.BasicDataSource
原因: commons-dbcp2.jar 未追加、またはモジュールパスに誤追加
解決策: クラスパスに追加し直す - エラー: ClassNotFound: org.apache.commons.logging.LogFactory
原因: commons-logging.jar が WEB-INF/lib にない
解決策: ダウンロードしてクラスパスに追加 - エラー: SQLException: No suitable driver for jdbc:mysql://…
原因: mysql-connector-j.jar が WEB-INF/lib にない
解決策: MySQL Connector J を追加
クラスパスとモジュールパスの違いとは?
クラスパスとは、JavaがクラスファイルやJARファイルを探すための道であり、依存関係やアクセス制御を行わず、全てのクラスが相互参照できるのが特徴です。Servlet、JSP、Spring Boot、学習・副業・現場のWeb開発では、このクラスパスを使うのが基本です。
モジュールパスは、Java 9以降で導入されたモジュールシステムで使う道です。module-info.javaでモジュール宣言を行い、外部からアクセス可能なパッケージを制御したり、モジュール間の依存関係を解決するために使います。これは、大規模開発やモジュール化が必要な場合に限り使用します。
判断基準は「module-info.javaがプロジェクトに存在するかどうか」だけで十分です。存在しないならクラスパスを使う、存在するならモジュールパスを使う。この基準で迷うことはありません。
「Java 9以降はモジュールパスを使わなければならない」という情報がありますが、それは誤解です。モジュールシステムは選択制であり、使用しなければ従来通りクラスパスだけで動作します。
モジュール化しないなら必ずクラスパスにライブラリを追加する。モジュール化が必要な時だけモジュールパスを使う。これが正しい使い分けです。
この理解があれば、クラスパスとモジュールパスの選択で迷うことがなくなり、ClassNotFoundExceptionなどのエラーを回避して作業に集中できます。
クラスパス(Classpath)とは?

クラスパスは、従来の Java アプリケーションで使用されるライブラリ管理方式です。JAR をクラスパスに追加することで、Java のクラスローダーがそれを読み込めるようになります。
「Javaは実行時に、クラスやライブラリ(JARファイル)をどこに置いたら使えるのかを探しに行く“探し場所”をあらかじめ登録しておく必要があります」これが「パスを通す」ということです。つまり「パスを通す」とは、JVM(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によるリクエスト処理、トランザクション管理について詳しく解説します。
【今回の改修対象ファイルは下記】
📂 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 を比較することで、データの競合を防ぐ排他制御を実装 します。
これにより、他のユーザーがすでにデータを更新していた場合、誤って上書きすることを防ぐことができます。
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 など)で使う
「try-with-resources」とは?
Java 7 以降で導入された構文で:try() の括弧内でリソース(Connection, File, Scanner など)を宣言すると、try ブロックを抜けたときに自動的に close() が呼ばれる仕組みを指します。
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 の値がインクリメントされ、競合が発生した場合にエラーを検出できます。
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() メソッドを実装します。
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();
}
}
}
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クラスへ更新処理を追加します。
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 を送信)
<%@ 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アプリを作ろう」から更新処理の追加を行いました。
改修したファイルは下記のとおりです。。
📂 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 アプリケーションの開発につながります。
JDBCやWebアプリの基礎を学んだら、次は「JavaFXでGUIアプリを作成する方法」を学び、視覚的に動くアプリ作成スキルを身につけておきましょう。業務ツール・デスクトップアプリを自作できる力は案件獲得や副業の幅を広げる強力な武器になります。


