はじめに
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(); // 接続を閉じる
この方法では、リクエストごとに新しい接続を作成し、処理後に閉じるため、以下の問題が発生します:
- 接続確立に時間がかかる(認証、リソース割り当てなど)
- 同時接続数が増えるとデータベース負荷が高くなる
- 接続の再利用ができないため非効率
接続プーリングのメリット
- パフォーマンス向上: 接続の作成・破棄コストを削減
- リソース管理: 接続数を制限し、データベース過負荷を防止
- スケーラビリティ: 多数の同時リクエストに対応可能
- 接続の再利用: 既存の接続を再利用できる
接続プーリングの仕組み
[アプリケーション] ←貸出/返却→ [接続プール] ←接続→ [データベース]
- アプリケーション起動時に初期接続数をプールに作成
- アプリケーションが接続を要求すると、プールから接続を貸し出す
- アプリケーションが接続を使用後、プールに返却
- プールは返却された接続を再利用可能な状態に維持
Apache DBCPを使用した接続プーリング実装
MySQLでの接続プーリング実装例(Apache Commons DBCPを使用):
- まず、必要なライブラリを追加(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>
- 接続プールを管理するクラスを作成:
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() {} // インスタンス化防止
}
- 使用例:
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
- 最大サイズ: アプリケーションの同時実行数に応じて調整
- 一般的な公式:
プールサイズ = Tn * (Cm - 1) + 1- Tn: スレッド数, Cm: 各スレッドが保持する接続数
- 接続リーク防止:
- try-with-resources文を使用して確実に接続を閉じる
- 接続が適切に返却されないとプールが枯渇する
- 接続検証:
- プールから取得した接続が有効かどうかを検証
dataSource.setValidationQuery("SELECT 1");を設定
- タイムアウト設定:
- 接続取得待ち時間(
maxWaitMillis)を設定 - アイドル接続のタイムアウト(
minEvictableIdleTimeMillis)を設定
DAOパターンの理解
DAOパターンとは
DAO(Data Access Object)パターンは、データベース操作をカプセル化し、ビジネスロジックからデータアクセス層を分離するデザインパターンです。
従来の問題点:
- ビジネスロジックにSQLが散在
- データベース変更時に多くの箇所を修正する必要がある
- コードの再利用性が低い
DAOパターンのメリット:
- 関心の分離: ビジネスロジックとデータアクセスロジックを分離
- 保守性向上: データベース関連の変更が一箇所に集中
- テスト容易性: モックを使用した単体テストが可能
- コードの再利用: 共通のデータアクセス操作を再利用可能
DAOパターンの基本構造
[ビジネスロジック層] → [DAOインターフェース] ←実装→ [DAO実装クラス] → [データベース]
DAOパターンの実装例
1. エンティティクラスの作成
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インターフェースの定義
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用)
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内)
@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パターンの進化形
1. ジェネリックDAO
public interface GenericDAO {
boolean insert(T entity) throws SQLException;
T getById(ID id) throws SQLException;
List getAll() throws SQLException;
boolean update(T entity) throws SQLException;
boolean delete(ID id) throws SQLException;
}
2. トランザクション管理
public class UserService {
private UserDAO userDao;
private Connection conn;
public UserService() throws SQLException {
conn = DBCPDataSource.getConnection();
conn.setAutoCommit(false); // 自動コミットを無効化
userDao = new UserDAOImpl(conn); // 接続を共有するDAO
}
public void createUserWithProfile(User user, Profile profile)
throws SQLException {
try {
userDao.insert(user);
profileDao.insert(profile);
conn.commit(); // 両方成功したらコミット
} catch (SQLException e) {
conn.rollback(); // 失敗したらロールバック
throw e;
} finally {
conn.close();
}
}
}
接続プーリングとDAOパターンの統合
統合アーキテクチャ
[Servlet/JSP] → [Service層] → [DAO層] → [接続プール] → [データベース]
完全な実装例
1. 接続プール設定 (context.xml)
<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を使用)
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層の実装
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での使用
@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");
}
}
よくある問題と解決策
接続プーリング関連
- 接続リーク:
- 症状: プールが枯渇し、新しい接続が取得できない
- 解決策:
- try-with-resourcesを使用
- 接続取得・解放をパターン化
- DBCPの
removeAbandonedTimeout設定を有効化
- 接続タイムアウト:
- 症状: 接続取得時にTimeoutExceptionが発生
- 解決策:
maxWaitMillisを増やす- プールサイズを調整
- アイドル接続テストを有効化
- 接続検証失敗:
- 症状: 無効な接続がプールから返される
- 解決策:
validationQueryを設定testOnBorrowまたはtestOnReturnをtrueに設定
DAOパターン関連
- 冗長なコード:
- 症状: 各DAOに同じようなCRUDコードが繰り返される
- 解決策:
- ジェネリックDAOを導入
- ORMフレームワーク(Hibernate, MyBatis)の使用を検討
- トランザクション管理:
- 症状: 複数テーブル更新時の整合性問題
- 解決策:
- Service層でトランザクション境界を定義
- 接続を共有して複数DAO操作を1トランザクションで実行
- 例外処理:
- 症状: SQLExceptionがビジネスロジック層に漏れる
- 解決策:
- カスタム例外を定義
- Service層でSQLExceptionをキャッチし、適切な例外に変換
パフォーマンスチューニング
接続プーリングの最適化
- プールサイズの調整:
- 一般的な推奨値:
- 初期サイズ: 5-10
- 最大サイズ: アプリケーションの同時実行ユーザー数に応じて
- ウェブアプリの場合:
maxTotal = (同時ユーザー数 * 0.1) + 1
- 接続検証の最適化:
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setValidationQueryTimeout(3); // 秒
- アイドル接続の管理:
dataSource.setTimeBetweenEvictionRunsMillis(60000); // 1分ごとにチェック
dataSource.setMinEvictableIdleTimeMillis(300000); // 5分以上アイドルなら削除
DAO層の最適化
- バッチ処理の実装:
public void batchInsert(List users) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(INSERT_SQL)) {
for (User user : users) {
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getEmail());
stmt.addBatch();
}
stmt.executeBatch();
}
}
- ページネーションの実装:
public List getUsers(int page, int size) throws SQLException {
String sql = "SELECT * FROM users LIMIT ? OFFSET ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, size);
stmt.setInt(2, (page - 1) * size);
try (ResultSet rs = stmt.executeQuery()) {
List users = new ArrayList<>();
while (rs.next()) {
users.add(mapUser(rs));
}
return users;
}
}
}
- プリペアドステートメントのキャッシュ:
- データソース設定で有効化:
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxOpenPreparedStatements(100);
テスト戦略
単体テスト
- DAOのテスト:
- テスト用データベースを使用
- 各テストケース前にデータを初期化
- トランザクションを利用してテスト後にロールバック
public class UserDAOTest {
private UserDAO userDao;
private Connection conn;
@Before
public void setUp() throws SQLException {
conn = TestDataSource.getConnection(); // テスト用データソース
conn.setAutoCommit(false); // 自動コミットを無効化
userDao = new UserDAOImpl(conn); // テスト用接続を渡す
}
@After
public void tearDown() throws SQLException {
conn.rollback(); // 変更を破棄
conn.close();
}
@Test
public void testInsertAndGetUser() throws SQLException {
User user = new User(0, "testuser", "test@example.com", new Date());
userDao.insert(user);
User retrieved = userDao.getById(user.getId());
assertNotNull(retrieved);
assertEquals("testuser", retrieved.getUsername());
}
}
統合テスト
- Service層のテスト:
- 実際のDAOを使用せず、モックを使用
- ビジネスロジックに焦点
public class UserServiceTest {
private UserService userService;
private UserDAO mockUserDao;
@Before
public void setUp() {
mockUserDao = Mockito.mock(UserDAO.class);
userService = new UserService(mockUserDao);
}
@Test
public void testGetAllUsers() throws SQLException {
// モックの設定
List mockUsers = Arrays.asList(
new User(1, "user1", "user1@example.com", new Date()),
new User(2, "user2", "user2@example.com", new Date())
);
when(mockUserDao.getAll()).thenReturn(mockUsers);
// テスト実行
List users = userService.getAllUsers();
// 検証
assertEquals(2, users.size());
verify(mockUserDao, times(1)).getAll();
}
}
次のステップ
接続プーリングとDAOパターンをマスターしたら、さらに進んだトピックに進むことができます:
- ORMフレームワーク: HibernateやMyBatisなどの使用
- Springフレームワーク: Spring JDBC、Spring Data JPAの利用
- マイクロサービスアーキテクチャ: サービス間でのデータアクセス戦略
- NoSQLデータベース: MongoDBやRedisなどの接続管理
まとめ
接続プーリングとDAOパターンを適切に実装することで、以下のようなメリットが得られます:
- パフォーマンスの向上: データベース接続の効率的な管理
- コードの保守性向上: データアクセスロジックの分離と整理
- アプリケーションの信頼性向上: リソース管理とエラー処理の改善
- スケーラビリティ: 高負荷環境での安定した動作
これらの技術を組み合わせることで、プロダクションレベルのJSPウェブアプリケーションを構築するための強固な基盤が得られます。実際のプロジェクトでは、この基礎の上にさらに高度な技術を積み上げていくことになります。