Skip to content
Code Blue プログラミング学習ノート
Java/Servlet/JSP

JSPウェブアプリ開発における接続プーリングとDAOパターンの実践

はじめに

ServletとJSP、JDBCの基本を学習した後の次のステップとして、本格的なウェブアプリケーション開発に必要な「接続プーリング」と「DAOパターン」について詳しく解説します。この知識は、パフォーマンスが高く保守性の良いアプリケーションを構築するために不可欠です。

接続プーリングの理解

接続プーリングとは

接続プーリングとは、データベース接続(Connection)を事前に作成してプール(池)として保持し、必要に応じてアプリケーションに貸し出し、使用後に返却させる仕組みです。

従来のJDBC接続方法(プーリングなし)は以下のとおりです。

// 従来の方法 - 毎回新しい接続を作成
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mydb", "user", "password");
// 処理実行
conn.close(); // 接続を閉じる

従来のJDBC接続では、リクエストが発生するたびにデータベースへの接続を新しく作成し、処理が終わるとその接続を閉じます。この方法は実装がシンプルで分かりやすい反面、毎回接続の確立と切断を繰り返すため、認証やリソースの確保に時間がかかり、処理速度が低下しやすくなります。

また、多数のユーザーが同時にアクセスするとデータベースへの接続数が急増し、サーバーへの負荷が高まる原因となります。さらに、一度使用した接続を再利用できないため、同じ処理を何度も繰り返すことになり、効率の悪い接続方法といえます。

接続プーリングのメリット

接続プーリングは、あらかじめデータベース接続を複数用意しておき、必要に応じて使い回す仕組みです。毎回接続を作成する必要がないため、処理速度が向上し、データベースへの負荷も軽減できます。また、接続数を適切に管理できるため、多くのユーザーが同時に利用しても安定した動作を実現できます。

  1. パフォーマンス向上: 接続の作成・破棄コストを削減
  2. リソース管理: 接続数を制限し、データベース過負荷を防止
  3. スケーラビリティ: 多数の同時リクエストに対応可能
  4. 接続の再利用: 既存の接続を再利用できる

接続プーリングの仕組み

接続プーリングでは、アプリケーションの起動時などにデータベース接続をあらかじめ生成し、プール(接続の保管場所)に保持しておきます。リクエストがあるとプールから接続を取得して処理を行い、処理終了後は接続を閉じるのではなくプールへ返却します。これにより、同じ接続を繰り返し利用できるため、高速かつ効率的にデータベースへアクセスできます。

[アプリケーション] ←貸出/返却→ [接続プール] ←接続→ [データベース]
  1. アプリケーション起動時に初期接続数をプールに作成
  2. アプリケーションが接続を要求すると、プールから接続を貸し出す
  3. アプリケーションが接続を使用後、プールに返却
  4. プールは返却された接続を再利用可能な状態に維持

Apache DBCPを使用した接続プーリング実装

MySQLでの接続プーリング実装例(Apache Commons DBCPを使用)は以下のとおりです。

  1. まず、必要なライブラリを追加(Mavenの場合)
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.9.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>
  1. 接続プールを管理するクラスを作成
import org.apache.commons.dbcp2.BasicDataSource;

public class DBCPDataSource {
    private static BasicDataSource dataSource = new BasicDataSource();

