第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)]

各層の責務

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を活用した動的なコンテンツ生成を確認できます。

理解度確認クイズ
  1. MVCアーキテクチャの理解
    JSP、JavaBeans、DAOクラスを使用したWebアプリケーションにおいて、各コンポーネントが担う役割を説明してください。また、なぜこの役割分担が重要なのか説明してください。
  2. DAOパターンの利点
    DAOパターンを使用することで得られる利点を3つ挙げ、それぞれについて具体例を交えて説明してください。
  3. エラーハンドリング
    データベースアクセス時に発生する可能性がある例外(SQLException等)をJSPページでどのように処理すべきか、コード例とともに説明してください。
  4. CRUD操作の実装
    タスク管理システムで必要となるCRUD操作(Create、Read、Update、Delete)のうち、Create操作をJSP + DAO で実装する際の処理フローを説明してください。
  5. パフォーマンス最適化
    データベース接続を伴うWebアプリケーションでパフォーマンスを向上させるための手法を3つ挙げ、それぞれの実装方法を説明してください。

9.5 まとめ

この章では、JSP、JDBC、JavaBeansを統合した実践的なWebアプリケーション開発について学習しました。

重要なポイント
  • MVCアーキテクチャ: プレゼンテーション層、ビジネスロジック層、データアクセス層の適切な分離
  • DAOパターン: データアクセスの抽象化によるメンテナンス性向上
  • JavaBeans活用: データ構造の標準化とビジネスルールの実装
  • EL/JSTL統合: 保守性の高いJSPページの実装
  • エラーハンドリング: 堅牢なWebアプリケーションの構築
実用化に向けた追加検討事項
  • セキュリティ対策: SQLインジェクション、XSS対策の実装
  • 接続プーリング: データベース接続の効率的な管理
  • トランザクション管理: データ整合性の保証
  • ログ機能: アプリケーションの動作監視
  • 国際化対応: 多言語サポートの実装