Java の基礎知識

【Javaの基礎知識】Todoアプリの更新制御&コネクションプール導入!

前回の記事「【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 DBCPTomcat などで広く使用されており、設定がシンプル
HikariCPパフォーマンスが高いが、設定がやや複雑
C3P0設定の自由度が高いが、やや古い

DBCP は設定が簡単で、Tomcat との相性が良いため、Servlet ベースのアプリケーションに適しています。

コネクションプールを動作させるために必要な JAR

Java でコネクションプールを導入するためには、以下の 4 つの JAR ファイルを「クラスパス」に追加する必要があります。

JAR ファイル役割ダウンロードリンク
commons-dbcp2-2.9.0.jarコネクションプールの管理(Apache Commons DBCP)ダウンロード
commons-pool2-2.11.1.jarDBCP 内部で使用するプール管理ダウンロード
mysql-connector-j-8.0.32.jarMySQL への接続ライブラリダウンロード
commons-logging-1.2.jarDBCP 内部で使用するロギングライブラリダウンロード

Eclipse で JAR を「クラスパス」に追加する方法

JAR ファイルを正しく追加しないと、以下のようなエラーが発生します。

java.lang.ClassNotFoundException: org.apache.commons.dbcp2.BasicDataSource

このエラーを回避するために、以下の手順で「クラスパス(Classpath)」に JAR を追加してください。

  1. Eclipse のプロジェクトを右クリック → 「プロパティ」
  2. 「Java のビルド・パス」 → 「ライブラリー」タブを開く
  3. 「JAR の追加」ボタンをクリック
  4. 「WEB-INF/lib/」フォルダ内の JAR(`commons-dbcp2-2.9.0.jar` など)を選択
  5. 「クラスパス(Classpath)」を選択して追加(モジュールパスではない!)
  6. 「適用して閉じる」
  7. 「プロジェクト」→「ビルド・プロジェクト」を実行
  8. 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によるリクエスト処理、トランザクション管理について詳しく解説します。

【今回の改修対象ファイルは下記】

コネクションプールを利用した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. 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-resourcesMySQLConnection.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 の値がインクリメントされ、競合が発生した場合にエラーを検出できます。

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() メソッドを実装します。

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クラスへ更新処理を追加します。

TodoServlet.javaの修正ポイント

  • doPost() メソッドの修正
    updateId, title, update_at をリクエストパラメータとして受け取り、updateTodo() を呼び出すように修正。
    update_at を Timestamp に変換し、updateTodo() メソッドに渡して、排他制御を適用。
    更新に失敗した場合、エラーメッセージをリクエスト属性にセットし、/index.jsp に表示。
    削除処理も追加。deleteId を受け取り、deleteTodo() を呼び出してタスクを削除。

index.jsp の改修(更新時に update_at を送信)

修正のポイント

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_TIMESTAMPUPDATE 文が実行されたとき、自動的に最新の時刻に更新

追加後のテーブル構造

カラムが正しく追加されたか、以下の SQL で確認します。

DESC todo_items;

想定される出力:

FieldTypeNullKeyDefaultExtra
idint(11)NOPRINULLauto_increment
titlevarchar(255)NONULL
completedtinyint(1)YES0
update_attimestampYESCURRENT_TIMESTAMPon 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_atNULL の場合、 UPDATE 文で修正可能

Todoアプリの仕様について

本セクションでは、今回開発したTodoアプリの仕様について詳しく解説します。本アプリは、シンプルながらも実用的なタスク管理を目的とし、排他制御(楽観ロック)やエラーメッセージの表示、ユーザーフィードバックの強化など、利便性を向上させるための機能を実装しています。

以下の項目ごとに、プロジェクト構成、アプリの主な機能、仕様の詳細、削除・追加時の動作、そして実際の実行イメージを紹介していきます。これにより、アプリの設計や動作の流れを明確に把握できるようになります。

プロジェクト構成

前回の「【Javaの基礎知識】Servlet/JSPでTodoアプリを作ろう」から更新処理の追加を行いました。
改修したファイルは下記のとおりです。。

Todoアプリの機能一覧

機能概要
📋 一覧表示データベースの todo_items テーブルから Todo リストを取得し、表示する
新規追加フォームで新しい Todo を入力し、リストへ追加する
✏️ 更新(編集)各Todoアイテムの「タイトル」を直接編集し、「更新」ボタンを押すと変更が反映される
削除各Todoアイテムの「削除」ボタンを押すと削除される
🔒 排他制御(楽観ロック)複数ユーザーが同じデータを編集した際に「他のユーザーが更新しました」というエラーメッセージを表示し、データの競合を防ぐ

