第9章: 簡易Webアプリ開発
JSP、JDBC、JavaBeansを統合した実践的Webアプリケーション開発
この章で学ぶこと
- JSP、JDBC、JavaBeansを組み合わせたMVCアーキテクチャの理解
- 各コンポーネントの適切な役割分担と責務の分離
- フォーム処理とデータベース連携の実装方法
- DAO(Data Access Object)パターンによるデータアクセス層の設計
- エラーハンドリングとバリデーション処理の実装
9.1 Webアプリケーションアーキテクチャの設計
本章では、JSPとJDBCを組み合わせて実用的なWebアプリケーションを構築します。MVCアーキテクチャの概念に基づいて、各層の責務を明確に分離した設計を学習します。
flowchart TD
A[クライアント
(ブラウザ)] --> B[Presentation Layer
プレゼンテーション層] B --> C[Business Logic Layer
ビジネスロジック層] C --> D[Data Access Layer
データアクセス層] D --> E[Database
データベース] B --> F[JSP
(View)] B --> G[Servlet/JSP
(Controller)] C --> H[JavaBeans
(Model)] C --> I[Service Classes
(ビジネスロジック)] D --> J[DAO Classes
(Data Access Object)] D --> K[JDBC
(データベース接続)] E --> L[H2 Database
(テスト用DB)]
(ブラウザ)] --> B[Presentation Layer
プレゼンテーション層] B --> C[Business Logic Layer
ビジネスロジック層] C --> D[Data Access Layer
データアクセス層] D --> E[Database
データベース] B --> F[JSP
(View)] B --> G[Servlet/JSP
(Controller)] C --> H[JavaBeans
(Model)] C --> I[Service Classes
(ビジネスロジック)] D --> J[DAO Classes
(Data Access Object)] D --> K[JDBC
(データベース接続)] E --> L[H2 Database
(テスト用DB)]
各層の責務
1. プレゼンテーション層(Presentation Layer)
- JSPページ: ユーザーインターフェースの表示
- フォーム処理: ユーザー入力の受付
- 画面遷移制御: リダイレクトとフォワード
- エラー表示: 入力エラーや処理エラーの表示
2. ビジネスロジック層(Business Logic Layer)
- JavaBeans: データ構造の定義とビジネスルール
- バリデーション: 入力データの妥当性チェック
- ビジネスルール: 業務固有の処理ロジック
- 例外処理: エラー処理と回復機能
3. データアクセス層(Data Access Layer)
- DAOクラス: データベースアクセスの抽象化
- CRUD操作: Create、Read、Update、Delete
- トランザクション管理: データ整合性の保証
- 接続管理: データベース接続の効率的な管理
9.2 データモデルとJavaBeansの設計
簡易タスク管理アプリケーションを例に、データモデルとJavaBeansの設計方法を学習します。
実習 9-1: タスク管理システムのデータモデル設計
タスク管理システムのJavaBeansとDAOクラスを設計・実装します。
手順1: Taskエンティティクラス(Task.java)
package com.example.model;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* タスクエンティティクラス
* データベースのtasksテーブルに対応
*/
public class Task implements Serializable {
private static final long serialVersionUID = 1L;
// フィールド(データベースカラムに対応)
private Integer taskId; // タスクID(主キー)
private String title; // タスクタイトル
private String description; // タスク説明
private TaskStatus status; // タスクステータス
private TaskPriority priority; // 優先度
private String assignedTo; // 担当者
private LocalDateTime createdAt; // 作成日時
private LocalDateTime updatedAt; // 更新日時
private LocalDateTime dueDate; // 期限日時
// ステータス列挙型
public enum TaskStatus {
TODO("未着手", "#6c757d"),
IN_PROGRESS("進行中", "#0d6efd"),
COMPLETED("完了", "#198754"),
CANCELLED("キャンセル", "#dc3545");
private final String displayName;
private final String color;
TaskStatus(String displayName, String color) {
this.displayName = displayName;
this.color = color;
}
public String getDisplayName() { return displayName; }
public String getColor() { return color; }
}
// 優先度列挙型
public enum TaskPriority {
LOW("低", 1, "#28a745"),
MEDIUM("中", 2, "#ffc107"),
HIGH("高", 3, "#fd7e14"),
URGENT("緊急", 4, "#dc3545");
private final String displayName;
private final int level;
private final String color;
TaskPriority(String displayName, int level, String color) {
this.displayName = displayName;
this.level = level;
this.color = color;
}
public String getDisplayName() { return displayName; }
public int getLevel() { return level; }
public String getColor() { return color; }
}
// デフォルトコンストラクタ
public Task() {
this.status = TaskStatus.TODO;
this.priority = TaskPriority.MEDIUM;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// コンストラクタ(必須項目)
public Task(String title, String description) {
this();
this.title = title;
this.description = description;
}
// getter/setterメソッド
public Integer getTaskId() { return taskId; }
public void setTaskId(Integer taskId) { this.taskId = taskId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) {
this.status = status;
this.updatedAt = LocalDateTime.now();
}
// String型のステータス設定(フォームからの入力用)
public void setStatusString(String statusString) {
if (statusString != null && !statusString.trim().isEmpty()) {
try {
this.status = TaskStatus.valueOf(statusString);
this.updatedAt = LocalDateTime.now();
} catch (IllegalArgumentException e) {
// 無効な値の場合はデフォルトのまま
}
}
}
public String getStatusString() {
return status != null ? status.name() : "";
}
public TaskPriority getPriority() { return priority; }
public void setPriority(TaskPriority priority) {
this.priority = priority;
this.updatedAt = LocalDateTime.now();
}
// String型の優先度設定(フォームからの入力用)
public void setPriorityString(String priorityString) {
if (priorityString != null && !priorityString.trim().isEmpty()) {
try {
this.priority = TaskPriority.valueOf(priorityString);
this.updatedAt = LocalDateTime.now();
} catch (IllegalArgumentException e) {
// 無効な値の場合はデフォルトのまま
}
}
}
public String getPriorityString() {
return priority != null ? priority.name() : "";
}
public String getAssignedTo() { return assignedTo; }
public void setAssignedTo(String assignedTo) {
this.assignedTo = assignedTo;
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDueDate() { return dueDate; }
public void setDueDate(LocalDateTime dueDate) {
this.dueDate = dueDate;
this.updatedAt = LocalDateTime.now();
}
// ビジネスメソッド
/**
* タスクが期限切れかどうかを判定
*/
public boolean isOverdue() {
return dueDate != null &&
!TaskStatus.COMPLETED.equals(status) &&
LocalDateTime.now().isAfter(dueDate);
}
/**
* タスクが完了しているかどうかを判定
*/
public boolean isCompleted() {
return TaskStatus.COMPLETED.equals(status);
}
/**
* タスクの進行度を%で取得(簡易実装)
*/
public int getProgressPercentage() {
switch (status) {
case TODO: return 0;
case IN_PROGRESS: return 50;
case COMPLETED: return 100;
case CANCELLED: return 0;
default: return 0;
}
}
/**
* 日時を文字列形式で取得
*/
public String getFormattedCreatedAt() {
return createdAt != null ?
createdAt.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")) : "";
}
public String getFormattedUpdatedAt() {
return updatedAt != null ?
updatedAt.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")) : "";
}
public String getFormattedDueDate() {
return dueDate != null ?
dueDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")) : "";
}
/**
* バリデーション
*/
public boolean isValid() {
return title != null && !title.trim().isEmpty() &&
title.length() <= 100 &&
(description == null || description.length() <= 1000);
}
/**
* バリデーションエラーメッセージを取得
*/
public String getValidationMessage() {
if (title == null || title.trim().isEmpty()) {
return "タイトルは必須です。";
}
if (title.length() > 100) {
return "タイトルは100文字以内で入力してください。";
}
if (description != null && description.length() > 1000) {
return "説明は1000文字以内で入力してください。";
}
return null;
}
@Override
public String toString() {
return String.format("Task{id=%d, title='%s', status=%s, priority=%s, assignedTo='%s'}",
taskId, title, status, priority, assignedTo);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Task task = (Task) obj;
return taskId != null && taskId.equals(task.taskId);
}
@Override
public int hashCode() {
return taskId != null ? taskId.hashCode() : 0;
}
}
手順2: TaskDAOインターフェース(TaskDAO.java)
package com.example.dao;
import com.example.model.Task;
import com.example.model.Task.TaskStatus;
import com.example.model.Task.TaskPriority;
import java.util.List;
import java.sql.SQLException;
/**
* Task データアクセスオブジェクトインターフェース
* データベースアクセス操作を定義
*/
public interface TaskDAO {
/**
* 新しいタスクを作成
* @param task 作成するタスク
* @return 作成されたタスクのID
* @throws SQLException データベースエラー
*/
Integer create(Task task) throws SQLException;
/**
* IDでタスクを取得
* @param taskId タスクID
* @return 該当するタスク、存在しない場合null
* @throws SQLException データベースエラー
*/
Task findById(Integer taskId) throws SQLException;
/**
* 全タスクを取得
* @return タスクリスト
* @throws SQLException データベースエラー
*/
List<Task> findAll() throws SQLException;
/**
* ステータス別にタスクを検索
* @param status タスクステータス
* @return 該当するタスクリスト
* @throws SQLException データベースエラー
*/
List<Task> findByStatus(TaskStatus status) throws SQLException;
/**
* 担当者別にタスクを検索
* @param assignedTo 担当者名
* @return 該当するタスクリスト
* @throws SQLException データベースエラー
*/
List<Task> findByAssignedTo(String assignedTo) throws SQLException;
/**
* 優先度別にタスクを検索
* @param priority 優先度
* @return 該当するタスクリスト
* @throws SQLException データベースエラー
*/
List<Task> findByPriority(TaskPriority priority) throws SQLException;
/**
* 期限切れタスクを検索
* @return 期限切れタスクリスト
* @throws SQLException データベースエラー
*/
List<Task> findOverdueTasks() throws SQLException;
/**
* タスクを更新
* @param task 更新するタスク
* @return 更新されたレコード数
* @throws SQLException データベースエラー
*/
int update(Task task) throws SQLException;
/**
* タスクを削除
* @param taskId 削除するタスクのID
* @return 削除されたレコード数
* @throws SQLException データベースエラー
*/
int delete(Integer taskId) throws SQLException;
/**
* ステータス別タスク数を取得
* @param status タスクステータス
* @return 該当するタスク数
* @throws SQLException データベースエラー
*/
int countByStatus(TaskStatus status) throws SQLException;
/**
* 全タスク数を取得
* @return 全タスク数
* @throws SQLException データベースエラー
*/
int countAll() throws SQLException;
/**
* データベーステーブルを初期化
* @throws SQLException データベースエラー
*/
void initializeTable() throws SQLException;
/**
* サンプルデータを挿入
* @throws SQLException データベースエラー
*/
void insertSampleData() throws SQLException;
}
期待される結果
タスク管理システムの基盤となるデータモデルとDAOインターフェースが定義され、オブジェクト指向設計の原則に従った構造が構築されます。
9.3 DAOパターンによるデータアクセス層の実装
DAOパターンを使用してデータベースアクセスを抽象化し、ビジネスロジックからデータアクセスの詳細を分離します。
実習 9-2: TaskDAO実装クラスの作成
H2データベースを使用したTaskDAOの具体実装を作成します。
手順1: TaskDAOImpl実装クラス(TaskDAOImpl.java)
package com.example.dao.impl;
import com.example.dao.TaskDAO;
import com.example.model.Task;
import com.example.model.Task.TaskStatus;
import com.example.model.Task.TaskPriority;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* TaskDAO の H2データベース実装
*/
public class TaskDAOImpl implements TaskDAO {
// H2インメモリデータベース接続URL
private static final String DB_URL = "jdbc:h2:mem:taskdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
private static final String DB_USER = "sa";
private static final String DB_PASSWORD = "";
// SQL文定義
private static final String CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS tasks (
task_id INTEGER AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description VARCHAR(1000),
status VARCHAR(20) NOT NULL DEFAULT 'TODO',
priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM',
assigned_to VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
due_date TIMESTAMP
)
""";
private static final String INSERT_SQL = """
INSERT INTO tasks (title, description, status, priority, assigned_to, created_at, updated_at, due_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
private static final String SELECT_BY_ID_SQL = "SELECT * FROM tasks WHERE task_id = ?";
private static final String SELECT_ALL_SQL = "SELECT * FROM tasks ORDER BY created_at DESC";
private static final String SELECT_BY_STATUS_SQL = "SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC";
private static final String SELECT_BY_ASSIGNED_TO_SQL = "SELECT * FROM tasks WHERE assigned_to = ? ORDER BY created_at DESC";
private static final String SELECT_BY_PRIORITY_SQL = "SELECT * FROM tasks WHERE priority = ? ORDER BY priority DESC, created_at DESC";
private static final String SELECT_OVERDUE_SQL = "SELECT * FROM tasks WHERE due_date < CURRENT_TIMESTAMP AND status != 'COMPLETED' ORDER BY due_date ASC";
private static final String UPDATE_SQL = """
UPDATE tasks SET title = ?, description = ?, status = ?, priority = ?,
assigned_to = ?, updated_at = ?, due_date = ?
WHERE task_id = ?
""";
private static final String DELETE_SQL = "DELETE FROM tasks WHERE task_id = ?";
private static final String COUNT_BY_STATUS_SQL = "SELECT COUNT(*) FROM tasks WHERE status = ?";
private static final String COUNT_ALL_SQL = "SELECT COUNT(*) FROM tasks";
/**
* データベース接続を取得
*/
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
}
/**
* ResultSetからTaskオブジェクトを生成
*/
private Task mapResultSetToTask(ResultSet rs) throws SQLException {
Task task = new Task();
task.setTaskId(rs.getInt("task_id"));
task.setTitle(rs.getString("title"));
task.setDescription(rs.getString("description"));
// Enumの変換
String statusStr = rs.getString("status");
if (statusStr != null) {
task.setStatus(TaskStatus.valueOf(statusStr));
}
String priorityStr = rs.getString("priority");
if (priorityStr != null) {
task.setPriority(TaskPriority.valueOf(priorityStr));
}
task.setAssignedTo(rs.getString("assigned_to"));
// Timestamp → LocalDateTime 変換
Timestamp createdTs = rs.getTimestamp("created_at");
if (createdTs != null) {
task.setCreatedAt(createdTs.toLocalDateTime());
}
Timestamp updatedTs = rs.getTimestamp("updated_at");
if (updatedTs != null) {
task.setUpdatedAt(updatedTs.toLocalDateTime());
}
Timestamp dueTs = rs.getTimestamp("due_date");
if (dueTs != null) {
task.setDueDate(dueTs.toLocalDateTime());
}
return task;
}
@Override
public Integer create(Task task) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, task.getTitle());
ps.setString(2, task.getDescription());
ps.setString(3, task.getStatus().name());
ps.setString(4, task.getPriority().name());
ps.setString(5, task.getAssignedTo());
ps.setTimestamp(6, Timestamp.valueOf(task.getCreatedAt()));
ps.setTimestamp(7, Timestamp.valueOf(task.getUpdatedAt()));
ps.setTimestamp(8, task.getDueDate() != null ? Timestamp.valueOf(task.getDueDate()) : null);
int result = ps.executeUpdate();
if (result > 0) {
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
Integer taskId = rs.getInt(1);
task.setTaskId(taskId);
return taskId;
}
}
}
throw new SQLException("タスクの作成に失敗しました");
}
}
@Override
public Task findById(Integer taskId) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_BY_ID_SQL)) {
ps.setInt(1, taskId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapResultSetToTask(rs);
}
return null;
}
}
}
@Override
public List<Task> findAll() throws SQLException {
List<Task> tasks = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_ALL_SQL);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
tasks.add(mapResultSetToTask(rs));
}
}
return tasks;
}
@Override
public List<Task> findByStatus(TaskStatus status) throws SQLException {
List<Task> tasks = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_BY_STATUS_SQL)) {
ps.setString(1, status.name());
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
tasks.add(mapResultSetToTask(rs));
}
}
}
return tasks;
}
@Override
public List<Task> findByAssignedTo(String assignedTo) throws SQLException {
List<Task> tasks = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_BY_ASSIGNED_TO_SQL)) {
ps.setString(1, assignedTo);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
tasks.add(mapResultSetToTask(rs));
}
}
}
return tasks;
}
@Override
public List<Task> findByPriority(TaskPriority priority) throws SQLException {
List<Task> tasks = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_BY_PRIORITY_SQL)) {
ps.setString(1, priority.name());
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
tasks.add(mapResultSetToTask(rs));
}
}
}
return tasks;
}
@Override
public List<Task> findOverdueTasks() throws SQLException {
List<Task> tasks = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_OVERDUE_SQL);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
tasks.add(mapResultSetToTask(rs));
}
}
return tasks;
}
@Override
public int update(Task task) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(UPDATE_SQL)) {
ps.setString(1, task.getTitle());
ps.setString(2, task.getDescription());
ps.setString(3, task.getStatus().name());
ps.setString(4, task.getPriority().name());
ps.setString(5, task.getAssignedTo());
ps.setTimestamp(6, Timestamp.valueOf(task.getUpdatedAt()));
ps.setTimestamp(7, task.getDueDate() != null ? Timestamp.valueOf(task.getDueDate()) : null);
ps.setInt(8, task.getTaskId());
return ps.executeUpdate();
}
}
@Override
public int delete(Integer taskId) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(DELETE_SQL)) {
ps.setInt(1, taskId);
return ps.executeUpdate();
}
}
@Override
public int countByStatus(TaskStatus status) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(COUNT_BY_STATUS_SQL)) {
ps.setString(1, status.name());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
return 0;
}
}
}
@Override
public int countAll() throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(COUNT_ALL_SQL);
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
return 0;
}
}
@Override
public void initializeTable() throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
stmt.executeUpdate(CREATE_TABLE_SQL);
}
}
@Override
public void insertSampleData() throws SQLException {
// サンプルタスクの作成
Task[] sampleTasks = {
new Task("Webアプリケーションの設計", "JSPとJDBCを使用したタスク管理システムの設計"),
new Task("データベース構築", "H2データベースのテーブル作成とサンプルデータ投入"),
new Task("DAO層の実装", "TaskDAOインターフェースと実装クラスの作成"),
new Task("JSPページの作成", "一覧表示、詳細表示、編集フォームの作成"),
new Task("バリデーション実装", "入力データの妥当性チェック機能の実装")
};
// ステータスと優先度を設定
sampleTasks[0].setStatus(TaskStatus.COMPLETED);
sampleTasks[0].setPriority(TaskPriority.HIGH);
sampleTasks[0].setAssignedTo("開発太郎");
sampleTasks[1].setStatus(TaskStatus.COMPLETED);
sampleTasks[1].setPriority(TaskPriority.HIGH);
sampleTasks[1].setAssignedTo("開発太郎");
sampleTasks[2].setStatus(TaskStatus.IN_PROGRESS);
sampleTasks[2].setPriority(TaskPriority.MEDIUM);
sampleTasks[2].setAssignedTo("開発次郎");
sampleTasks[3].setStatus(TaskStatus.TODO);
sampleTasks[3].setPriority(TaskPriority.MEDIUM);
sampleTasks[3].setAssignedTo("開発花子");
sampleTasks[4].setStatus(TaskStatus.TODO);
sampleTasks[4].setPriority(TaskPriority.LOW);
sampleTasks[4].setAssignedTo("開発花子");
// 期限日時を設定(現在時刻から数日後)
LocalDateTime now = LocalDateTime.now();
sampleTasks[2].setDueDate(now.plusDays(3));
sampleTasks[3].setDueDate(now.plusDays(7));
sampleTasks[4].setDueDate(now.plusDays(14));
// データベースに挿入
for (Task task : sampleTasks) {
create(task);
}
}
}
期待される結果
TaskDAOの完全な実装により、データベースアクセス機能が提供され、CRUD操作とビジネス固有の検索機能が利用可能になります。
9.4 JSPによるプレゼンテーション層の実装
ELとJSTLを活用して、タスク管理システムのユーザーインターフェースを構築します。
実習 9-3: タスク管理画面の実装
タスクの一覧表示、詳細表示、新規作成、編集機能を持つJSPページを作成します。
手順1: タスク一覧画面(taskList.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ page import="com.example.dao.impl.TaskDAOImpl, com.example.dao.TaskDAO" %>
<%@ page import="com.example.model.Task, com.example.model.Task.TaskStatus, com.example.model.Task.TaskPriority" %>
<%@ page import="java.util.List, java.sql.SQLException" %>
<%
// DAOのインスタンス化とデータ取得
TaskDAO taskDAO = new TaskDAOImpl();
List<Task> tasks = null;
String errorMessage = null;
try {
// テーブル初期化
taskDAO.initializeTable();
// 初回アクセス時にサンプルデータを挿入
if (taskDAO.countAll() == 0) {
taskDAO.insertSampleData();
}
// フィルタ条件の取得
String statusFilter = request.getParameter("status");
String assignedToFilter = request.getParameter("assignedTo");
String priorityFilter = request.getParameter("priority");
// 条件に応じてタスクを取得
if (statusFilter != null && !statusFilter.isEmpty()) {
tasks = taskDAO.findByStatus(TaskStatus.valueOf(statusFilter));
} else if (assignedToFilter != null && !assignedToFilter.isEmpty()) {
tasks = taskDAO.findByAssignedTo(assignedToFilter);
} else if (priorityFilter != null && !priorityFilter.isEmpty()) {
tasks = taskDAO.findByPriority(TaskPriority.valueOf(priorityFilter));
} else {
tasks = taskDAO.findAll();
}
// 統計情報の取得
int totalTasks = taskDAO.countAll();
int todoTasks = taskDAO.countByStatus(TaskStatus.TODO);
int inProgressTasks = taskDAO.countByStatus(TaskStatus.IN_PROGRESS);
int completedTasks = taskDAO.countByStatus(TaskStatus.COMPLETED);
List<Task> overdueTasks = taskDAO.findOverdueTasks();
request.setAttribute("totalTasks", totalTasks);
request.setAttribute("todoTasks", todoTasks);
request.setAttribute("inProgressTasks", inProgressTasks);
request.setAttribute("completedTasks", completedTasks);
request.setAttribute("overdueTasks", overdueTasks);
} catch (SQLException e) {
errorMessage = "データベースエラー: " + e.getMessage();
e.printStackTrace();
}
request.setAttribute("tasks", tasks);
request.setAttribute("errorMessage", errorMessage);
// ステータス・優先度の選択肢
request.setAttribute("taskStatuses", TaskStatus.values());
request.setAttribute("taskPriorities", TaskPriority.values());
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タスク管理システム - タスク一覧</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
border-bottom: 2px solid #f57c00;
padding-bottom: 15px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #f57c00, #ff9800);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.todo { background: linear-gradient(135deg, #6c757d, #868e96); }
.stat-card.progress { background: linear-gradient(135deg, #0d6efd, #3d8bfd); }
.stat-card.completed { background: linear-gradient(135deg, #198754, #20c997); }
.stat-card.overdue { background: linear-gradient(135deg, #dc3545, #fd7e14); }
.filter-section {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.task-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.task-table th, .task-table td {
border: 1px solid #dee2e6;
padding: 12px;
text-align: left;
}
.task-table th {
background-color: #f57c00;
color: white;
font-weight: 600;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
color: white;
font-size: 0.85em;
font-weight: 500;
}
.priority-badge {
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 0.8em;
font-weight: bold;
}
.progress-bar {
width: 100px;
height: 20px;
background: #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #28a745;
transition: width 0.3s ease;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 0.9em;
margin: 2px;
display: inline-block;
}
.btn-primary { background: #0d6efd; color: white; }
.btn-success { background: #198754; color: white; }
.btn-warning { background: #ffc107; color: #212529; }
.btn-danger { background: #dc3545; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.error-message {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
.overdue { background-color: #fff5f5; }
.completed-task { opacity: 0.7; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 タスク管理システム</h1>
<p>JSP + JDBC + JavaBeansによる実践的Webアプリケーション</p>
</div>
<!-- エラーメッセージ表示 -->
<c:if test="${not empty errorMessage}">
<div class="error-message">
${errorMessage}
</div>
</c:if>
<!-- 統計情報 -->
<div class="stats-grid">
<div class="stat-card">
<h3>${totalTasks}</h3>
<p>総タスク数</p>
</div>
<div class="stat-card todo">
<h3>${todoTasks}</h3>
<p>未着手</p>
</div>
<div class="stat-card progress">
<h3>${inProgressTasks}</h3>
<p>進行中</p>
</div>
<div class="stat-card completed">
<h3>${completedTasks}</h3>
<p>完了</p>
</div>
<div class="stat-card overdue">
<h3>${fn:length(overdueTasks)}</h3>
<p>期限切れ</p>
</div>
</div>
<!-- フィルタセクション -->
<div class="filter-section">
<h3>🔍 フィルタ・操作</h3>
<a href="taskList.jsp" class="btn btn-secondary">全て表示</a>
<!-- ステータス別フィルタ -->
<c:forEach var="status" items="${taskStatuses}">
<a href="taskList.jsp?status=${status.name()}" class="btn btn-primary">
${status.displayName}
</a>
</c:forEach>
<a href="taskForm.jsp" class="btn btn-success">➕ 新規タスク作成</a>
<a href="taskList.jsp?status=overdue" class="btn btn-danger">⚠️ 期限切れタスク</a>
</div>
<!-- タスク一覧テーブル -->
<c:if test="${empty tasks}">
<div style="text-align: center; padding: 40px; color: #6c757d;">
<h3>📝 表示するタスクがありません</h3>
<p><a href="taskForm.jsp" class="btn btn-success">最初のタスクを作成</a></p>
</div>
</c:if>
<c:if test="${not empty tasks}">
<table class="task-table">
<thead>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>ステータス</th>
<th>優先度</th>
<th>担当者</th>
<th>進捗</th>
<th>期限</th>
<th>作成日</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach var="task" items="${tasks}" varStatus="status">
<tr class="${task.overdue and not task.completed ? 'overdue' : ''} ${task.completed ? 'completed-task' : ''}">
<td>${task.taskId}</td>
<td>
<strong>${task.title}</strong>
<c:if test="${not empty task.description}">
<br><small style="color: #6c757d;">${fn:substring(task.description, 0, 50)}...</small>
</c:if>
<c:if test="${task.overdue and not task.completed}">
<span style="color: #dc3545; font-weight: bold;"> ⚠️ 期限切れ</span>
</c:if>
</td>
<td>
<span class="status-badge" style="background-color: ${task.status.color};">
${task.status.displayName}
</span>
</td>
<td>
<span class="priority-badge" style="background-color: ${task.priority.color};">
${task.priority.displayName}
</span>
</td>
<td>${task.assignedTo}</td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: ${task.progressPercentage}%;"></div>
</div>
<small>${task.progressPercentage}%</small>
</td>
<td>
<c:if test="${not empty task.formattedDueDate}">
${task.formattedDueDate}
</c:if>
<c:if test="${empty task.formattedDueDate}">
<span style="color: #6c757d;">未設定</span>
</c:if>
</td>
<td>${task.formattedCreatedAt}</td>
<td>
<a href="taskDetail.jsp?id=${task.taskId}" class="btn btn-primary">詳細</a>
<a href="taskForm.jsp?id=${task.taskId}" class="btn btn-warning">編集</a>
<a href="taskDelete.jsp?id=${task.taskId}" class="btn btn-danger"
onclick="return confirm('タスク「${task.title}」を削除しますか?')">削除</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
<!-- フッター -->
<div style="margin-top: 40px; text-align: center; color: #6c757d; border-top: 1px solid #dee2e6; padding-top: 20px;">
<p>JSP + JDBC + JavaBeans タスク管理システム デモアプリケーション</p>
<p><small>総件数: ${fn:length(tasks)}件 | 最終更新: <fmt:formatDate value="<%= new java.util.Date() %>" pattern="yyyy/MM/dd HH:mm:ss" /></small></p>
</div>
</div>
</body>
</html>
手順2: タスク詳細画面(taskDetail.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ page import="com.example.dao.impl.TaskDAOImpl, com.example.dao.TaskDAO" %>
<%@ page import="com.example.model.Task" %>
<%@ page import="java.sql.SQLException" %>
<%
String taskIdStr = request.getParameter("id");
Task task = null;
String errorMessage = null;
if (taskIdStr == null || taskIdStr.trim().isEmpty()) {
errorMessage = "タスクIDが指定されていません。";
} else {
try {
Integer taskId = Integer.valueOf(taskIdStr);
TaskDAO taskDAO = new TaskDAOImpl();
taskDAO.initializeTable();
task = taskDAO.findById(taskId);
if (task == null) {
errorMessage = "指定されたタスク(ID: " + taskId + ")が見つかりません。";
}
} catch (NumberFormatException e) {
errorMessage = "無効なタスクIDです: " + taskIdStr;
} catch (SQLException e) {
errorMessage = "データベースエラー: " + e.getMessage();
}
}
request.setAttribute("task", task);
request.setAttribute("errorMessage", errorMessage);
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タスク詳細 - タスク管理システム</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.task-header {
border-bottom: 2px solid #f57c00;
padding-bottom: 15px;
margin-bottom: 30px;
}
.task-title {
color: #f57c00;
margin: 0 0 10px 0;
font-size: 2.2em;
}
.task-meta {
color: #6c757d;
font-size: 0.95em;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.info-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.info-label {
font-weight: 600;
color: #495057;
margin-bottom: 8px;
}
.info-value {
font-size: 1.1em;
}
.status-badge, .priority-badge {
padding: 8px 16px;
border-radius: 20px;
color: white;
font-weight: 600;
display: inline-block;
}
.description-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.progress-section {
text-align: center;
padding: 20px;
background: #e3f2fd;
border-radius: 8px;
margin-bottom: 30px;
}
.progress-circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: #e9ecef;
margin: 0 auto 15px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
font-weight: bold;
color: #495057;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
font-size: 1em;
margin: 5px;
display: inline-block;
font-weight: 500;
}
.btn-primary { background: #0d6efd; color: white; }
.btn-warning { background: #ffc107; color: #212529; }
.btn-danger { background: #dc3545; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.error-message {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
.overdue-warning {
background: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #ffeaa7;
}
.actions-section {
text-align: center;
padding-top: 30px;
border-top: 1px solid #dee2e6;
}
</style>
</head>
<body>
<div class="container">
<!-- エラーメッセージ表示 -->
<c:if test="${not empty errorMessage}">
<div class="error-message">
${errorMessage}
</div>
<div style="text-align: center;">
<a href="taskList.jsp" class="btn btn-secondary">← タスク一覧に戻る</a>
</div>
</c:if>
<c:if test="${not empty task}">
<!-- タスクヘッダー -->
<div class="task-header">
<h1 class="task-title">${task.title}</h1>
<div class="task-meta">
Task ID: #${task.taskId} | 作成日: ${task.formattedCreatedAt} | 最終更新: ${task.formattedUpdatedAt}
</div>
</div>
<!-- 期限切れ警告 -->
<c:if test="${task.overdue and not task.completed}">
<div class="overdue-warning">
⚠️ <strong>このタスクは期限切れです</strong>
<br>期限: ${task.formattedDueDate}
</div>
</c:if>
<!-- 進捗セクション -->
<div class="progress-section">
<h3>進捗状況</h3>
<div class="progress-circle" style="background: conic-gradient(${task.status.color} ${task.progressPercentage * 3.6}deg, #e9ecef 0deg);">
${task.progressPercentage}%
</div>
<p>${task.status.displayName}</p>
</div>
<!-- 基本情報グリッド -->
<div class="info-grid">
<div class="info-section">
<div class="info-label">ステータス</div>
<div class="info-value">
<span class="status-badge" style="background-color: ${task.status.color};">
${task.status.displayName}
</span>
</div>
</div>
<div class="info-section">
<div class="info-label">優先度</div>
<div class="info-value">
<span class="priority-badge" style="background-color: ${task.priority.color};">
${task.priority.displayName}
</span>
</div>
</div>
<div class="info-section">
<div class="info-label">担当者</div>
<div class="info-value">
<c:if test="${not empty task.assignedTo}">
👤 ${task.assignedTo}
</c:if>
<c:if test="${empty task.assignedTo}">
<span style="color: #6c757d;">未設定</span>
</c:if>
</div>
</div>
<div class="info-section">
<div class="info-label">期限日時</div>
<div class="info-value">
<c:if test="${not empty task.formattedDueDate}">
📅 ${task.formattedDueDate}
</c:if>
<c:if test="${empty task.formattedDueDate}">
<span style="color: #6c757d;">未設定</span>
</c:if>
</div>
</div>
</div>
<!-- 説明セクション -->
<div class="description-section">
<h3>📝 説明</h3>
<c:if test="${not empty task.description}">
<p style="line-height: 1.6; font-size: 1.05em;">${task.description}</p>
</c:if>
<c:if test="${empty task.description}">
<p style="color: #6c757d; font-style: italic;">説明は設定されていません。</p>
</c:if>
</div>
<!-- 詳細情報 -->
<div class="info-section">
<h3>📊 詳細情報</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-weight: 600;">タスクID</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">#${task.taskId}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-weight: 600;">作成日時</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${task.formattedCreatedAt}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-weight: 600;">最終更新日時</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">${task.formattedUpdatedAt}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6; font-weight: 600;">完了判定</td>
<td style="padding: 8px; border-bottom: 1px solid #dee2e6;">
${task.completed ? '✅ 完了' : '⏳ 未完了'}
</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: 600;">期限切れ判定</td>
<td style="padding: 8px;">
${task.overdue and not task.completed ? '⚠️ 期限切れ' : '⏰ 正常'}
</td>
</tr>
</table>
</div>
<!-- アクションセクション -->
<div class="actions-section">
<a href="taskForm.jsp?id=${task.taskId}" class="btn btn-warning">✏️ 編集</a>
<a href="taskDelete.jsp?id=${task.taskId}" class="btn btn-danger"
onclick="return confirm('タスク「${task.title}」を削除しますか?')">🗑️ 削除</a>
<a href="taskList.jsp" class="btn btn-secondary">📋 一覧に戻る</a>
</div>
</c:if>
</div>
</body>
</html>
期待される結果
タスクの詳細情報が視覚的に分かりやすく表示され、ELとJSTLを活用した動的なコンテンツ生成を確認できます。
理解度確認クイズ
-
MVCアーキテクチャの理解
JSP、JavaBeans、DAOクラスを使用したWebアプリケーションにおいて、各コンポーネントが担う役割を説明してください。また、なぜこの役割分担が重要なのか説明してください。 -
DAOパターンの利点
DAOパターンを使用することで得られる利点を3つ挙げ、それぞれについて具体例を交えて説明してください。 -
エラーハンドリング
データベースアクセス時に発生する可能性がある例外(SQLException等)をJSPページでどのように処理すべきか、コード例とともに説明してください。 -
CRUD操作の実装
タスク管理システムで必要となるCRUD操作(Create、Read、Update、Delete)のうち、Create操作をJSP + DAO で実装する際の処理フローを説明してください。 -
パフォーマンス最適化
データベース接続を伴うWebアプリケーションでパフォーマンスを向上させるための手法を3つ挙げ、それぞれの実装方法を説明してください。
9.5 まとめ
この章では、JSP、JDBC、JavaBeansを統合した実践的なWebアプリケーション開発について学習しました。
重要なポイント
- MVCアーキテクチャ: プレゼンテーション層、ビジネスロジック層、データアクセス層の適切な分離
- DAOパターン: データアクセスの抽象化によるメンテナンス性向上
- JavaBeans活用: データ構造の標準化とビジネスルールの実装
- EL/JSTL統合: 保守性の高いJSPページの実装
- エラーハンドリング: 堅牢なWebアプリケーションの構築
実用化に向けた追加検討事項
- セキュリティ対策: SQLインジェクション、XSS対策の実装
- 接続プーリング: データベース接続の効率的な管理
- トランザクション管理: データ整合性の保証
- ログ機能: アプリケーションの動作監視
- 国際化対応: 多言語サポートの実装