1. 概要

接続プーリングはよく知られているデータアクセスパターンであり、その主な目的は、データベース接続とデータベースの読み取り/書き込み操作の実行に伴うオーバーヘッドを削減することです。

簡単に言うと、接続プールは、最も基本的なレベルでは、データベース接続キャッシュの実装であり、特定の要件に合わせて構成できます。

このチュートリアルでは、いくつかの一般的な接続プールフレームワークを簡単にまとめ、独自の接続プールを最初から実装する方法を学習します。

2. なぜ接続プールなのか?

もちろん、問題は修辞的です。

一般的なデータベース接続のライフサイクルに含まれる一連の手順を分析すると、その理由がわかります。

  1. データベースドライバを使用してデータベースへの接続を開く
  2. データの読み取り/書き込み用にTCPソケットを開く
  3. ソケットを介したデータの読み取り/書き込み
  4. 接続を閉じる
  5. ソケットを閉じる

データベース接続はかなりコストのかかる操作であることが明らかになります。そのため、考えられるすべてのユースケースで最小限に抑える必要があります(エッジケースでは、回避する必要があります)。

ここで、接続プールの実装が役立ちます。

多くの既存の接続を再利用できるデータベース接続コンテナを実装するだけで、膨大な数の高価なデータベーストリップを実行するコストを効果的に節約できるため、データベース駆動型アプリケーションの全体的なパフォーマンスが向上します。

3. JDBC接続プーリングフレームワーク

実用的な観点からは、接続プールをゼロから実装することは、そこにある「エンタープライズ対応」の接続プールフレームワークの数を考えると、まったく無意味です。

この記事の目的である教訓的なものから、そうではありません。

それでも、基本的な接続プールを実装する方法を学ぶ前に、まずいくつかの一般的な接続プールフレームワークを紹介しましょう。

3.1. Apache Commons DBCP

この簡単なまとめを、フル機能の接続プールJDBCフレームワークである Apache Commons DBCPComponentから始めましょう。

public class DBCPDataSource {
    
    private static BasicDataSource ds = new BasicDataSource();
    
    static {
        ds.setUrl("jdbc:h2:mem:test");
        ds.setUsername("user");
        ds.setPassword("password");
        ds.setMinIdle(5);
        ds.setMaxIdle(10);
        ds.setMaxOpenPreparedStatements(100);
    }
    
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
    
    private DBCPDataSource(){ }
}

この場合、静的ブロックを持つラッパークラスを使用して、DBCPのプロパティを簡単に構成しました。

DBCPDataSourceクラスでプールされた接続を取得する方法は次のとおりです。

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

次に、 BrettWooldridgeによって作成された超高速のJDBC接続プールフレームワークであるHikariCPを見てみましょう(HikariCPを構成して最大限に活用する方法の詳細については、こちらをご覧ください。チェックこの記事):

public class HikariCPDataSource {
    
    private static HikariConfig config = new HikariConfig();
    private static HikariDataSource ds;
    
    static {
        config.setJdbcUrl("jdbc:h2:mem:test");
        config.setUsername("user");
        config.setPassword("password");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        ds = new HikariDataSource(config);
    }
    
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
    
    private HikariCPDataSource(){}
}

同様に、HikariCPDataSourceクラスとのプールされた接続を取得する方法は次のとおりです。

Connection con = HikariCPDataSource.getConnection();

3.3. C3P0

このレビューの最後は、 C3P0 です。これは、SteveWaldmanによって開発された強力なJDBC4接続およびステートメントプーリングフレームワークです。

public class C3p0DataSource {

    private static ComboPooledDataSource cpds = new ComboPooledDataSource();

    static {
        try {
            cpds.setDriverClass("org.h2.Driver");
            cpds.setJdbcUrl("jdbc:h2:mem:test");
            cpds.setUser("user");
            cpds.setPassword("password");
        } catch (PropertyVetoException e) {
            // handle the exception
        }
    }
    
    public static Connection getConnection() throws SQLException {
        return cpds.getConnection();
    }
    
    private C3p0DataSource(){}
}

予想どおり、 C3p0DataSource クラスでプールされた接続を取得することは、前の例と同様です。

Connection con = C3p0DataSource.getConnection();

4. 簡単な実装

接続プールの基礎となるロジックをよりよく理解するために、簡単な実装を作成しましょう。

たった1つのインターフェースに基づいた、ゆるく結合された設計から始めましょう。

public interface ConnectionPool {
    Connection getConnection();
    boolean releaseConnection(Connection connection);
    String getUrl();
    String getUser();
    String getPassword();
}

ConnectionPool インターフェースは、基本的な接続プールのパブリックAPIを定義します。

次に、プールされた接続の取得と解放など、いくつかの基本的な機能を提供する実装を作成しましょう。

