📚 Spring Boot タスク管理アプリケーション 開発手順書
🎯 はじめに
この手順書では、Spring Bootを使用したタスク管理アプリケーションを ゼロから 作成する方法を詳しく説明します。Java初学者でも理解できるよう、各ステップを丁寧に解説し、なぜその操作が必要なのかも併せて説明しています。
📋 目次
1. 開発環境の準備
🎯 目的
Spring Bootアプリケーションを開発・実行するために必要なソフトウェアをインストールします。これらがないとプログラムの作成や実行ができません。
1.1 必要なソフトウェア
以下のソフトウェアをインストールしてください:
Java Development Kit (JDK)
目的: Javaプログラムをコンパイル(機械語に翻訳)し、実行するために必要
理由: Spring BootはJavaフレームワークなので、Java実行環境が必須
# macOSの場合(Homebrewを使用)
brew install openjdk@17
# Windowsの場合
# Oracle JDK または OpenJDK 17以上をダウンロードしてインストール
Maven
目的: プロジェクトのビルド(コンパイル・パッケージ化)と依存関係管理を自動化
理由: 手動でライブラリを管理するのは非効率で、依存関係の競合も起こりやすいため
# macOSの場合
brew install maven
# Windowsの場合
# Apache Mavenの公式サイトからダウンロードしてインストール
IDE(統合開発環境)
目的: コード作成、デバッグ、実行を効率的に行うため
理由: メモ帳でもプログラムは書けるが、構文ハイライト、自動補完、エラー検出などの機能で開発効率が大幅に向上する
- IntelliJ IDEA (推奨) - Spring Boot開発に最適化された機能が豊富
- Eclipse - 無料で高機能、Java開発の定番
- Visual Studio Code (Spring Boot Extension Pack) - 軽量で拡張性が高い
1.2 環境確認
目的: インストールが正しく完了し、コマンドが使用可能か確認
理由: 環境に問題があると後の手順でエラーが発生し、原因特定が困難になる
ターミナル(コマンドプロンプト)で以下のコマンドを実行して、正しくインストールされているか確認:
# Javaバージョン確認
# 目的: Java実行環境の動作確認とバージョン確認
java -version
# Mavenバージョン確認
# 目的: Mavenコマンドが使用可能か確認
mvn -version
2. プロジェクトの作成
🎯 目的
Spring Bootアプリケーションの基盤となるプロジェクト構造を作成します。適切な構造により、コードの整理と保守性が向上します。
2.1 プロジェクトフォルダの作成
目的: プロジェクト専用のディレクトリを作成し、ファイルを整理
理由: 他のプロジェクトとファイルが混在すると管理が困難になる
# ワークスペースディレクトリに移動
cd ~/Desktop
# プロジェクトフォルダを作成
# 目的: タスク管理アプリ専用の作業領域を確保
mkdir タスク管理
cd タスク管理
2.2 Spring Boot プロジェクトの初期化
目的: Spring Bootアプリケーションの基本構造と必要な依存関係を自動生成
理由: 手動でプロジェクト構造を作成するのは時間がかかり、設定ミスも起こりやすい
Spring Initializrを使用してプロジェクトを作成します:
方法1: Web版 Spring Initializr
目的: ブラウザ上で簡単にプロジェクト設定を行い、カスタマイズされたプロジェクトをダウンロード
-
https://start.spring.io/ にアクセス
-
以下の設定を選択:
- Project: Maven(ビルドツール選択 - 依存関係管理とビルド自動化のため)
- Language: Java(プログラミング言語選択)
- Spring Boot: 3.2.0(フレームワークバージョン - 最新安定版で最新機能を利用)
- Group: com.example(パッケージの基本構造 - 組織識別のため)
- Artifact: taskmanagement(プロジェクト名 - 成果物の識別名)
- Name: taskmanagement(表示名)
- Package name: com.example.taskmanagement(Javaパッケージ名)
- Packaging: Jar(実行可能な単一ファイルとして配布するため)
- Java: 17(Java言語バージョン - 最新の安定版)
-
依存関係を追加:
- Spring Web(Webアプリケーション機能 - HTTP処理とMVC機能のため)
- Spring Data JPA(データベース操作簡素化 - SQLを書かずにデータ操作するため)
- Thymeleaf(テンプレートエンジン - 動的なHTML生成のため)
- Validation(入力値検証 - データの整合性確保のため)
-
GENERATE ボタンをクリックしてダウンロード
方法2: コマンドライン
目的: コマンドで自動化し、繰り返し可能な方法でプロジェクトを作成
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,thymeleaf,validation \
-d type=maven-project \
-d language=java \
-d bootVersion=3.2.0 \
-d baseDir=taskmanagement \
-d groupId=com.example \
-d artifactId=taskmanagement \
-o taskmanagement.zip
unzip taskmanagement.zip
2.3 プロジェクト構造の確認
目的: 生成されたプロジェクト構造を理解し、各ディレクトリの役割を把握
理由: ファイルの配置場所を理解することで、適切な場所にコードを作成できる
作成されたプロジェクトの構造を確認:
タスク管理/
├── pom.xml # Maven設定ファイル(依存関係とビルド設定)
├── src/
│ ├── main/
│ │ ├── java/ # Javaソースコード配置場所
│ │ │ └── com/example/taskmanagement/
│ │ │ └── TaskManagementApplication.java # アプリケーションのエントリーポイント
│ │ └── resources/ # 設定ファイルとリソース配置場所
│ │ ├── application.properties # アプリケーション設定ファイル
│ │ ├── static/ # CSS, JS, 画像ファイル(静的リソース)
│ │ └── templates/ # Thymeleafテンプレート(動的HTML)
│ └── test/ # テストファイル(品質保証のため)
└── target/ # ビルド成果物(自動生成、通常は無視)
3. 基本設定
🎯 目的
アプリケーションがデータベースと連携し、適切に動作するための基本設定を行います。設定により、フレームワークの動作をカスタマイズできます。
3.1 pom.xml の編集
目的: SQLiteデータベースを使用するために必要な依存関係を追加
理由: Spring BootのデフォルトはH2データベースですが、SQLiteの方がファイルベースで理解しやすく、実用的
SQLite依存関係を追加します:
<!-- 既存の依存関係の後に追加 -->
<!-- SQLite JDBCドライバー - SQLiteデータベースに接続するため -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.44.1.0</version>
</dependency>
<!-- Hibernate SQLite方言 - HibernateがSQLite特有のSQL文法を理解するため -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>
3.2 application.properties の設定
目的: データベース接続設定とアプリケーションの動作設定を定義
理由: 設定ファイルにより、コードを変更せずに動作を調整でき、環境ごとに異なる設定も可能
src/main/resources/application.properties
ファイルを編集:
# データベース設定
# 目的: SQLiteデータベースファイルの場所と接続方法を指定
spring.datasource.url=jdbc:sqlite:data/taskmanagement.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
# JPA/Hibernate設定
# 目的: データベーススキーマの自動管理とSQL出力設定
spring.jpa.hibernate.ddl-auto=update # テーブル構造を自動更新(開発時便利)
spring.jpa.show-sql=true # 実行されるSQLを表示(デバッグ用)
spring.jpa.properties.hibernate.format_sql=true # SQLを見やすく整形
# ログ設定
# 目的: デバッグ時に詳細な情報を出力し、問題の原因特定を容易にする
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Thymeleaf設定
# 目的: 開発時にテンプレートの変更を即座に反映(キャッシュ無効化)
spring.thymeleaf.cache=false
3.3 データベースディレクトリの作成
目的: SQLiteデータベースファイルを保存するディレクトリを事前に作成
理由: ディレクトリが存在しないとデータベースファイルの作成に失敗する可能性がある
# プロジェクトルートで実行
# 目的: データベースファイル専用の保存場所を確保
mkdir data
4. エンティティクラスの作成
🎯 目的
データベースのテーブル構造を表現するJavaクラスを作成します。エンティティクラスにより、オブジェクト指向プログラミングでデータベース操作が可能になります。
4.1 Taskエンティティの作成
目的: タスクデータの構造を定義し、データベースとJavaオブジェクトの橋渡しを行う
理由: JPAを使用することで、SQLを直接書かずにオブジェクト操作でデータベース操作が可能
src/main/java/com/example/taskmanagement/entity/Task.java
を作成:
package com.example.taskmanagement.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* タスクエンティティクラス
* 目的: タスクデータの構造を定義し、データベーステーブルとマッピング
*/
@Entity // JPAエンティティであることを示す(データベーステーブルと対応)
@Table(name = "tasks") // データベースのテーブル名を明示的に指定
public class Task {
/**
* 優先度を表すenum
* 目的: 優先度の値を制限し、タイプセーフな操作を可能にする
* 理由: 文字列での管理だと入力ミスや不正な値が入る可能性がある
*/
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
private final String displayName;
Priority(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
/**
* 主キー(一意識別子)
* 目的: 各タスクを一意に識別するためのID
*/
@Id
private Long id;
/**
* タスクタイトル
* 目的: タスクの概要を表す必須項目
* バリデーション: 空白不可、100文字以内
*/
@NotBlank(message = "タイトルは必須です") // 空白チェック
@Size(max = 100, message = "タイトルは100文字以内で入力してください") // 文字数制限
@Column(nullable = false, length = 100) // データベース制約
private String title;
/**
* タスク詳細説明
* 目的: タスクの詳細情報(任意項目)
*/
@Column(length = 500) // 最大500文字
private String description;
/**
* 完了状態
* 目的: タスクが完了しているかどうかを管理
*/
@NotNull // null不可
@Column(nullable = false)
private Boolean completed = false; // デフォルトは未完了
/**
* 優先度
* 目的: タスクの重要度を管理し、優先順位付けを可能にする
*/
@Enumerated(EnumType.STRING) // enumを文字列としてデータベースに保存
@Column(nullable = false)
private Priority priority = Priority.MEDIUM; // デフォルトは中優先度
/**
* 作成日時
* 目的: タスクがいつ作成されたかを記録(自動設定)
*/
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
/**
* 更新日時
* 目的: タスクが最後に更新された日時を記録(自動更新)
*/
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
// コンストラクタ
/**
* デフォルトコンストラクタ
* 目的: JPAがエンティティをインスタンス化するために必要
*/
public Task() {}
/**
* 便利コンストラクタ
* 目的: タイトルと説明を指定してタスクを簡単に作成
*/
public Task(String title, String description) {
this.title = title;
this.description = description;
}
/**
* ライフサイクルコールバック
* 目的: エンティティの保存・更新時に自動的に日時を設定
*/
@PrePersist // 初回保存前に実行
protected void onCreate() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
updatedAt = LocalDateTime.now();
}
@PreUpdate // 更新前に実行
protected void onUpdate() {
// 作成日時が未設定の場合のみ設定(更新時に作成日時を変更しない)
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
updatedAt = LocalDateTime.now();
}
/**
* ビジネスメソッド
* 目的: タスクの完了状態を切り替える業務ロジック
*/
public void toggleCompleted() {
Boolean oldValue = this.completed;
this.completed = !this.completed;
// デバッグ用ログ出力
System.out.println("Task ID: " + this.id + " - Completed changed from " + oldValue + " to " + this.completed);
}
/**
* 高優先度判定メソッド
* 目的: 高優先度タスクかどうかを簡単に判定
*/
public boolean isHighPriority() {
return this.priority == Priority.HIGH;
}
// Getter and Setter methods
// 目的: プライベートフィールドへの安全なアクセスを提供
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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 Boolean getCompleted() { return completed; }
public void setCompleted(Boolean completed) { this.completed = completed; }
public Priority getPriority() { return priority; }
public void setPriority(Priority priority) { this.priority = priority; }
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; }
/**
* オブジェクト比較メソッド
* 目的: 同じIDを持つタスクを同一オブジェクトとして扱う
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Task task = (Task) obj;
return Objects.equals(id, task.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
/**
* 文字列表現メソッド
* 目的: デバッグ時にオブジェクトの内容を分かりやすく表示
*/
@Override
public String toString() {
return "Task{" +
"id=" + id +
", title='" + title + '\'' +
", completed=" + completed +
'}';
}
}
4.2 エンティティクラスの解説
アノテーションの説明と目的
-
@Entity
: このクラスがデータベースエンティティであることをJPAに伝える -
@Table(name = "tasks")
: データベースのテーブル名を明示的に指定(クラス名と異なる場合に使用) -
@Id
: 主キーフィールドを示す(データベースの一意制約) -
@Column
: データベースカラムの詳細設定(長さ制限、null制約など) -
@NotBlank
,@Size
: 入力値のバリデーションルール(データ整合性の確保)
enumの活用とその理由
public enum Priority {
LOW("低"), MEDIUM("中"), HIGH("高");
// 目的: 優先度の値を限定し、タイプセーフな操作を実現
// 理由: 文字列だと入力ミスや不正な値が入る可能性があるため
}
5. リポジトリ層の作成
🎯 目的
データベースアクセス層を作成し、エンティティの永続化操作を簡素化します。Spring Data JPAにより、複雑なSQL文を書かずにデータベース操作が可能になります。
5.1 TaskRepositoryの作成
目的: Taskエンティティのデータベース操作を抽象化し、再利用可能なデータアクセス層を提供
理由: ビジネスロジックとデータアクセスロジックを分離することで、保守性と可読性が向上
src/main/java/com/example/taskmanagement/repository/TaskRepository.java
を作成:
package com.example.taskmanagement.repository;
import com.example.taskmanagement.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* タスクリポジトリインターフェース
* 目的: タスクエンティティのデータベース操作を定義
* 理由: データアクセス層を抽象化し、ビジネスロジックから分離
*/
@Repository // Spring管理のリポジトリコンポーネントとして登録
public interface TaskRepository extends JpaRepository<Task, Long> {
// JpaRepository<Task, Long>を継承することで基本的なCRUD操作が自動提供される
// Task: エンティティ型, Long: 主キーの型
/**
* 完了状態で検索
* 目的: 完了済み/未完了のタスクを絞り込み表示するため
* 命名規則: findBy + フィールド名 でクエリが自動生成される
*/
List<Task> findByCompleted(Boolean completed);
/**
* 優先度で検索
* 目的: 特定の優先度のタスクのみを表示するフィルタ機能
*/
List<Task> findByPriority(Task.Priority priority);
/**
* 優先度と完了状態で検索
* 目的: 複数条件での絞り込み検索(例:高優先度の未完了タスク)
* 命名規則: And で複数条件を結合
*/
List<Task> findByPriorityAndCompleted(Task.Priority priority, Boolean completed);
/**
* キーワード検索(タイトルまたは説明文)
* 目的: ユーザーが入力したキーワードでタスクを検索
* 理由: 命名規則だけでは複雑な検索ができないため、カスタムクエリを使用
*/
@Query("SELECT t FROM Task t WHERE " +
"LOWER(t.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"LOWER(t.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
List<Task> searchByKeyword(@Param("keyword") String keyword);
/**
* 最大IDを取得(手動ID管理用)
* 目的: SQLiteの自動生成キー問題を回避するため、手動でIDを管理
* 理由: SQLiteのJDBCドライバーが生成されたキーの取得をサポートしていない
*/
@Query("SELECT MAX(t.id) FROM Task t")
Long findMaxId();
}
5.2 リポジトリの解説
Spring Data JPAの特徴と利点
- インターフェース定義のみ: 実装クラスは Spring が自動生成するため、ボイラープレートコードが不要
- 命名規則による自動クエリ生成: メソッド名からSQLクエリが自動生成される
- @Query: 複雑な検索条件は JPQL で記述可能
メソッド命名規則の例と目的
// 目的: 完了状態でタスクを検索
findByCompleted → WHERE completed = ?
// 目的: 複数条件での検索
findByPriorityAndCompleted → WHERE priority = ? AND completed = ?
// 目的: 部分一致検索
findByTitleContaining → WHERE title LIKE %?%
カスタムクエリの必要性
// 複雑な検索ロジックは命名規則では表現できないため
@Query("SELECT t FROM Task t WHERE " +
"LOWER(t.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"LOWER(t.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
// 目的: タイトルまたは説明文での部分一致検索(大文字小文字を区別しない)
6. サービス層の作成
🎯 目的
ビジネスロジックを実装し、コントローラーとリポジトリの間を仲介します。サービス層により、複雑な業務処理を整理し、再利用可能な形で提供できます。
6.1 TaskServiceの作成
目的: タスク管理に関する業務処理を集約し、トランザクション管理と共に提供
理由: コントローラーに業務ロジックを書くと、コードが複雑になり、テストも困難になる
src/main/java/com/example/taskmanagement/service/TaskService.java
を作成:
package com.example.taskmanagement.service;
import com.example.taskmanagement.entity.Task;
import com.example.taskmanagement.repository.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* タスクサービスクラス
* 目的: タスク管理の業務ロジックを実装し、トランザクション管理を行う
* 理由: ビジネスロジックをコントローラーから分離し、再利用性と保守性を向上
*/
@Service // Spring管理のサービスコンポーネントとして登録
public class TaskService {
private final TaskRepository taskRepository;
/**
* コンストラクタインジェクション
* 目的: 依存関係を注入し、テストしやすい設計にする
* 理由: フィールドインジェクションより安全で、final修飾子が使用可能
*/
@Autowired
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
// 基本的なCRUD操作
/**
* 全タスク取得
* 目的: 全てのタスクを取得してタスク一覧画面で表示
* @Transactional(readOnly = true): 読み取り専用トランザクションで性能最適化
*/
@Transactional(readOnly = true)
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
/**
* ID指定でタスク取得(Optional版)
* 目的: 指定されたIDのタスクを安全に取得
* Optional使用理由: null チェックを強制し、NullPointerException を防ぐ
*/
@Transactional(readOnly = true)
public Optional<Task> getTaskById(Long id) {
return taskRepository.findById(id);
}
/**
* ID指定でタスク取得(例外スロー版)
* 目的: タスクが必ず存在することを前提とした処理で使用
* 理由: Optional の処理を簡略化し、存在しない場合は明確にエラーとする
*/
@Transactional(readOnly = true)
public Task getTaskByIdOrThrow(Long id) {
return taskRepository.findById(id)
.orElseThrow(() -> new RuntimeException("タスクが見つかりません。ID: " + id));
}
/**
* タスク保存(作成・更新)
* 目的: タスクの新規作成と更新を統一的に処理
* @Transactional: データベース操作の原子性を保証(全て成功するか全て失敗)
*/
@Transactional
public Task saveTask(Task task) {
// バリデーション - 目的: データの整合性を確保
if (task.getTitle() == null || task.getTitle().trim().isEmpty()) {
throw new IllegalArgumentException("タスクのタイトルは必須です");
}
// 手動ID管理(SQLite対応)
// 目的: SQLiteのJDBCドライバーが自動生成キーをサポートしていない問題を回避
if (task.getId() == null) {
Long maxId = taskRepository.findMaxId();
task.setId(maxId != null ? maxId + 1 : 1L);
}
return taskRepository.save(task);
}
/**
* タスク削除
* 目的: 指定されたIDのタスクをデータベースから削除
*/
@Transactional
public void deleteTask(Long id) {
taskRepository.deleteById(id);
}
/**
* タスク完了状態切り替え
* 目的: タスクの完了/未完了状態をワンクリックで切り替え
* 理由: ユーザビリティ向上のため、簡単に状態変更できる機能を提供
*/
@Transactional
public Task toggleTaskCompletion(Long id) {
Task task = getTaskByIdOrThrow(id);
task.toggleCompleted(); // エンティティのビジネスメソッドを使用
return taskRepository.save(task);
}
// 検索メソッド群
/**
* 完了状態で検索
* 目的: 完了済み/未完了タスクのフィルタリング機能を提供
*/
@Transactional(readOnly = true)
public List<Task> getTasksByCompleted(Boolean completed) {
return taskRepository.findByCompleted(completed);
}
/**
* 優先度で検索
* 目的: 特定の優先度のタスクのみを表示
*/
@Transactional(readOnly = true)
public List<Task> getTasksByPriority(Task.Priority priority) {
return taskRepository.findByPriority(priority);
}
/**
* 優先度と完了状態で検索
* 目的: 複数条件での絞り込み検索を提供
*/
@Transactional(readOnly = true)
public List<Task> getTasksByPriorityAndCompleted(Task.Priority priority, Boolean completed) {
return taskRepository.findByPriorityAndCompleted(priority, completed);
}
/**
* キーワード検索
* 目的: ユーザーが入力したキーワードでタスクを検索
* 理由: タスクが多くなった時の検索機能として必要
*/
@Transactional(readOnly = true)
public List<Task> searchTasks(String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return getAllTasks(); // キーワードが空の場合は全件表示
}
return taskRepository.searchByKeyword(keyword.trim());
}
/**
* Stream APIを使った高度な検索
* 目的: 高優先度の未完了タスクを作成日時の降順で取得
* 理由: 緊急性の高いタスクを優先的に表示するダッシュボード機能
*/
@Transactional(readOnly = true)
public List<Task> getHighPriorityIncompleteTasks() {
return getAllTasks()
.stream()
.filter(task -> !task.getCompleted()) // 未完了タスクのみ
.filter(task -> task.getPriority() == Task.Priority.HIGH) // 高優先度のみ
.sorted(Comparator.comparing(Task::getCreatedAt).reversed()) // 作成日時降順
.collect(Collectors.toList());
}
/**
* 統計情報取得
* 目的: ダッシュボードで表示する統計データを計算
* 理由: ユーザーがタスクの進捗状況を一目で把握できるようにする
*/
@Transactional(readOnly = true)
public TaskStatistics getTaskStatistics() {
List<Task> allTasks = getAllTasks();
long totalTasks = allTasks.size();
// Stream API で完了タスク数を計算
long completedTasks = allTasks.stream()
.mapToLong(task -> task.getCompleted() ? 1 : 0)
.sum();
long incompleteTasks = totalTasks - completedTasks;
// 高優先度タスク数を計算
long highPriorityTasks = allTasks.stream()
.mapToLong(task -> task.getPriority() == Task.Priority.HIGH ? 1 : 0)
.sum();
return new TaskStatistics(totalTasks, completedTasks, incompleteTasks, highPriorityTasks);
}
/**
* 優先度別タスク数取得
* 目的: 優先度ごとのタスク分布を可視化
* Stream API使用理由: 関数型プログラミングで簡潔に集計処理を記述
*/
@Transactional(readOnly = true)
public Map<Task.Priority, Long> getTaskCountByPriority() {
return getAllTasks()
.stream()
.collect(Collectors.groupingBy(
Task::getPriority, // グループ化キー
Collectors.counting() // 各グループの要素数をカウント
));
}
/**
* 統計情報クラス
* 目的: 統計データを構造化して保持し、計算ロジックも含む
* 理由: データと関連ロジックを一箇所にまとめ、保守性を向上
*/
public static class TaskStatistics {
private final long totalTasks;
private final long completedTasks;
private final long incompleteTasks;
private final long highPriorityTasks;
public TaskStatistics(long totalTasks, long completedTasks,
long incompleteTasks, long highPriorityTasks) {
this.totalTasks = totalTasks;
this.completedTasks = completedTasks;
this.incompleteTasks = incompleteTasks;
this.highPriorityTasks = highPriorityTasks;
}
// Getter methods
public long getTotalTasks() { return totalTasks; }
public long getCompletedTasks() { return completedTasks; }
public long getIncompleteTasks() { return incompleteTasks; }
public long getHighPriorityTasks() { return highPriorityTasks; }
/**
* 完了率計算
* 目的: パーセンテージでの進捗表示
*/
public double getCompletionRate() {
return totalTasks > 0 ? (double) completedTasks / totalTasks : 0.0;
}
/**
* 完了率の文字列表現
* 目的: UI表示用のフォーマット済み文字列を提供
*/
public String getCompletionPercentage() {
return String.format("%.1f%%", getCompletionRate() * 100);
}
}
}
6.2 サービス層の解説
トランザクション管理の目的
-
@Transactional
: データベース操作の原子性を保証(全て成功するか全て失敗) -
readOnly = true
: 読み取り専用の最適化
Stream APIの活用理由
// 目的: 関数型プログラミングで簡潔で読みやすいコードを記述
.filter(task -> !task.getCompleted()) // 条件でフィルタリング
.filter(task -> task.getPriority() == Task.Priority.HIGH) // 複数条件
.sorted(Comparator.comparing(Task::getCreatedAt).reversed()) // ソート
依存性注入の利点
// コンストラクタインジェクション使用理由:
// 1. final修飾子が使用可能(不変性)
// 2. テスト時にモックオブジェクトを注入しやすい
// 3. 循環依存を防げる
private final TaskRepository taskRepository;
📚 Spring Boot タスク管理アプリケーション 開発手順書(コントローラー層)
🎯 目的
コントローラー層は、WebブラウザからのHTTPリクエストを受け取り、適切なサービスを呼び出して、結果をビューに渡すWeb層の中核部分です。MVCアーキテクチャにおけるController部分を担当し、ユーザーインターフェースとビジネスロジックを橋渡しします。
7. コントローラー層の作成
🎯 目的
Webアプリケーションのリクエスト処理を担当するコントローラー層を作成します。コントローラーにより、ユーザーの操作(クリック、フォーム送信など)を適切なビジネスロジックに変換し、結果を画面に表示できます。
7.1 TaskControllerの作成
目的: HTTPリクエストを受け取り、サービス層と連携してレスポンスを返すWeb層の制御
理由: ユーザーの操作とビジネスロジックを分離し、Webアプリケーションとしての機能を提供
src/main/java/com/example/taskmanagement/controller/TaskController.java
を作成:
package com.example.taskmanagement.controller;
import com.example.taskmanagement.entity.Task;
import com.example.taskmanagement.service.TaskService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* タスクコントローラークラス
* 目的: Webリクエストを処理し、タスク管理機能のHTTPエンドポイントを提供
* 理由: ユーザーのブラウザ操作とサーバーサイドロジックを橋渡しするため
*/
@Controller // Spring MVCのコントローラーコンポーネントとして登録
public class TaskController {
private final TaskService taskService;
/**
* コンストラクタインジェクション
* 目的: TaskServiceを注入し、ビジネスロジックを利用可能にする
* 理由: コントローラーは薄く保ち、ビジネスロジックはサービス層に委譲するため
*/
@Autowired
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
// ===== 基本的なページ表示メソッド =====
/**
* ルートパスのリダイレクト
* 目的: アプリケーションのルートURL(/)にアクセスした際にタスク一覧へリダイレクト
* 理由: ユーザーがドメインのみでアクセスした場合の適切な誘導のため
*/
@GetMapping("/")
public String redirectToTasks() {
return "redirect:/tasks"; // /tasks へリダイレクト
}
/**
* タスク一覧表示
* 目的: 全てのタスクを一覧表示し、ユーザーがタスクの概要を把握できるようにする
* @param model: ビューにデータを渡すためのオブジェクト(Spring MVCの仕組み)
*/
@GetMapping("/tasks")
public String listTasks(Model model) {
// サービス層からタスクデータを取得
// 目的: データベースから最新のタスク情報を取得
List<Task> tasks = taskService.getAllTasks();
// 統計情報を取得
// 目的: ダッシュボード機能として進捗状況を表示
TaskService.TaskStatistics statistics = taskService.getTaskStatistics();
// ビューにデータを渡す
// 理由: Thymeleafテンプレートでデータを表示するため
model.addAttribute("tasks", tasks);
model.addAttribute("statistics", statistics);
// テンプレート名を返す(src/main/resources/templates/task-list.html)
return "task-list";
}
/**
* 新規タスク作成フォーム表示
* 目的: ユーザーが新しいタスクを入力するためのフォーム画面を表示
* 理由: データ入力のためのユーザーインターフェースを提供
*/
@GetMapping("/tasks/new")
public String showCreateForm(Model model) {
// 空のTaskオブジェクトをフォームにバインド
// 目的: Thymeleafのフォームバインディング機能を使用するため
// 理由: フォームの各フィールドをオブジェクトのプロパティと対応付ける
model.addAttribute("task", new Task());
// 優先度の選択肢を提供
// 目的: ドロップダウンリストで優先度を選択可能にする
model.addAttribute("priorities", Task.Priority.values());
return "task-form"; // 新規作成・編集共通のフォームテンプレート
}
/**
* タスク編集フォーム表示
* 目的: 既存のタスクを編集するためのフォーム画面を表示
* @param id: 編集対象のタスクID(URLパスから取得)
*/
@GetMapping("/tasks/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) {
// 指定されたIDのタスクを取得
// 目的: 編集対象のタスクデータをフォームに表示するため
Optional<Task> taskOptional = taskService.getTaskById(id);
if (taskOptional.isPresent()) {
// タスクが存在する場合
model.addAttribute("task", taskOptional.get());
model.addAttribute("priorities", Task.Priority.values());
return "task-form";
} else {
// タスクが存在しない場合
// 目的: 不正なIDでのアクセス時のエラーハンドリング
return "redirect:/tasks?error=notfound";
}
}
// ===== フォーム処理メソッド =====
/**
* タスク保存処理(新規作成・更新共通)
* 目的: フォームから送信されたタスクデータを検証し、データベースに保存
* @Valid: Bean Validationによる入力値検証を実行
* BindingResult: 検証結果を格納(エラー情報など)
*/
@PostMapping("/tasks")
public String saveTask(@Valid @ModelAttribute Task task,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
// バリデーションエラーがある場合
// 目的: 不正なデータの保存を防ぎ、ユーザーにエラーを通知
if (bindingResult.hasErrors()) {
// エラーがある場合はフォームを再表示
model.addAttribute("priorities", Task.Priority.values());
return "task-form"; // エラーメッセージ付きでフォームを再表示
}
try {
// タスクを保存
// 目的: 検証済みのデータをデータベースに永続化
Task savedTask = taskService.saveTask(task);
// 成功メッセージを設定
// 目的: ユーザーに操作が成功したことを通知
// RedirectAttributes使用理由: リダイレクト後にメッセージを表示するため
String message = (task.getId() == null) ?
"タスクが正常に作成されました。" :
"タスクが正常に更新されました。";
redirectAttributes.addFlashAttribute("successMessage", message);
// PRG(Post-Redirect-Get)パターン
// 目的: ブラウザの再読み込みによる重複送信を防ぐ
// 理由: フォーム送信後は必ずリダイレクトして、重複処理を回避
return "redirect:/tasks";
} catch (Exception e) {
// エラーハンドリング
// 目的: 予期しないエラーが発生した場合の適切な処理
model.addAttribute("errorMessage", "タスクの保存中にエラーが発生しました: " + e.getMessage());
model.addAttribute("priorities", Task.Priority.values());
return "task-form";
}
}
/**
* タスク削除処理
* 目的: 指定されたタスクをデータベースから削除
* @PathVariable: URLパスから削除対象のIDを取得
*/
@PostMapping("/tasks/{id}/delete")
public String deleteTask(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {
// タスクを削除
// 目的: 不要になったタスクをシステムから除去
taskService.deleteTask(id);
// 成功メッセージ
redirectAttributes.addFlashAttribute("successMessage", "タスクが正常に削除されました。");
} catch (Exception e) {
// エラーメッセージ
// 目的: 削除に失敗した場合のユーザー通知
redirectAttributes.addFlashAttribute("errorMessage", "タスクの削除中にエラーが発生しました。");
}
return "redirect:/tasks";
}
// ===== Ajax API エンドポイント =====
/**
* タスク完了状態切り替え(Ajax用)
* 目的: ページ全体を再読み込みせずに、タスクの完了状態を切り替える
* 理由: ユーザビリティ向上のため、軽快な操作感を提供
* @ResponseBody: JSON形式でレスポンスを返す(画面遷移なし)
*/
@PostMapping("/tasks/{id}/toggle")
@ResponseBody // JSON形式でレスポンスを返す
public ResponseEntity<?> toggleTaskCompletion(@PathVariable Long id) {
try {
// タスクの完了状態を切り替え
// 目的: ワンクリックでタスクの状態を変更
Task updatedTask = taskService.toggleTaskCompletion(id);
// JSON形式でレスポンスを返す
// 目的: JavaScriptから処理結果を受け取れるようにする
return ResponseEntity.ok().body(new TaskToggleResponse(
true,
"タスクの状態が更新されました。",
updatedTask.getId(),
updatedTask.getCompleted()
));
} catch (Exception e) {
// エラーレスポンス
// 目的: Ajax処理でのエラーを適切にハンドリング
return ResponseEntity.badRequest().body(new TaskToggleResponse(
false,
"エラーが発生しました: " + e.getMessage(),
id,
null
));
}
}
// ===== 検索・フィルタリング機能 =====
/**
* タスク検索処理
* 目的: ユーザーが入力したキーワードでタスクを検索
* 理由: タスクが多くなった際の効率的な検索機能を提供
* @RequestParam: クエリパラメータから検索条件を取得
*/
@GetMapping("/tasks/search")
public String searchTasks(@RequestParam(required = false) String keyword,
@RequestParam(required = false) String priority,
@RequestParam(required = false) Boolean completed,
Model model) {
List<Task> tasks;
// 検索条件に応じてタスクを取得
// 目的: 複数の検索条件を組み合わせた柔軟な検索機能
if (keyword != null && !keyword.trim().isEmpty()) {
// キーワード検索
tasks = taskService.searchTasks(keyword);
} else if (priority != null && !priority.isEmpty()) {
// 優先度検索
Task.Priority priorityEnum = Task.Priority.valueOf(priority.toUpperCase());
if (completed != null) {
// 優先度 + 完了状態での検索
tasks = taskService.getTasksByPriorityAndCompleted(priorityEnum, completed);
} else {
// 優先度のみでの検索
tasks = taskService.getTasksByPriority(priorityEnum);
}
} else if (completed != null) {
// 完了状態のみでの検索
tasks = taskService.getTasksByCompleted(completed);
} else {
// 条件なしの場合は全件表示
tasks = taskService.getAllTasks();
}
// 検索結果と統計情報をビューに渡す
model.addAttribute("tasks", tasks);
model.addAttribute("statistics", taskService.getTaskStatistics());
model.addAttribute("searchKeyword", keyword); // 検索キーワードを保持
return "task-list";
}
// ===== RESTful API エンドポイント =====
/**
* REST API: 全タスク取得
* 目的: 外部システムやJavaScriptからJSON形式でタスクデータを取得
* 理由: SPA(Single Page Application)や他システムとの連携のため
*/
@GetMapping("/api/tasks")
@ResponseBody
public List<Task> getTasksApi() {
return taskService.getAllTasks();
}
/**
* REST API: 特定タスク取得
* 目的: 指定されたIDのタスクをJSON形式で取得
*/
@GetMapping("/api/tasks/{id}")
@ResponseBody
public ResponseEntity<Task> getTaskApi(@PathVariable Long id) {
Optional<Task> task = taskService.getTaskById(id);
return task.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* REST API: タスク作成
* 目的: JSON形式でタスクを作成するAPIエンドポイント
* @RequestBody: HTTPリクエストボディのJSONをTaskオブジェクトに変換
*/
@PostMapping("/api/tasks")
@ResponseBody
public ResponseEntity<Task> createTaskApi(@Valid @RequestBody Task task, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().build();
}
try {
Task savedTask = taskService.saveTask(task);
return ResponseEntity.ok(savedTask);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
// ===== エラーハンドリング =====
/**
* 例外ハンドラー
* 目的: コントローラー内で発生した例外を統一的に処理
* 理由: エラー画面の表示を統一し、ユーザーに分かりやすいメッセージを提供
*/
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
// ログ出力(実際のアプリケーションではLoggerを使用)
System.err.println("エラーが発生しました: " + e.getMessage());
e.printStackTrace();
// エラー情報をビューに渡す
model.addAttribute("errorMessage", "予期しないエラーが発生しました。");
model.addAttribute("errorDetails", e.getMessage());
return "error"; // エラーページテンプレート
}
// ===== レスポンス用内部クラス =====
/**
* Ajax通信用のレスポンスクラス
* 目的: タスク切り替え処理の結果をJSON形式で返すためのデータ構造
* 理由: JavaScript側で処理結果を適切に判定し、UIを更新するため
*/
public static class TaskToggleResponse {
private boolean success; // 処理の成功/失敗
private String message; // メッセージ
private Long taskId; // 対象タスクのID
private Boolean completed; // 更新後の完了状態
// コンストラクタ
public TaskToggleResponse(boolean success, String message, Long taskId, Boolean completed) {
this.success = success;
this.message = message;
this.taskId = taskId;
this.completed = completed;
}
// Getter methods(JSONシリアライゼーション用)
// 目的: オブジェクトをJSON形式に変換する際に使用
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public Long getTaskId() { return taskId; }
public Boolean getCompleted() { return completed; }
}
}
7.2 コントローラー層の解説
Spring MVCアノテーションの目的と使い方
@Controller vs @RestController
// @Controller: 従来のMVCコントローラー(ビューを返す)
// 目的: HTMLページを生成してブラウザに返す
@Controller
public String method() {
return "template-name"; // テンプレート名を返す
}
// @RestController: RESTfulAPIコントローラー(データを返す)
// 目的: JSON/XMLデータを直接返す
@RestController
public Object method() {
return data; // データオブジェクトを直接返す
}
リクエストマッピングの目的
// 目的: HTTPメソッドとURLパスを適切なメソッドにマッピング
@GetMapping("/tasks") // GET リクエスト - データ取得用
@PostMapping("/tasks") // POST リクエスト - データ作成・更新用
@PutMapping("/tasks/{id}") // PUT リクエスト - データ更新用
@DeleteMapping("/tasks/{id}") // DELETE リクエスト - データ削除用
パラメータ取得方法とその用途
@PathVariable の使用目的
// 目的: URLパスから動的な値を取得(RESTful URLの実現)
@GetMapping("/tasks/{id}")
public String method(@PathVariable Long id) {
// /tasks/123 → id = 123
// 理由: リソースの一意識別のため
}
@RequestParam の使用目的
// 目的: クエリパラメータから検索条件等を取得
@GetMapping("/tasks/search")
public String search(@RequestParam String keyword) {
// /tasks/search?keyword=会議 → keyword = "会議"
// 理由: 検索条件やフィルタリングのため
}
@ModelAttribute の使用目的
// 目的: フォームデータをオブジェクトにバインド
@PostMapping("/tasks")
public String save(@ModelAttribute Task task) {
// HTMLフォームの各フィールドがTaskオブジェクトのプロパティに自動設定
// 理由: フォーム処理の簡素化とタイプセーフティの確保
}
バリデーション処理の重要性
@Valid + BindingResult の目的
@PostMapping("/tasks")
public String save(@Valid @ModelAttribute Task task, BindingResult result) {
if (result.hasErrors()) {
// バリデーションエラーがある場合の処理
// 目的: 不正なデータの保存を防ぎ、ユーザーにエラーを通知
return "task-form"; // エラーメッセージ付きでフォーム再表示
}
// 正常処理
}
PRG(Post-Redirect-Get)パターンの必要性
PRGパターンの目的と理由
@PostMapping("/tasks")
public String save(Task task, RedirectAttributes redirectAttributes) {
taskService.saveTask(task);
// 成功メッセージを設定
redirectAttributes.addFlashAttribute("successMessage", "保存しました");
// リダイレクト(PRGパターン)
// 目的: ブラウザの再読み込みによる重複送信を防ぐ
// 理由: フォーム送信後のF5キー押下で重複処理が発生するのを回避
return "redirect:/tasks";
}
Ajax処理の実装目的
@ResponseBody の使用理由
@PostMapping("/tasks/{id}/toggle")
@ResponseBody // JSON形式でレスポンスを返す
public ResponseEntity<?> toggleTask(@PathVariable Long id) {
// 目的: ページ全体を再読み込みせずに部分的な更新を実現
// 理由: ユーザビリティ向上(軽快な操作感)
Task task = taskService.toggleTaskCompletion(id);
return ResponseEntity.ok(new TaskToggleResponse(true, "更新しました", task.getId(), task.getCompleted()));
}
エラーハンドリングの重要性
@ExceptionHandler(Exception.class)
public String handleException(Exception e, Model model) {
// 目的: 予期しない例外を統一的に処理
// 理由: ユーザーに適切なエラーメッセージを表示し、システムの安定性を確保
model.addAttribute("errorMessage", "エラーが発生しました");
return "error";
}
RESTful API設計の考慮点
REST API エンドポイントの設計原則
// 目的: 外部システムとの連携やSPA開発のためのAPI提供
GET /api/tasks // 全タスク取得
GET /api/tasks/{id} // 特定タスク取得
POST /api/tasks // タスク作成
PUT /api/tasks/{id} // タスク更新
DELETE /api/tasks/{id} // タスク削除
// 理由: RESTful設計により、直感的で保守しやすいAPIを提供
📝 次のステップ
コントローラー層の作成が完了しました。次は以下のファイルでビュー層の作成を行います:
-
開発手順書_ビュー.md
- Thymeleafテンプレートの作成(画面表示) -
開発手順書_実行とテスト.md
- アプリケーションの実行とテスト(動作確認)
コントローラー層により、Webアプリケーションとしての基本的な機能が実装されました。ユーザーのリクエストを適切に処理し、サービス層と連携してレスポンスを返す仕組みが完成しています。
📚 Spring Boot タスク管理アプリケーション 開発手順書(ビュー層)
🎯 目的
ビュー層は、ユーザーが実際に操作する画面(HTML)を作成する部分です。Thymeleafテンプレートエンジンを使用して、動的なWebページを生成し、美しく使いやすいユーザーインターフェースを提供します。
なぜビュー層が重要なのか:
- ユーザーが直接触れる部分のため、使いやすさが重要
- データの表示方法により、アプリケーションの価値が決まる
- レスポンシブデザインにより、様々なデバイスで利用可能
- 適切なバリデーション表示により、ユーザーの入力ミスを防ぐ
8. ビュー層の作成
🎯 目的
Thymeleafテンプレートエンジンを使用して、動的なHTMLページを作成します。テンプレートにより、サーバーサイドのデータを画面に表示し、ユーザーとの対話を可能にします。
なぜThymeleafを使用するのか:
- サーバーサイドレンダリングでSEOに有利
- Spring Bootとの統合が優秀で設定が簡単
- HTMLの構造を保ちながら動的コンテンツを生成可能
- デザイナーとの協業がしやすい(通常のHTMLとして表示可能)
8.1 共通レイアウトの作成
目的: 全ページで共通するヘッダー、ナビゲーション、フッターを定義し、コードの重複を避ける
理由:
- メンテナンス性向上:共通部分の修正が一箇所で済む
- 一貫したデザインの実現:全ページで統一されたUI/UX
- 開発効率向上:新しいページ作成時に共通部分を再実装する必要がない
- ブランディング統一:ロゴ、色彩、フォントなどの統一
src/main/resources/templates/layout.html
を作成:
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 目的: レスポンシブデザインのため、モバイルデバイスでの表示を最適化 -->
<!-- 理由: スマートフォンやタブレットでも使いやすいアプリケーションにするため -->
<title th:text="${title} + ' - タスク管理アプリ'">タスク管理アプリ</title>
<!-- Bootstrap CSS - 目的: 美しいUIと迅速な開発のため -->
<!-- 理由: 一からCSSを書くより、実績のあるフレームワークを使用することで開発効率向上 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome - 目的: アイコンによる視覚的な分かりやすさの向上 -->
<!-- 理由: テキストだけより、アイコンがあることで直感的な操作が可能 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- カスタムCSS - 目的: アプリケーション独自のスタイル定義 -->
<style>
/* 完了済みタスクのスタイル - 目的: 視覚的に完了状態を表現 */
/* 理由: ユーザーが一目で完了したタスクを識別できるようにする */
.task-completed {
text-decoration: line-through;
opacity: 0.6;
background-color: #f8f9fa;
}
/* 優先度別の色分け - 目的: 優先度を一目で判別可能にする */
/* 理由: 色による視覚的な区別で、重要なタスクを見逃さないようにする */
.priority-high { border-left: 4px solid #dc3545; } /* 赤 - 高優先度 */
.priority-medium { border-left: 4px solid #ffc107; } /* 黄 - 中優先度 */
.priority-low { border-left: 4px solid #28a745; } /* 緑 - 低優先度 */
/* ナビゲーションバーのスタイル */
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}
/* 統計カードのスタイル - 目的: 情報の見やすさ向上 */
/* 理由: 重要な統計情報を目立たせ、ユーザーの理解を促進 */
.stats-card {
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* フォームのスタイル改善 */
.form-container {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 2rem;
}
</style>
</head>
<body>
<!-- ナビゲーションバー - 目的: サイト内の移動を容易にする -->
<!-- 理由: ユーザーがどのページからでも主要機能にアクセスできるようにする -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<!-- ブランドロゴ - 目的: アプリケーションの識別とホームへのリンク -->
<a class="navbar-brand" href="/tasks">
<i class="fas fa-tasks me-2"></i>タスク管理
</a>
<!-- モバイル用ハンバーガーメニュー - 目的: 小画面での操作性向上 -->
<!-- 理由: スマートフォンでは画面が狭いため、メニューを折りたたんで表示 -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<!-- ナビゲーションメニュー -->
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/tasks">
<i class="fas fa-list me-1"></i>タスク一覧
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tasks/new">
<i class="fas fa-plus me-1"></i>新規作成
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- メインコンテンツエリア -->
<div class="container-fluid">
<!-- フラッシュメッセージ表示エリア - 目的: ユーザーへの操作結果通知 -->
<!-- 理由: ユーザーが操作が成功したか失敗したかを即座に把握できるようにする -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show mt-3" role="alert">
<i class="fas fa-check-circle me-2"></i>
<span th:text="${successMessage}">成功メッセージ</span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<span th:text="${errorMessage}">エラーメッセージ</span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<!-- ページコンテンツ挿入部分 - 目的: 各ページ固有の内容を表示 -->
<!-- 理由: 共通レイアウトの中に個別ページのコンテンツを動的に挿入 -->
<div th:replace="${content}">
<!-- 個別ページのコンテンツがここに挿入される -->
</div>
</div>
<!-- フッター - 目的: アプリケーション情報の表示 -->
<!-- 理由: 著作権情報や技術情報を表示し、アプリケーションの信頼性を向上 -->
<footer class="bg-light text-center py-3 mt-5">
<div class="container">
<p class="text-muted mb-0">
<i class="fas fa-copyright me-1"></i>
2024 タスク管理アプリケーション - Spring Boot + Thymeleaf
</p>
</div>
</footer>
<!-- JavaScript ライブラリ - 目的: 動的な機能とインタラクティブな操作の実現 -->
<!-- Bootstrap JS - 目的: ドロップダウン、モーダル等のUI機能 -->
<!-- 理由: 静的なHTMLだけでは実現できない動的な操作を可能にする -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery - 目的: DOM操作とAjax通信の簡素化 -->
<!-- 理由: 生のJavaScriptより簡潔にDOM操作とサーバー通信ができる -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- カスタムJavaScript - 目的: アプリケーション固有の動的機能 -->
<script>
// タスク完了状態切り替え機能 - 目的: ページ再読み込みなしでの状態変更
// 理由: ユーザビリティ向上のため、軽快な操作感を提供
function toggleTask(taskId) {
$.ajax({
url: '/tasks/' + taskId + '/toggle',
type: 'POST',
success: function(response) {
if (response.success) {
// 成功時: ページを再読み込みして最新状態を表示
// 理由: 確実な状態反映とシンプルな実装のため
window.location.reload();
} else {
alert('エラー: ' + response.message);
}
},
error: function() {
alert('通信エラーが発生しました。');
}
});
}
// 削除確認ダイアログ - 目的: 誤操作による削除を防ぐ
// 理由: 削除は取り消しできない操作のため、確認が必要
function confirmDelete(taskTitle) {
return confirm('タスク「' + taskTitle + '」を削除してもよろしいですか?');
}
// 検索フォームの自動送信 - 目的: ユーザビリティ向上
// 理由: いちいち検索ボタンを押さなくても、入力と同時に検索結果が表示される
$(document).ready(function() {
// 検索キーワード入力時の自動検索(デバウンス処理)
let searchTimeout;
$('#searchInput').on('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
$('#searchForm').submit();
}, 500); // 500ms後に検索実行(連続入力時の無駄な通信を防ぐ)
});
});
</script>
</body>
</html>
8.2 タスク一覧ページの作成
目的: 全てのタスクを一覧表示し、検索・フィルタリング・操作機能を提供
理由:
- ユーザーがタスクの全体像を把握できる
- 効率的なタスク管理が可能
- 検索・フィルタ機能により、必要なタスクを素早く見つけられる
- 統計情報により進捗状況を一目で把握
src/main/resources/templates/task-list.html
を作成:
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
th:replace="~{layout :: layout(title='タスク一覧', content=~{::content})}">
<div th:fragment="content">
<!-- ページヘッダー - 目的: ページの目的と現在位置を明確化 -->
<!-- 理由: ユーザーが現在どのページにいるかを明確にし、主要なアクションへの導線を提供 -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">
<i class="fas fa-tasks text-primary me-2"></i>
タスク一覧
</h1>
<!-- 新規作成ボタン - 目的: 主要なアクションへの分かりやすいアクセス -->
<!-- 理由: タスク管理において最も重要な「新規作成」機能を目立つ場所に配置 -->
<a href="/tasks/new" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>新規タスク作成
</a>
</div>
</div>
</div>
<!-- 統計情報表示 - 目的: タスクの進捗状況を一目で把握 -->
<!-- 理由: 数値による進捗の可視化で、ユーザーのモチベーション向上と効率的な管理を支援 -->
<div class="row mb-4" th:if="${statistics}">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card bg-primary text-white">
<div class="card-body text-center">
<i class="fas fa-list-ul fa-2x mb-2"></i>
<h4 th:text="${statistics.totalTasks}">0</h4>
<p class="mb-0">総タスク数</p>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card bg-success text-white">
<div class="card-body text-center">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<h4 th:text="${statistics.completedTasks}">0</h4>
<p class="mb-0">完了済み</p>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card bg-warning text-white">
<div class="card-body text-center">
<i class="fas fa-clock fa-2x mb-2"></i>
<h4 th:text="${statistics.incompleteTasks}">0</h4>
<p class="mb-0">未完了</p>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card bg-danger text-white">
<div class="card-body text-center">
<i class="fas fa-exclamation fa-2x mb-2"></i>
<h4 th:text="${statistics.highPriorityTasks}">0</h4>
<p class="mb-0">高優先度</p>
</div>
</div>
</div>
</div>
<!-- 検索・フィルタリング機能 - 目的: 大量のタスクから必要なものを効率的に検索 -->
<!-- 理由: タスクが増えると一覧表示だけでは管理が困難になるため、絞り込み機能が必要 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="fas fa-search me-2"></i>検索・フィルタ
</div>
<div class="card-body">
<form id="searchForm" action="/tasks/search" method="get" class="row g-3">
<!-- キーワード検索 - 目的: 部分一致でタスクを検索 -->
<!-- 理由: タスクのタイトルや内容の一部を覚えている場合の検索手段 -->
<div class="col-md-4">
<label for="searchInput" class="form-label">キーワード検索</label>
<input type="text" class="form-control" id="searchInput" name="keyword"
th:value="${searchKeyword}" placeholder="タイトルまたは説明で検索...">
</div>
<!-- 優先度フィルタ - 目的: 特定の優先度のタスクのみ表示 -->
<!-- 理由: 高優先度のタスクに集中したい場合などの用途 -->
<div class="col-md-3">
<label for="priorityFilter" class="form-label">優先度</label>
<select class="form-select" id="priorityFilter" name="priority">
<option value="">すべて</option>
<option value="HIGH">高</option>
<option value="MEDIUM">中</option>
<option value="LOW">低</option>
</select>
</div>
<!-- 完了状態フィルタ - 目的: 完了済み/未完了タスクの絞り込み -->
<!-- 理由: 進行中のタスクのみ表示したい場合や、完了したタスクを確認したい場合 -->
<div class="col-md-3">
<label for="statusFilter" class="form-label">状態</label>
<select class="form-select" id="statusFilter" name="completed">
<option value="">すべて</option>
<option value="false">未完了</option>
<option value="true">完了済み</option>
</select>
</div>
<!-- 検索ボタン - 目的: 検索条件の実行 -->
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="fas fa-search me-1"></i>検索
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- タスク一覧テーブル - 目的: タスクデータの構造化された表示 -->
<!-- 理由: 複数のタスク情報を整理して表示し、一目で比較・確認できるようにする -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<i class="fas fa-list me-2"></i>
タスク一覧
<!-- 件数表示 - 目的: 現在表示されているタスク数の明示 -->
<span class="badge bg-secondary" th:text="${#lists.size(tasks)}">0</span>
</span>
</div>
<div class="card-body p-0">
<!-- タスクが存在しない場合の表示 - 目的: 空の状態を分かりやすく表示 -->
<!-- 理由: 何もない画面だとユーザーが混乱するため、適切なガイダンスを提供 -->
<div th:if="${#lists.isEmpty(tasks)}" class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">タスクがありません</h5>
<p class="text-muted">新しいタスクを作成してください。</p>
<a href="/tasks/new" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>最初のタスクを作成
</a>
</div>
<!-- タスクテーブル - 目的: タスクデータの表形式での表示 -->
<!-- 理由: 構造化されたデータを効率的に表示し、比較しやすくする -->
<div th:if="${!#lists.isEmpty(tasks)}" class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 50px;">完了</th>
<th>タイトル</th>
<th style="width: 120px;">優先度</th>
<th style="width: 150px;">作成日</th>
<th style="width: 150px;">更新日</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 各タスクの表示 - 目的: 個別タスクの詳細情報と操作の提供 -->
<!-- 理由: 各タスクの状態を視覚的に表現し、直接操作できるようにする -->
<tr th:each="task : ${tasks}"
th:class="${task.completed ? 'task-completed' : ''} + ' ' +
${task.priority.name() == 'HIGH' ? 'priority-high' :
(task.priority.name() == 'MEDIUM' ? 'priority-medium' : 'priority-low')}">
<!-- 完了チェックボックス - 目的: ワンクリックでの完了状態切り替え -->
<!-- 理由: 最も頻繁に使用される操作を最も簡単にアクセスできるようにする -->
<td class="text-center">
<input type="checkbox" class="form-check-input"
th:checked="${task.completed}"
th:onclick="'toggleTask(' + ${task.id} + ')'"
title="クリックで完了状態を切り替え">
</td>
<!-- タスクタイトルと説明 - 目的: タスクの主要情報の表示 -->
<td>
<div>
<strong th:text="${task.title}">タスクタイトル</strong>
<div th:if="${task.description}"
class="text-muted small mt-1"
th:text="${task.description}">説明</div>
</div>
</td>
<!-- 優先度表示 - 目的: 視覚的な優先度の表現 -->
<!-- 理由: 色分けにより優先度を瞬時に判別できるようにする -->
<td>
<span class="badge"
th:classappend="${task.priority.name() == 'HIGH' ? 'bg-danger' :
(task.priority.name() == 'MEDIUM' ? 'bg-warning text-dark' : 'bg-success')}"
th:text="${task.priority.displayName}">優先度</span>
</td>
<!-- 作成日時 - 目的: タスクの作成時期の確認 -->
<td>
<small th:text="${#temporals.format(task.createdAt, 'yyyy/MM/dd HH:mm')}">
2024/01/01 12:00
</small>
</td>
<!-- 更新日時 - 目的: 最終更新時期の確認 -->
<td>
<small th:text="${#temporals.format(task.updatedAt, 'yyyy/MM/dd HH:mm')}">
2024/01/01 12:00
</small>
</td>
<!-- 操作ボタン - 目的: タスクの編集・削除操作 -->
<!-- 理由: 各タスクに対する操作を直接実行できるようにする -->
<td>
<div class="btn-group btn-group-sm" role="group">
<!-- 編集ボタン - 目的: タスク情報の修正 -->
<a th:href="@{/tasks/{id}/edit(id=${task.id})}"
class="btn btn-outline-primary" title="編集">
<i class="fas fa-edit"></i>
</a>
<!-- 削除ボタン - 目的: 不要なタスクの削除 -->
<!-- 理由: タスク管理において、完了後や不要になったタスクを削除する機能が必要 -->
<form th:action="@{/tasks/{id}/delete(id=${task.id})}"
method="post" class="d-inline">
<button type="submit" class="btn btn-outline-danger"
title="削除"
th:onclick="'return confirmDelete(\'' + ${task.title} + '\')'">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</html>
8.3 タスクフォームページの作成
目的: タスクの新規作成と編集を行うフォーム画面を提供
理由:
- ユーザーがタスクデータを入力・編集するためのインターフェースが必要
- バリデーション機能により、データの整合性を保つ
- ユーザーフレンドリーなフォーム設計により、入力ミスを防ぐ
- 新規作成と編集を同一フォームで処理することで、保守性を向上
src/main/resources/templates/task-form.html
を作成:
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
th:replace="~{layout :: layout(title=${task.id != null ? 'タスク編集' : 'タスク作成'}, content=~{::content})}">
<div th:fragment="content">
<!-- ページヘッダー - 目的: 現在の操作(新規作成 or 編集)を明確化 -->
<!-- 理由: ユーザーが現在何をしているかを明確にし、適切なボタンを表示 -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">
<i th:class="${task.id != null ? 'fas fa-edit' : 'fas fa-plus'} + ' text-primary me-2'"></i>
<span th:text="${task.id != null ? 'タスク編集' : '新規タスク作成'}">タスク作成</span>
</h1>
<!-- 戻るボタン - 目的: ユーザーが迷わず前の画面に戻れるようにする -->
<!-- 理由: フォーム入力を中断したい場合の明確な退避ルートを提供 -->
<a href="/tasks" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>一覧に戻る
</a>
</div>
</div>
</div>
<!-- タスクフォーム - 目的: タスクデータの入力・編集インターフェース -->
<!-- 理由: 構造化されたフォームにより、必要な情報を確実に収集 -->
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="form-container">
<!-- フォーム - 目的: タスクデータの入力と送信 -->
<!-- novalidate属性の理由: サーバーサイドバリデーションを優先し、一貫したエラー処理を行う -->
<form th:action="@{/tasks}" th:object="${task}" method="post" novalidate>
<!-- 隠しフィールド(編集時のID保持) - 目的: 編集対象の特定 -->
<!-- 理由: 編集時にどのタスクを更新するかをサーバーに伝える必要がある -->
<input type="hidden" th:field="*{id}">
<!-- タイトル入力 - 目的: タスクの主要な識別情報の入力 -->
<!-- 理由: タスクの最も重要な情報であり、必須項目として設定 -->
<div class="mb-3">
<label for="title" class="form-label">
<i class="fas fa-heading me-1"></i>
タイトル <span class="text-danger">*</span>
</label>
<input type="text"
class="form-control"
th:field="*{title}"
th:classappend="${#fields.hasErrors('title')} ? 'is-invalid' : ''"
id="title"
placeholder="タスクのタイトルを入力してください..."
maxlength="100"
required>
<!-- バリデーションエラー表示 - 目的: ユーザーに入力エラーを通知 -->
<!-- 理由: 何が間違っているかを明確に伝え、修正を促す -->
<div class="invalid-feedback" th:if="${#fields.hasErrors('title')}">
<span th:errors="*{title}">タイトルエラー</span>
</div>
<!-- 文字数カウンター - 目的: 入力制限の可視化 -->
<!-- 理由: ユーザーが制限を意識して入力できるようにし、エラーを防ぐ -->
<div class="form-text">
<span id="titleCounter">0</span>/100文字
</div>
</div>
<!-- 説明入力 - 目的: タスクの詳細情報の入力(任意項目) -->
<!-- 理由: タスクの詳細な内容を記録し、後から参照できるようにする -->
<div class="mb-3">
<label for="description" class="form-label">
<i class="fas fa-align-left me-1"></i>
説明
</label>
<textarea class="form-control"
th:field="*{description}"
id="description"
rows="4"
placeholder="タスクの詳細説明を入力してください...(任意)"
maxlength="500"></textarea>
<!-- 文字数カウンター -->
<div class="form-text">
<span id="descriptionCounter">0</span>/500文字
</div>
</div>
<!-- 優先度選択 - 目的: タスクの重要度設定 -->
<!-- 理由: タスクの優先順位付けにより、効率的な作業計画を立てられる -->
<div class="mb-3">
<label for="priority" class="form-label">
<i class="fas fa-flag me-1"></i>
優先度
</label>
<select class="form-select" th:field="*{priority}" id="priority">
<option th:each="p : ${priorities}"
th:value="${p}"
th:text="${p.displayName}"
th:selected="${task.priority == p}">優先度</option>
</select>
<!-- 優先度の説明 - 目的: ユーザーが適切な選択をできるようにガイド -->
<!-- 理由: 優先度の基準を明確にし、一貫した判断ができるようにする -->
<div class="form-text">
<small>
<span class="badge bg-danger me-1">高</span>緊急・重要なタスク
<span class="badge bg-warning text-dark me-1">中</span>通常のタスク
<span class="badge bg-success me-1">低</span>余裕があるときに
</small>
</div>
</div>
<!-- 完了状態(編集時のみ表示) - 目的: 編集時に完了状態を変更可能にする -->
<!-- 理由: 編集画面でタスクの完了状態も一緒に変更できると便利 -->
<div class="mb-3" th:if="${task.id != null}">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
th:field="*{completed}"
id="completed">
<label class="form-check-label" for="completed">
<i class="fas fa-check-circle me-1"></i>
完了済み
</label>
</div>
</div>
<!-- 送信ボタン群 - 目的: フォームの送信とキャンセル操作 -->
<!-- 理由: ユーザーが操作を完了または中断する明確な手段を提供 -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<!-- キャンセルボタン - 目的: 入力を破棄して前の画面に戻る -->
<a href="/tasks" class="btn btn-outline-secondary me-md-2">
<i class="fas fa-times me-1"></i>キャンセル
</a>
<!-- 送信ボタン - 目的: 入力内容をサーバーに送信 -->
<button type="submit" class="btn btn-primary">
<i th:class="${task.id != null ? 'fas fa-save' : 'fas fa-plus'} + ' me-1'"></i>
<span th:text="${task.id != null ? '更新' : '作成'}">作成</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- フォーム用JavaScript - 目的: ユーザビリティ向上のためのクライアントサイド機能 -->
<!-- 理由: サーバーサイド処理だけでは実現できないリアルタイムな機能を提供 -->
<script>
$(document).ready(function() {
// 文字数カウンター機能 - 目的: リアルタイムでの入力制限の可視化
// 理由: ユーザーが制限に達する前に警告を表示し、エラーを防ぐ
function updateCounter(inputId, counterId, maxLength) {
const input = document.getElementById(inputId);
const counter = document.getElementById(counterId);
function updateCount() {
const currentLength = input.value.length;
counter.textContent = currentLength;
// 制限に近づいたら色を変更 - 目的: 視覚的な警告
// 理由: 制限を超える前にユーザーに注意を促す
if (currentLength > maxLength * 0.8) {
counter.className = 'text-warning';
} else if (currentLength >= maxLength) {
counter.className = 'text-danger';
} else {
counter.className = '';
}
}
// 初期表示とイベントリスナー設定
updateCount();
input.addEventListener('input', updateCount);
}
// 各フィールドのカウンター初期化
updateCounter('title', 'titleCounter', 100);
updateCounter('description', 'descriptionCounter', 500);
// フォーム送信時の確認 - 目的: 誤操作防止とクライアントサイドバリデーション
// 理由: サーバーに送信する前に基本的な検証を行い、無駄な通信を防ぐ
$('form').on('submit', function(e) {
const title = $('#title').val().trim();
if (!title) {
e.preventDefault();
alert('タイトルは必須です。');
$('#title').focus();
return false;
}
});
// タイトルフィールドにフォーカス - 目的: ユーザビリティ向上
// 理由: ページ読み込み後、すぐに入力を開始できるようにする
$('#title').focus();
});
</script>
</div>
</html>
📝 ビュー層完成のまとめ
ビュー層の基本的なテンプレートが完成しました。これらのテンプレートにより以下が実現されます:
🎯 実現される機能と目的
-
統一されたデザイン:
- 目的: 共通レイアウトによる一貫したUI/UX
- 理由: ユーザーが迷わず操作でき、学習コストを削減
-
レスポンシブ対応:
- 目的: Bootstrap活用によるモバイル対応
- 理由: スマートフォンやタブレットでも快適に使用可能
-
直感的な操作:
- 目的: アイコンと色分けによる視覚的な分かりやすさ
- 理由: 文字だけでなく視覚的な手がかりでユーザビリティ向上
-
リアルタイム機能:
- 目的: Ajax通信による軽快な操作感
- 理由: ページ全体の再読み込みなしで状態変更が可能
-
バリデーション表示:
- 目的: ユーザーフレンドリーなエラー表示
- 理由: 入力ミスを即座に通知し、修正を促す
🔧 技術的な実装のポイント
- Thymeleaf: サーバーサイドレンダリングでSEO対応
- Bootstrap: 迅速な開発と美しいUI
- jQuery: DOM操作とAjax通信の簡素化
- Font Awesome: 豊富なアイコンライブラリ
- レスポンシブデザイン: 様々な画面サイズに対応
次のステップでは、アプリケーションの実行とテスト方法について詳しく説明します。
🚀 アプリケーションの実行とテスト手順
🎯 目的
開発したSpring Bootタスク管理アプリケーションを実際に動作させ、機能をテストします。この段階では、アプリケーションが正常に動作するか確認し、問題があれば修正します。
なぜ実行とテストが重要なのか:
- 開発したコードが期待通りに動作するかを確認
- ユーザーの視点からアプリケーションの使いやすさを評価
- バグや不具合を早期に発見し、修正する
- パフォーマンスや安定性を確認し、本番環境での運用に備える
9. アプリケーションの実行とテスト
🎯 目的
開発したアプリケーションを起動し、Webブラウザからアクセスできる状態にします。
なぜこの手順が必要なのか:
- 開発したコードが実際に動作するかを確認するため
- ユーザーインターフェースが正しく表示されるかをチェックするため
- データベース接続やビジネスロジックが正常に機能するかを検証するため
9.1 コマンドラインからの実行
目的: Mavenを使用してアプリケーションを起動
理由:
- IDEに依存せず、どの環境でも同じ方法で実行可能
- 本番環境での実行方法と同じため、デプロイ時の参考になる
- 起動時のログを詳細に確認できる
# プロジェクトディレクトリに移動(目的:実行対象のプロジェクトを指定)
# 理由:Mavenコマンドはpom.xmlがある場所で実行する必要がある
cd task-management
# Mavenでアプリケーションを起動(目的:Spring Bootアプリケーションの実行)
# 理由:spring-boot:runプラグインにより、開発用サーバーが起動される
mvn spring-boot:run
起動時に表示される重要な情報:
- ポート番号: アプリケーションがリッスンするポート(通常8080)
- データベース接続: SQLiteデータベースファイルの場所と接続状況
- コンテキストパス: アプリケーションのベースURL
- 起動時間: アプリケーションの起動にかかった時間
9.2 IDEからの実行
目的: 統合開発環境を使用した簡単な実行とデバッグ
理由:
- より簡単にアプリケーションを起動できる
- デバッグ機能を使用して問題を特定しやすい
- 開発中の頻繁な再起動に適している
IntelliJ IDEAの場合:
-
メインクラスの実行
-
目的:
TaskManagementApplication.java
を直接実行 - 理由: Spring Bootのメインクラスから直接アプリケーションを起動
-
src/main/java/com/example/taskmanagement/TaskManagementApplication.java
を右クリック - 「Run 'TaskManagementApplication'」を選択
-
目的:
-
Maven実行構成の作成
- 目的: 繰り返し実行するための設定保存
- 理由: 毎回コマンドを入力する手間を省き、効率的な開発を実現
- Run/Debug Configurations → 「+」→ Maven
- Working directory: プロジェクトルート
- Command line:
spring-boot:run
VS Code + Spring Boot Extensionの場合:
-
Spring Boot Dashboard使用
- 目的: 視覚的なアプリケーション管理
- 理由: GUIでアプリケーションの起動・停止・監視が可能
- Spring Boot Dashboardパネルを開く
- アプリケーション名の横の「▶」ボタンをクリック
9.3 実行確認
目的: アプリケーションが正常に起動したかを確認
理由: 起動エラーがないか、必要なサービスが利用可能かをチェックするため
ブラウザでのアクセス確認:
http://localhost:8080/tasks
期待される表示内容:
- ナビゲーションバーにアプリケーション名とメニュー
- 「タスクがありません」メッセージ(初回起動時)
- 「新規タスク作成」ボタンの表示
- レスポンシブデザインの確認(画面サイズを変更して確認)
ログの確認ポイント:
Started TaskManagementApplication in X.XXX seconds
このメッセージが表示されれば起動成功です。
なぜログ確認が重要なのか:
- エラーや警告メッセージから問題を早期発見できる
- データベース接続状況を確認できる
- アプリケーションの動作状況を監視できる
10. 機能テスト
🎯 目的
アプリケーションの各機能が仕様通りに動作するかを確認します。ユーザーの視点から実際の操作を行い、期待される結果が得られるかをテストします。
なぜ機能テストが必要なのか:
- 開発した機能が要求仕様を満たしているかを確認
- ユーザビリティの問題を発見し、改善点を特定
- 異なるブラウザや画面サイズでの動作を確認
- データの整合性と永続化が正しく行われているかを検証
10.1 基本機能のテスト
1. タスク作成機能のテスト
目的: 新規タスクが正しく作成されるかを確認
理由: アプリケーションの最も基本的な機能であり、他の機能の前提となる
テスト手順:
-
新規作成画面への遷移
-
http://localhost:8080/tasks/new
にアクセス - または一覧画面の「新規タスク作成」ボタンをクリック
- 確認点: フォーム画面が正しく表示される
-
-
必須項目の入力
タイトル: 「初回テストタスク」 説明: 「機能テストのためのサンプルタスク」 優先度: 「高」
- 確認点: 各フィールドに正しく入力できる
-
バリデーションのテスト
- タイトルを空にして送信 → エラーメッセージの表示確認
- 理由: 必須項目のバリデーションが正しく動作するかを確認
-
正常な作成
- 正しいデータで「作成」ボタンをクリック
-
確認点:
- 一覧画面にリダイレクトされる
- 作成したタスクが表示される
- 成功メッセージが表示される
2. タスク一覧表示機能のテスト
目的: 作成したタスクが正しく一覧表示されるかを確認
理由: ユーザーがタスクの全体状況を把握するための重要な機能
テスト手順:
-
基本表示の確認
- 作成したタスクが表示される
- タイトル、説明、優先度、作成日時が正しく表示される
- 優先度に応じた色分けが適用される
-
統計情報の確認
- 総タスク数: 1
- 未完了: 1
- 完了済み: 0
- 高優先度: 1(高優先度で作成した場合)
-
レスポンシブデザインの確認
- ブラウザの幅を変更して、レイアウトが適切に調整されるかを確認
- 理由: 様々なデバイスで使用される可能性があるため
3. タスク編集機能のテスト
目的: 既存のタスクを正しく編集できるかを確認
理由: タスクの内容変更は頻繁に発生する操作のため
テスト手順:
-
編集画面への遷移
- タスク一覧の「編集」ボタンをクリック
- 確認点: 既存のデータがフォームに正しく表示される
-
データの変更
タイトル: 「更新されたテストタスク」 説明: 「編集機能のテスト用に更新」 優先度: 「中」に変更 完了状態: チェックを入れる
-
更新の確認
- 「更新」ボタンをクリック
-
確認点:
- 一覧画面で変更内容が反映される
- 完了済みタスクのスタイル(取り消し線)が適用される
- 統計情報が更新される
4. タスク完了切り替え機能のテスト
目的: チェックボックスによる完了状態の切り替えが正しく動作するかを確認
理由: 最も頻繁に使用される機能の一つであり、ユーザビリティに直結
テスト手順:
-
未完了→完了への切り替え
- 未完了タスクのチェックボックスをクリック
-
確認点:
- ページが再読み込みされる
- タスクに取り消し線が適用される
- 統計情報が更新される
-
完了→未完了への切り替え
- 完了済みタスクのチェックボックスをクリック
- 確認点: 取り消し線が削除され、通常の表示に戻る
5. タスク削除機能のテスト
目的: 不要なタスクを安全に削除できるかを確認
理由: データの誤削除を防ぐ確認機能が重要
テスト手順:
-
削除確認ダイアログ
- 「削除」ボタンをクリック
- 確認点: 確認ダイアログが表示される
- 「キャンセル」を選択 → 削除されないことを確認
-
実際の削除
- 再度「削除」ボタンをクリック
- 確認ダイアログで「OK」を選択
-
確認点:
- タスクが一覧から削除される
- 統計情報が更新される
10.2 検索・フィルタ機能のテスト
目的: 大量のタスクから必要なものを効率的に見つけられるかを確認
理由: タスクが増加した際の実用性を確保するため
テストデータの準備
複数のタスクを作成して、検索・フィルタ機能をテストします:
タスク1: タイトル「重要な会議の準備」、優先度「高」、未完了
タスク2: タイトル「レポート作成」、優先度「中」、完了済み
タスク3: タイトル「メール返信」、優先度「低」、未完了
タスク4: タイトル「プロジェクト会議」、優先度「高」、未完了
テスト手順:
-
キーワード検索
- 目的: 部分一致でタスクを検索できるかを確認
- 検索キーワード「会議」を入力
- 確認点: 「重要な会議の準備」と「プロジェクト会議」が表示される
-
優先度フィルタ
- 目的: 特定の優先度のタスクのみ表示できるかを確認
- 優先度「高」を選択
- 確認点: 高優先度のタスクのみ表示される
-
完了状態フィルタ
- 目的: 完了済み/未完了の絞り込みができるかを確認
- 状態「完了済み」を選択
- 確認点: 完了済みタスクのみ表示される
-
複合検索
- 目的: 複数の条件を組み合わせた検索ができるかを確認
- キーワード「会議」+ 優先度「高」+ 状態「未完了」
- 確認点: 条件に合致するタスクのみ表示される
10.3 エラーハンドリングのテスト
目的: 異常な操作や入力に対して適切にエラー処理されるかを確認
理由: アプリケーションの安定性とユーザビリティを確保するため
テスト項目:
-
不正なURLアクセス
- 目的: 存在しないページへのアクセス処理を確認
-
http://localhost:8080/tasks/999/edit
にアクセス - 確認点: 適切なエラーページまたはリダイレクトが行われる
-
フォームバリデーション
- 目的: サーバーサイドバリデーションが正しく動作するかを確認
- タイトルに101文字以上入力して送信
- 確認点: エラーメッセージが表示され、データが保存されない
-
重複送信の防止
- 目的: フォーム送信ボタンを連続クリックした場合の処理を確認
- 「作成」ボタンを素早く複数回クリック
- 確認点: 重複してタスクが作成されない
11. パフォーマンステスト
🎯 目的
アプリケーションが適切なパフォーマンスで動作するかを確認します。レスポンス時間やリソース使用量を測定し、改善点を特定します。
なぜパフォーマンステストが必要なのか:
- ユーザーエクスペリエンスの向上(快適な操作感の実現)
- サーバーリソースの効率的な使用
- 将来的なユーザー増加に対する準備
- ボトルネックの早期発見と対策
11.1 基本的なパフォーマンス測定
ページ読み込み時間の測定
目的: 各ページの読み込み速度を確認
理由: ユーザーの離脱率に直結する重要な指標
測定方法:
-
ブラウザの開発者ツールを使用
- F12キーで開発者ツールを開く
- 「Network」タブを選択
- ページを再読み込み(Ctrl+F5)
-
確認点:
- 総読み込み時間
- 各リソース(CSS、JS、画像)の読み込み時間
- データベースクエリの実行時間
-
期待値
- 初回読み込み: 3秒以内
- 2回目以降(キャッシュ有効): 1秒以内
- 理由: 一般的にWebページの読み込みは3秒以内が推奨される
データベースクエリのパフォーマンス
目的: データベース操作の効率性を確認
理由: アプリケーションのボトルネックになりやすい部分
確認方法:
-
アプリケーションログの確認
- Spring Bootのログレベルを調整してSQLクエリを表示
-
application.properties
に以下を追加:
# SQLクエリの表示(目的:実行されるクエリの確認) # 理由:N+1問題などのパフォーマンス問題を発見するため spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # クエリ実行時間の表示(目的:遅いクエリの特定) # 理由:パフォーマンス改善の対象を明確にするため logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
-
パフォーマンス指標
- 単一タスク取得: 10ms以内
- タスク一覧取得(100件): 50ms以内
- タスク作成・更新: 20ms以内
11.2 負荷テスト(簡易版)
目的: 複数のユーザーが同時にアクセスした場合の動作を確認
理由: 実際の運用環境では複数ユーザーの同時アクセスが発生するため
ブラウザでの簡易負荷テスト
方法:
-
複数タブでの同時アクセス
- 同じブラウザで複数タブを開く
- 各タブで異なる操作を同時に実行
- 確認点: エラーが発生しないか、レスポンスが著しく遅くならないか
-
大量データでのテスト
- 100個程度のテストタスクを作成
- 一覧表示のパフォーマンスを確認
- 検索機能のレスポンス時間を測定
リソース使用量の監視
目的: アプリケーションのメモリ使用量とCPU使用率を確認
理由: リソース不足によるパフォーマンス低下を防ぐため
確認方法:
-
タスクマネージャー(Windows)/ Activity Monitor(Mac)での確認
- Java プロセスのメモリ使用量
- CPU使用率の推移
-
期待値:
- メモリ使用量: 200MB以下(小規模アプリケーション)
- CPU使用率: アイドル時5%以下
-
JVMメトリクスの確認
- Spring Boot Actuatorを使用した詳細な監視
-
application.properties
に追加:
# Actuatorエンドポイントの有効化(目的:アプリケーション監視) # 理由:メモリ使用量、スレッド数、データベース接続数などを監視するため management.endpoints.web.exposure.include=health,info,metrics management.endpoint.health.show-details=always
-
http://localhost:8080/actuator/metrics
でメトリクス確認
12. トラブルシューティング
🎯 目的
開発・実行中に発生する可能性のある問題とその解決方法を提供します。問題を迅速に特定し、解決できるようにします。
なぜトラブルシューティングが重要なのか:
- 開発効率の向上(問題解決時間の短縮)
- 学習効果の向上(エラーの原因と対策を理解)
- 本番環境での問題対応能力の向上
- チーム開発での知識共有
12.1 起動時の問題
問題1: ポートが既に使用されている
エラーメッセージ:
Port 8080 was already in use.
原因:
- 他のアプリケーションがポート8080を使用している
- 前回起動したアプリケーションが正常に終了していない
解決方法:
-
他のプロセスの確認と終了
# Windows netstat -ano | findstr :8080 taskkill /PID <プロセスID> /F # Mac/Linux lsof -i :8080 kill -9 <プロセスID>
- 目的: ポートを占有しているプロセスを特定し終了
- 理由: 同じポートを複数のアプリケーションが使用することはできない
-
別のポートを使用
# application.propertiesに追加 server.port=8081
- 目的: 競合を避けるため別のポートを使用
- 理由: 開発環境では柔軟にポートを変更できる
問題2: データベース接続エラー
エラーメッセージ:
Unable to obtain connection from database
原因:
- SQLiteデータベースファイルの権限問題
- データベース設定の誤り
解決方法:
-
データベースファイルの確認
# プロジェクトルートでデータベースファイルを確認 ls -la *.db
- 目的: データベースファイルの存在と権限を確認
- 理由: ファイルが存在しないか、書き込み権限がない可能性
-
設定の確認
# application.propertiesの確認 spring.datasource.url=jdbc:sqlite:taskdb.db spring.datasource.driver-class-name=org.sqlite.JDBC
12.2 実行時の問題
問題3: 404 Not Found エラー
症状: ブラウザで404エラーが表示される
原因:
- URLの間違い
- コントローラーのマッピング設定ミス
- テンプレートファイルの配置ミス
解決方法:
-
URLの確認
- 正しいURL:
http://localhost:8080/tasks
- 理由: ベースURLとコンテキストパスの確認が必要
- 正しいURL:
-
コントローラーマッピングの確認
@RequestMapping("/tasks") // このアノテーションが正しく設定されているか
-
テンプレートファイルの配置確認
-
src/main/resources/templates/
配下にHTMLファイルが存在するか - ファイル名がコントローラーの戻り値と一致するか
-
問題4: Thymeleafテンプレートエラー
エラーメッセージ:
Template parsing error
原因:
- HTMLの構文エラー
- Thymeleaf属性の記述ミス
- 存在しないオブジェクトへの参照
解決方法:
-
HTMLの構文確認
- タグの閉じ忘れがないか
- 属性の記述が正しいか
- 理由: Thymeleafは正しいHTMLを前提として動作する
-
Thymeleaf属性の確認
<!-- 正しい記述 --> <span th:text="${task.title}">タイトル</span> <!-- 間違った記述 --> <span th:text="task.title">タイトル</span> <!-- ${}が不足 -->
12.3 データベース関連の問題
問題5: データが保存されない
症状: フォームから送信してもデータベースに保存されない
原因:
- トランザクション設定の問題
- エンティティクラスの設定ミス
- バリデーションエラー
解決方法:
-
ログの確認
# ログレベルを調整してエラー詳細を確認 logging.level.org.springframework.web=DEBUG logging.level.org.hibernate=DEBUG
- 目的: 詳細なエラー情報を取得
- 理由: 問題の根本原因を特定するため
-
バリデーションエラーの確認
- フォームでバリデーションエラーが発生していないか
- コントローラーでエラーハンドリングが適切に行われているか
12.4 パフォーマンス問題
問題6: ページの読み込みが遅い
症状: ページ表示に時間がかかる
原因:
- 非効率なデータベースクエリ
- 大量のデータ取得
- リソースファイルの読み込み問題
解決方法:
-
SQLクエリの最適化
// N+1問題の解決例 @Query("SELECT t FROM Task t LEFT JOIN FETCH t.category") List<Task> findAllWithCategory();
- 目的: 不要なクエリ実行を削減
- 理由: データベースアクセス回数を最小限に抑える
-
ページネーションの実装
// 大量データの分割表示 Page<Task> findAll(Pageable pageable);
- 目的: 一度に表示するデータ量を制限
- 理由: メモリ使用量とレスポンス時間を改善
12.5 デバッグのベストプラクティス
効果的なデバッグ手順
目的: 問題を体系的に特定し、効率的に解決する
理由: 試行錯誤による時間の浪費を避け、学習効果を最大化
-
エラーメッセージの詳細確認
- スタックトレースの読み方を理解
- エラーが発生している正確な場所を特定
- 理由: 問題の根本原因を正確に把握するため
-
ログレベルの調整
# 開発時のログ設定 logging.level.com.example.taskmanagement=DEBUG logging.level.org.springframework.web=DEBUG
- 目的: 詳細な実行情報を取得
- 理由: アプリケーションの動作を詳細に追跡するため
-
段階的な問題の切り分け
- 最小限の機能から確認
- 一つずつ機能を追加して問題箇所を特定
- 理由: 複雑な問題を単純な要素に分解して解決
📝 実行とテストの完了
アプリケーションの実行とテストが完了しました。この段階で以下が確認できています:
✅ 確認済み項目
-
基本機能:
- タスクの作成、編集、削除
- 完了状態の切り替え
- 一覧表示と統計情報
-
ユーザビリティ:
- レスポンシブデザイン
- エラーメッセージ表示
- 直感的な操作
-
パフォーマンス:
- 適切なレスポンス時間
- リソース使用量の確認
- データベースクエリの効率性
-
安定性:
- エラーハンドリング
- データの整合性
- 異常な操作への対応
🚀 次のステップ
アプリケーションが正常に動作することを確認できたら、以下の拡張機能を検討できます:
- ユーザー認証機能: Spring Securityを使用したログイン機能
- カテゴリ機能: タスクの分類管理
- 期限管理: 締切日の設定と通知
- ファイル添付: タスクへのファイル添付機能
- API機能: REST APIの提供
- デプロイ: クラウドサービスへのデプロイ
これで基本的なタスク管理アプリケーションの開発、実行、テストが完了しました。各工程で「なぜその操作が必要なのか」を理解することで、より効果的な開発ができるようになります。