    static {
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false");
        dataSource.setUsername("user");
        dataSource.setPassword("password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // プール設定
        dataSource.setInitialSize(5);    // 初期接続数
        dataSource.setMaxTotal(20);     // 最大接続数
        dataSource.setMaxIdle(10);      // 最大アイドル接続数
        dataSource.setMinIdle(5);       // 最小アイドル接続数
        dataSource.setMaxWaitMillis(10000); // 接続取得待ち時間(ms)
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    private DBCPDataSource() {} // インスタンス化防止
}
  1. 使用例
try (Connection conn = DBCPDataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
     ResultSet rs = stmt.executeQuery()) {

    while (rs.next()) {
        // 結果処理
    }
} catch (SQLException e) {
    e.printStackTrace();
}
// conn.close()が呼ばれても、実際には接続はプールに返却される

接続プーリングのベストプラクティス

適切なプールサイズ設定

接続プールのサイズは、アプリケーションの利用状況に合わせて設定することが重要です。初期サイズは一般的に5〜10程度に設定されることが多く、最大サイズは同時に処理されるリクエスト数を考慮して調整します。必要以上に大きなプールはデータベースに負荷をかけ、小さすぎると接続待ちが発生するため、適切なサイズを設定することが重要です。

接続リーク防止

データベース接続は、使用後に必ずプールへ返却しなければなりません。そのため、try-with-resources 文を利用して接続を確実に閉じることが推奨されます。接続が返却されない状態が続くと、利用可能な接続がなくなり、新しいリクエストが処理できなくなる「接続リーク」が発生するため注意が必要です。

接続検証

接続プールから取得した接続が正常に利用できるかを確認することを接続検証といいます。長時間使用されていない接続は切断されている場合があるため、SELECT 1 のような簡単なSQLを実行して接続状態を確認し、有効な接続のみをアプリケーションへ渡すことで、エラーの発生を防ぐことができます。

タイムアウト設定

接続プールでは、接続待ち時間やアイドル状態の接続に対するタイムアウトを設定できます。接続取得待ち時間を設定することで、接続が取得できない場合に長時間待機し続けることを防げます。また、長時間使用されていないアイドル接続を自動的に解放することで、不要なリソース消費を抑え、接続プールを効率的に運用できます。

DAOパターンの理解

DAOパターンとは

DAO(Data Access Object)パターンは、データベース操作をカプセル化し、ビジネスロジックからデータアクセス層を分離するデザインパターンです。

従来の問題点

    従来の実装では、データベースへアクセスするSQL文をビジネスロジックの中に直接記述することが多くありました。そのため、データベースの仕様変更やSQLの修正が必要になった場合、複数のクラスやメソッドを修正しなければならず、保守性が低下します。また、同じようなデータアクセス処理を何度も記述することになり、コードの再利用性も低くなってしまいます。

    DAOパターンのメリット

    DAO(Data Access Object)パターンは、データベースへのアクセス処理を専用のクラスへまとめる設計手法です。これにより、ビジネスロジックとデータアクセスロジックを分離できるため、役割が明確になり、コードが読みやすくなります。また、データベースに関する変更はDAOクラスのみを修正すればよいため保守性が向上し、テストではDAOをモックに置き換えることでビジネスロジックだけを簡単に検証できます。さらに、共通のデータアクセス処理を複数の機能から再利用できるため、重複コードを減らし、開発効率の向上にもつながります。

    DAOパターンの基本構造

    DAOパターンでは、ビジネスロジック層はデータベースを直接操作するのではなく、DAOインターフェースを通じてデータの取得や保存を依頼します。

    [ビジネスロジック層] → [DAOインターフェース] ←実装→ [DAO実装クラス] → [データベース]

    実際のデータベース操作は、DAOインターフェースを実装したDAO実装クラスが担当します。このように役割を分離することで、ビジネスロジックはデータベースの詳細を意識する必要がなくなり、保守性や拡張性の高いプログラムを実現できます。

    DAOパターンの実装例

    1. エンティティクラスの作成

    エンティティクラス(Entity Class)とは、データベースのテーブルと1対1で対応するJavaクラスのことです。クラスの1つのインスタンスがテーブルの1行(レコード)を表し、データベースのデータをJavaプログラム上で扱うための「データの入れ物」として機能します。

    public class User {
        private int id;
        private String username;
        private String email;
        private Date createdAt;
    
        // コンストラクタ、ゲッター、セッター
        public User() {}
    
        public User(int id, String username, String email, Date createdAt) {
            this.id = id;
            this.username = username;
            this.email = email;
            this.createdAt = createdAt;
        }
    
        // getters and setters...
    }

    2. DAOインターフェースの定義

    JavaのDAO(Data Access Object)インターフェースとは、データベースへのアクセス処理(CRUD操作など)の窓口となるメソッド群を定義する仕様です。

    public interface UserDAO {
        // ユーザーを追加
        boolean insert(User user) throws SQLException;
    
        // IDでユーザーを取得
        User getById(int id) throws SQLException;
    
        // 全ユーザーを取得
        List getAll() throws SQLException;
    
        // ユーザーを更新
        boolean update(User user) throws SQLException;
    
        // ユーザーを削除
        boolean delete(int id) throws SQLException;
    }

    3. DAO実装クラス (MySQL用)

    DAO(Data Access Object)実装クラスとは、データベースとのデータやり取り(検索・追加・更新・削除)を専門に行うクラスです。データベース接続やSQLの実行といった処理をビジネスロジックから分離し、システムの保守性とテスト容易性を高めるために使用されます。

    public class UserDAOImpl implements UserDAO {
        private static final String INSERT_SQL = 
            "INSERT INTO users (username, email) VALUES (?, ?)";
        private static final String GET_BY_ID_SQL = 
            "SELECT * FROM users WHERE id = ?";
        private static final String GET_ALL_SQL = 
            "SELECT * FROM users";
        private static final String UPDATE_SQL = 
            "UPDATE users SET username = ?, email = ? WHERE id = ?";
        private static final String DELETE_SQL = 
            "DELETE FROM users WHERE id = ?";
    
        @Override
        public boolean insert(User user) throws SQLException {
            try (Connection conn = DBCPDataSource.getConnection();
                 PreparedStatement stmt = conn.prepareStatement(INSERT_SQL, 
                     Statement.RETURN_GENERATED_KEYS)) {
    
                stmt.setString(1, user.getUsername());
                stmt.setString(2, user.getEmail());
    
                int affectedRows = stmt.executeUpdate();
                if (affectedRows == 0) {
                    return false;
                }
    
                try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        user.setId(generatedKeys.getInt(1));
                    }
                }
                return true;
            }
        }
    