public class BasicConnectionPool 
  implements ConnectionPool {

    private String url;
    private String user;
    private String password;
    private List<Connection> connectionPool;
    private List<Connection> usedConnections = new ArrayList<>();
    private static int INITIAL_POOL_SIZE = 10;
    
    public static BasicConnectionPool create(
      String url, String user, 
      String password) throws SQLException {
 
        List<Connection> pool = new ArrayList<>(INITIAL_POOL_SIZE);
        for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
            pool.add(createConnection(url, user, password));
        }
        return new BasicConnectionPool(url, user, password, pool);
    }
    
    // standard constructors
    
    @Override
    public Connection getConnection() {
        Connection connection = connectionPool
          .remove(connectionPool.size() - 1);
        usedConnections.add(connection);
        return connection;
    }
    
    @Override
    public boolean releaseConnection(Connection connection) {
        connectionPool.add(connection);
        return usedConnections.remove(connection);
    }
    
    private static Connection createConnection(
      String url, String user, String password) 
      throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }
    
    public int getSize() {
        return connectionPool.size() + usedConnections.size();
    }

    // standard getters
}

BasicConnectionPool クラスは非常に単純ですが、一般的な接続プールの実装に期待される最小限の機能を提供します。

一言で言えば、クラスは、簡単に再利用できる10個の接続を格納するArrayListに基づいて接続プールを初期化します。

DriverManagerクラスおよびDatasource実装を使用してJDBC接続を作成することが可能です。

接続データベースの作成に依存しない方がはるかに優れているため、 create()静的ファクトリメソッド内で前者を使用しました。

この場合、メソッドを BasicConnectionPool 内に配置しました。これは、これがインターフェイスの唯一の実装であるためです。

複数のConnectionPool実装を備えたより複雑な設計では、インターフェイスに配置することが望ましいため、より柔軟な設計とより高いレベルの結束が得られます。

ここで強調する最も重要な点は、プールが作成されると、接続がプールからフェッチされるため、新しい接続を作成する必要がないことです。

さらに、接続が解放されると、実際にはプールに戻されるため、他のクライアントはそれを再利用できます

Connectionのclose()メソッドへの明示的な呼び出しなど、基盤となるデータベースとのそれ以上の相互作用はありません。

5. BasicConnectionPoolクラスの使用

予想どおり、BasicConnectionPoolクラスの使用は簡単です。

簡単な単体テストを作成して、プールされたメモリ内H2接続を取得しましょう。

@Test
public whenCalledgetConnection_thenCorrect() {
    ConnectionPool connectionPool = BasicConnectionPool
      .create("jdbc:h2:mem:test", "user", "password");
 
    assertTrue(connectionPool.getConnection().isValid(1));
}

6. さらなる改善とリファクタリング

もちろん、接続プールの実装の現在の機能を微調整/拡張する余地は十分にあります。

たとえば、 getConnection()メソッドをリファクタリングし、最大プールサイズのサポートを追加できます。 使用可能なすべての接続が取得され、現在のプールサイズが構成された最大値よりも小さい場合、メソッドは新しい接続を作成します。

また、プールから取得した接続がまだ有効であるかどうかを確認してから、クライアントに渡すこともできます。

@Override
public Connection getConnection() throws SQLException {
    if (connectionPool.isEmpty()) {
        if (usedConnections.size() < MAX_POOL_SIZE) {
            connectionPool.add(createConnection(url, user, password));
        } else {
            throw new RuntimeException(
              "Maximum pool size reached, no available connections!");
        }
    }

    Connection connection = connectionPool
      .remove(connectionPool.size() - 1);

    if(!connection.isValid(MAX_TIMEOUT)){
        connection = createConnection(url, user, password);
    }

    usedConnections.add(connection);
    return connection;
}

メソッドがSQLExceptionをスローするようになったことに注意してください。つまり、インターフェイスのシグネチャも更新する必要があります。

または、接続プールインスタンスを正常にシャットダウンするメソッドを追加することもできます。

public void shutdown() throws SQLException {
    usedConnections.forEach(this::releaseConnection);
    for (Connection c : connectionPool) {
        c.close();
    }
    connectionPool.clear();
}

本番環境に対応した実装では、接続プールは、現在使用されている接続を追跡する機能、プリペアドステートメントプーリングのサポートなど、多数の追加機能を提供する必要があります。

物事を単純にするため、これらの追加機能を実装する方法を省略し、わかりやすくするために実装をスレッドセーフではない状態に保ちます。

7. 結論

この記事では、接続プールとは何かを詳しく調べ、独自の接続プールの実装を展開する方法を学びました。

もちろん、フル機能の接続プールレイヤーをアプリケーションに追加するたびに、最初から始める必要はありません。

そのため、最初に最も人気のある接続プールフレームワークのいくつかを示す簡単なまとめを作成しました。これにより、それらを操作する方法を明確に把握し、要件に最適なフレームワークを選択できます。

いつものように、この記事に示されているすべてのコードサンプルは、GitHubから入手できます。