
JSP MVCモデルを使った模擬アプリ課題
以下に、Servlet、JSP、JDBC、フロントコントローラーパターンを活用した3つの課題を提案します。 課題1: 簡易タスク管理アプリケーション 要件 模範 […]
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(); // 接続を閉じる
この方法では、リクエストごとに新しい接続を作成し、処理後に閉じるため、以下の問題が発生します:
[アプリケーション] ←貸出/返却→ [接続プール] ←接続→ [データベース]
MySQLでの接続プーリング実装例(Apache Commons DBCPを使用):
<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()が呼ばれても、実際には接続はプールに返却される
プールサイズ = Tn * (Cm - 1) + 1
dataSource.setValidationQuery("SELECT 1");
を設定maxWaitMillis
)を設定minEvictableIdleTimeMillis
)を設定DAO(Data Access Object)パターンは、データベース操作をカプセル化し、ビジネスロジックからデータアクセス層を分離するデザインパターンです。
従来の問題点:
DAOパターンのメリット:
[ビジネスロジック層] → [DAOインターフェース] ←実装→ [DAO実装クラス] → [データベース]
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...
}
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;
}
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;
}
}
@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処理なども同様に実装...
}
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;
}
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();
}
}
}
[Servlet/JSP] → [Service層] → [DAO層] → [接続プール] → [データベース]
<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>
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;
}
}
}
// 他のメソッドも同様...
}
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);
}
}
// 他のメソッド...
}
@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");
}
}
removeAbandonedTimeout
設定を有効化maxWaitMillis
を増やすvalidationQuery
を設定testOnBorrow
またはtestOnReturn
をtrueに設定maxTotal = (同時ユーザー数 * 0.1) + 1
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setValidationQueryTimeout(3); // 秒
dataSource.setTimeBetweenEvictionRunsMillis(60000); // 1分ごとにチェック
dataSource.setMinEvictableIdleTimeMillis(300000); // 5分以上アイドルなら削除
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);
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());
}
}
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パターンをマスターしたら、さらに進んだトピックに進むことができます:
接続プーリングとDAOパターンを適切に実装することで、以下のようなメリットが得られます:
これらの技術を組み合わせることで、プロダクションレベルのJSPウェブアプリケーションを構築するための強固な基盤が得られます。実際のプロジェクトでは、この基礎の上にさらに高度な技術を積み上げていくことになります。