        @Override
        public User getById(int id) throws SQLException {
            try (Connection conn = DBCPDataSource.getConnection();
                 PreparedStatement stmt = conn.prepareStatement(GET_BY_ID_SQL)) {
    
                stmt.setInt(1, id);
                try (ResultSet rs = stmt.executeQuery()) {
                    if (rs.next()) {
                        return mapUser(rs);
                    }
                    return null;
                }
            }
        }
    
        // 他のメソッドも同様に実装...
    
        private User mapUser(ResultSet rs) throws SQLException {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setUsername(rs.getString("username"));
            user.setEmail(rs.getString("email"));
            user.setCreatedAt(rs.getTimestamp("created_at"));
            return user;
        }
    }

    4. DAOの使用例 (Servlet内)

    Servletにおいて、DAO(Data Access Object)とはデータベースへのアクセス処理(接続、SQL実行、切断)を別のクラスに切り分けるデザインパターンです。Servletはビジネスロジックと画面遷移の制御に専念し、データ操作はDAOに任せることで、保守性と可読性が向上します。

    @WebServlet("/users")
    public class UserServlet extends HttpServlet {
        private UserDAO userDao;
    
        @Override
        public void init() throws ServletException {
            super.init();
            userDao = new UserDAOImpl(); // DAOインスタンスを初期化
        }
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                throws ServletException, IOException {
    
            try {
                List users = userDao.getAll();
                req.setAttribute("users", users);
                req.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, resp);
            } catch (SQLException e) {
                throw new ServletException("Database error", e);
            }
        }
    
        // POST処理なども同様に実装...
    }

    接続プーリングとDAOパターンの統合

    統合アーキテクチャ

    接続プーリングとDAOパターンを組み合わせた構成では、Servlet/JSP がユーザーからのリクエストを受け取り、必要な処理を Service層 に依頼します。Service層はビジネスロジックを実行し、データの取得や保存が必要な場合は DAO層 を呼び出します。

    [Servlet/JSP] → [Service層] → [DAO層] → [接続プール] → [データベース]

    DAO層はデータベースへ直接アクセスする役割を担い、接続プールからデータベース接続を取得してSQLを実行します。処理が完了すると、接続は閉じるのではなく接続プールへ返却され、次の処理で再利用されます。

    このように各層が役割を分担することで、コードの保守性や再利用性が向上するとともに、接続プーリングによって高速で安定したデータベースアクセスを実現できます。

    完全な実装例

    1. 接続プール設定 (context.xml)

    この設定は、Tomcatでデータベース接続プール(DataSource)を構成するための設定です。接続先のデータベース情報やユーザー名・パスワードに加え、初期接続数や最大接続数、接続待ち時間などを指定しています。また、validationQuery によって接続が有効かどうかを確認するため、安全かつ効率的にデータベース接続を管理できます。アプリケーションは、この設定で作成された接続プールから接続を取得して利用するため、毎回新しい接続を作成する必要がなくなり、パフォーマンスの向上につながります。

    <Context>
        <Resource name="jdbc/mydb" auth="Container"
                  type="javax.sql.DataSource"
                  factory="org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory"
                  driverClassName="com.mysql.cj.jdbc.Driver"
                  url="jdbc:mysql://localhost:3306/mydb"
                  username="user" password="password"
                  initialSize="5" maxTotal="20" maxIdle="10"
                  maxWaitMillis="10000" validationQuery="SELECT 1"/>
    </Context>

    2. DAO実装 (JNDIを使用)

    DAOクラスで接続プール(DataSource)を利用してデータベースへアクセスする実装例です。コンストラクタでは、Tomcatに設定したDataSourceをJNDI経由で取得し、データベース接続の準備を行います。

    getById() メソッドでは、接続プールから接続を取得してSQLを実行し、取得したデータを User オブジェクトへ変換して返します。また、try-with-resources を使用しているため、処理終了後は接続が自動的に閉じられ、実際には接続プールへ返却されるため、安全かつ効率的にデータベース接続を再利用できます。

    public class UserDAOImpl implements UserDAO {
        private DataSource dataSource;
    
        public UserDAOImpl() {
            try {
                Context ctx = new InitialContext();
                dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/mydb");
            } catch (NamingException e) {
                throw new RuntimeException("DataSource lookup failed", e);
            }
        }
    
        @Override
        public User getById(int id) throws SQLException {
            try (Connection conn = dataSource.getConnection();
                 PreparedStatement stmt = conn.prepareStatement(GET_BY_ID_SQL)) {
    
                stmt.setInt(1, id);
                try (ResultSet rs = stmt.executeQuery()) {
                    if (rs.next()) {
                        return mapUser(rs);
                    }
                    return null;
                }
            }
        }
        // 他のメソッドも同様...
    }

    3. Service層の実装

    Service層からDAOを利用してデータベース操作を行う実装例です。Service層はビジネスロジックを担当し、データベースへのアクセスはDAOへ処理を依頼します。

    getAllUsers() や createUser() では、DAOのメソッドを呼び出してデータの取得や登録を行い、発生した SQLException をアプリケーションで扱いやすい例外へ変換しています。このようにService層とDAO層の役割を分離することで、コードの保守性や再利用性が向上し、データベースの変更にも柔軟に対応できる設計になります。

    public class UserService {
        private UserDAO userDao;
    
        public UserService() {
            userDao = new UserDAOImpl();
        }
    
        public List getAllUsers() {
            try {
                return userDao.getAll();
            } catch (SQLException e) {
                throw new RuntimeException("Database error", e);
            }
        }
    
        public void createUser(User user) {
            try {
                if (!userDao.insert(user)) {
                    throw new RuntimeException("User creation failed");
                }
            } catch (SQLException e) {
                throw new RuntimeException("Database error", e);
            }
        }
        // 他のメソッド...
    }

    4. Servletでの使用

    ServletからService層を利用してユーザー情報を取得・登録する実装例です。ServletはHTTPリクエストを受け付ける役割を担い、データベースへ直接アクセスするのではなく、Service層へ処理を依頼します。

    doGet()では、Service層から取得したユーザー一覧をリクエスト属性に設定し、JSPへ渡して画面を表示します。doPost()では、フォームから受け取った入力値をUserオブジェクトに設定し、Service層を通してデータベースへ登録した後、一覧画面へリダイレクトします。

    @WebServlet("/users")
    public class UserServlet extends HttpServlet {
        private UserService userService;
    
        @Override
        public void init() throws ServletException {
            super.init();
            userService = new UserService();
        }
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                throws ServletException, IOException {
    
            List users = userService.getAllUsers();
            req.setAttribute("users", users);
            req.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, resp);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
                throws ServletException, IOException {
    
            User user = new User();
            user.setUsername(req.getParameter("username"));
            user.setEmail(req.getParameter("email"));
    
            userService.createUser(user);
            resp.sendRedirect(req.getContextPath() + "/users");
        }
    }

    このようにServlet・Service・DAOの各層が役割を分担することで、処理の流れが分かりやすくなり、保守性や拡張性の高いアプリケーションを実現できます。

    まとめ

    接続プーリングとDAOパターンを適切に実装することで、アプリケーションの性能や保守性を大きく向上させることができます。接続プーリングはデータベース接続を効率的に再利用することで、接続確立の時間を短縮し、高速な処理を実現します。

    また、DAOパターンによってデータアクセス処理をビジネスロジックから分離することで、コードの見通しが良くなり、データベースに関する変更にも柔軟に対応できます。

    さらに、リソース管理やエラー処理が容易になることでアプリケーションの信頼性が向上し、多数のユーザーが同時に利用するような高負荷環境でも安定した動作を維持できる、拡張性の高いシステムを構築できます。

    これらの技術を組み合わせることで、プロダクションレベルのJSPウェブアプリケーションを構築するための強固な基盤が得られます。実際のプロジェクトでは、この基礎の上にさらに高度な技術を積み上げていくことになります。