Todoアプリの仕様

Todoアプリの最終的な仕様を下記に示します。改修している間にあれも欲しいな、これも欲しいなとなるのをグッと堪えてJava言語の理解に必要最低限の使用し絞っています。

🔒 排他制御(楽観ロック)の導入

  • update_at(タイムスタンプ) を用いて更新チェックを実施
  • 他のユーザーが先にデータを更新していた場合、処理をキャンセル
  • 排他エラーが発生した場合はエラーメッセージを表示し、リストを最新化

⚠️ エラーメッセージの表示

  • 処理失敗時は画面上部にエラーメッセージを表示
  • エラーが発生してもTodoリストを再取得し、最新の情報を表示

📢 ユーザーフィードバックの強化

  • 成功時・失敗時のメッセージをセッション管理
  • session.setAttribute() でメッセージを保存し、 session.removeAttribute() で一度のみ表示

🎨 UIの改善

  • 各Todo項目に「更新」ボタンを追加し、直接編集が可能
  • メッセージの種類を統一し、視認性を向上

💬 メッセージ一覧

メッセージ種別内容
⚠️ 他のユーザーがデータを更新しました排他エラー時に表示
✅ 更新が完了しました正常に更新された場合
✅ 追加が完了しました新規追加が成功した場合
🗑 削除が完了しました削除が成功した場合
⚠️ 入力値が不正です不正データ入力時

削除・追加時の挙動

  • 削除成功時に「削除が完了しました」と表示
  • 追加成功時に「追加が完了しました」と表示
  • エラー時はエラーメッセージを表示し、リストの最新状態を維持

この仕様により、データの整合性を確保しながら直感的に操作できるTodoアプリ を実現しました。 🚀

今回のTodoアプリの改修では、更新処理が実装されました。また、複数のユーザーが同時に更新を行った際に矛盾が生じないよう、排他処理も実装しました。処理の画面キャプチャを下記に貼り付けておきます。

データ更新処理のイメージ

  1. 初期画面表示
    ブラウザから http://localhost:8080/todo にアクセスして、以下のようにTodoリストを表示する。

  2. 既存タイトルの更新
    リスト表示のタイトルへ更新タイトルを入力して「更新」ボタンをクリック

  3. リストデータ更新確認
    リスト内のタイトル名が変更されている

排他制御エラーの確認

排他エラー操作手順

  1. ブラウザA で index.jsp を開き、Todo の編集フォーム を開く
  2. ブラウザB で 同じTodoを開き、編集フォームを開く
  3. ブラウザB で タイトル を変更し、「更新」ボタンを押す(正常に更新)
  4. ブラウザA で 何も変更せず「更新」ボタンを押す
  5. 排他制御が機能すれば、「他のユーザーが更新しました」とエラーになる

まとめ

今回の Todo アプリは Java の基本技術 を用いて作成されており、 プロジェクトで必須とされる 3 層システム(プレゼンテーション層、ビジネスロジック層、データアクセス層) の構成を採用しています。

また、本アプリの設計は他のプログラムでも 共通する開発方針 に沿っており、基本となる実装の上に バリデーションやチェック機能 を付加する形で応用されています。つまり、今回の実装を理解することで、今後の開発においても 一貫した設計・実装の方針を持つプログラム を構築できるようになります。

本記事では 更新処理の実装と排他制御の導入 を中心に解説しましたが、こうした基礎技術の積み重ねが、安定した Web アプリケーションの開発につながります。

よく読まれている記事

1

IT入門シリーズ 🟢 STEP 1: ITの基礎を知る(ITとは何か?)├─【IT入門】ITとは?仕組みや活用方法をわかりやすく解説├─【IT入門】インターネットとは?仕組み・使い方を ...

2

「私たちが日々利用しているスマートフォンやインターネット、そしてスーパーコンピュータやクラウドサービス――これらの多くがLinuxの力で動いていることをご存じですか?無料で使えるだけでなく、高い柔軟性 ...

3

Shellスクリプト基礎知識(全13記事+2) ├─【Shellの基礎知識】Shellスクリプト入門|初心者が押さえる基本├─【Shellの基礎知識】変数と特殊変数の使い方|初心者向け解説├─【She ...

-Java の基礎知識