3
0

新規登録機能を作成していきましょう。

2.設計で検討した一覧機能をおさらい

機能一覧
1.タスクの登録:
ユーザーが新しいタスクを入力し、ToDoリストに追加できる機能。タスクの登録項目としては、タイトル、説明、期日がある。

画面遷移
一覧画面 ⇔ 登録画面 ⇔ 確認画面 → 完了画面 (→ 一覧画面)

画面一覧

  • 登録画面
  • 確認画面
  • 完了画面

3.画面項目

  • 登録画面
    タイトル 入力 100文字まで/必須
    説明 入力 200文字まで
    期限 入力 必須 yyyy/mm/dd
    確認 ボタン 確認画面に遷移
    もどる ボタン 一覧画面に遷移
  • 確認画面
    タイトル 出力
    説明 出力
    期限 出力 yyyy/mm/dd
    ステータス 出力 変更のときのみ
    完了 ボタン 登録/変更処理を実行し、完了画面に遷移
    もどる ボタン 確認画面に遷移
  • 完了画面
    完了メッセージ 出力 
    "The data was successfully saved."登録したときのみ表示
    go to list ボタン 一覧画面に遷移

4.バリデーションメッセージ
必須チェックエラー:
文字数チェックエラー:
整合性チェックエラー:
Titleを入力してください。
100文字以内で入力してください。
descriptionは200文字以内で入力してください。
deadlineを入力してください。

ここまでみていくと、新規登録機能とは、登録画面~確認画面~完了画面を経て登録ができるようです。以下のような画面になりそうです。

登録画面
image.png

確認画面
image.png

完了画面
image.png

ユーザーからの見られ方を想像してみます。

  1. ブラウザから一覧画面にアクセス
  2. 一覧画面が表示される
  3. 新規登録ボタンを押下
  4. タイトル、説明、期限を入力し、確認ボタンを押下
  5. 確認画面で内容を確認し、完了ボタンを押下
  6. 完了画面で登録できたメッセージを確認し、go to listボタンを押下
  7. 一覧画面で新規登録したタスクが一覧に表示される

URL設計で検討したもので、新規登録機能で関係があるのは以下のものでしょうか

画面名: タスク新規登録画面
URL: /task/add
HTTPメソッド: GET

画面名: タスク確認画面
URL: /task/confirm
HTTPメソッド: GET

画面名: (リダイレクト先が存在しないため、リダイレクトされないため画面名はなし)
URL: /task/save
HTTPメソッド: POST

画面名: タスク完了画面
URL: /task/complete
HTTPメソッド: GET

実装方針(箇条書き)

  • Controllerクラス

    • HTTPリクエストを受け取り、適切なServiceクラスのメソッドを呼び出す。
    • 必要なデータを取得し、ビューに渡して適切なHTMLファイルを返す。
  • Serviceクラス

    • ビジネスロジックを実装。
    • データの操作や処理を行い、Repositoryクラスを介してデータベースとのやり取りを行う。
    • ビジネスロジックをControllerクラスから切り離し、コードの再利用性や保守性を向上。
  • Repositoryクラス

    • データベースとのやり取りを担当。
    • Mapperクラスを介してSQLクエリを定義し、Repositoryクラスから呼び出す。
  • Mapperクラス

    • MyBatisを使用してSQLクエリを実行し、データの永続化や取得、更新、削除などの操作を行う。

実装の流れ

  • まずControllerクラスから始めて、リクエストに対する処理を実装。
  • 次にServiceクラスでビジネスロジックを実装し、必要なデータの操作を行う。
  • Repositoryクラスを実装し、Mapperクラスを呼び出す。
  • Mapperクラスを実装し、MyBatisを使用してSQLクエリを実行するメソッドを定義。
  • 最後にHTMLファイルを作成し、ThymeleafでHTMLにマージする。

URL設計をベースに登録画面~確認画面~完了画面と画面遷移をしながらタスクを登録する処理を書いていきます。

それでは、Javaの実装を行いましょう。

controllerクラスを書いてみよう

URL設計で検討した4つURLをもとにcontrollerクラスのメソッドを書いていきましょう。

まず、1つ目は登録画面を表示させるメソッドです。「/task/add」というURLを受け取り、登録画面のhtmlと返り値として返してあげます。
このメソッドでのポイントは、TaskFormを生成し、Modelにセットすることです。その理由はThymeleafテンプレートエンジンを使用してフォームの入力フィールドとtaskFormオブジェクトがバインドするためです。

	@GetMapping(value = "/task/add")
	public String showForm(Model model) {
	    // タスクフォームを作成
	    TaskForm taskForm = new TaskForm();
	    
	    model.addAttribute("taskForm", taskForm);
	    return "task/edit";
	}

2つ目のメソッドを書きましょう。登録画面から確認画面に遷移するメソッドです。登録画面で値を入力し、確認画面に遷移する際に、バリデーションチェックがはしることを想定するのがポイントです。

「/task/confirm」というURLを受け取り、確認画面のhtmlと返り値として返してあげます。さらに、バリデーションチェックはSpringの機能を使って実装します。@Validatedをつけるとメソッドの引数のオブジェクトに対してバリデーションを行います。BindingResultは、バリデーション結果を格納するオブジェクトです。@Validatedでバリデーションが実行された後、その結果がBindingResultに格納されます。hasErrors()メソッドを使うと、バリデーションエラーがあるかどうかをチェックできます。

バリデーションチェックでエラーがある場合は、変更画面に戻します。その場合は、登録画面のhtmlを返り値として返してあげます。

	@GetMapping(value = "/task/confirm")
	public String showConfirmForm(@Validated TaskForm taskForm, BindingResult bindingResult, Model model) {
		
		// バリデーションチェックでエラーがある場合は変更画面に戻る
		if (bindingResult.hasErrors()) {
			return "task/edit";
		}
		
		model.addAttribute("taskForm", taskForm);
		return "task/confirm";
	}


3つ目4つ目のメソッドを書いていきましょう。確認画面でsubmitボタンを押下すと、保存処理を呼び出しリダイレクトして(3つ目のメソッド)、完了画面に遷移(4つ目のメソッド)します。保存処理のメソッドからリダイレクトして完了画面を表示させるメソッドに繋げるところがポイントです。

postメソッドで「/task/save」というURLを受け取り、確認画面のhtmlと返り値としてリダイレクトさせます。リダイレクトさせるときの記法は、画面のhtmlのパスに「redirect:」をつけることです。

なぜリダイレクトさせるかというと、フォームの再送信を防ぐためです。フォームの再送信がされてしまうと、データベース更新が再実行され、データの重複や不整合が発生する原因となります。フォーム送信後にリダイレクトを行うことで、GETリクエストに置き換わり再送信を防止できます。PRGパターンという一般的な設計パターンです。

保存処理の前にバリデーションチェックでエラーがあったら変更画面に遷移させる記述をしておきます。登録画面から確認画面に遷移する際にバリデーションは行っていますが、確認画面でいたずらされると保存処理に影響が出る可能性もあるので、保存処理の直前でチェックNGで変更画面に遷移する処理を書きます。

そして、保存処理のメソッドなので、serviceクラスに画面で入力された値(taskForm)を渡してあげます。serviceクラスからは完了画面で表示するメッセージが渡ってくるため、completeMessageという変数で受け取り、完了画面に渡してあげます。リダイレクト先に値を渡すにはmodelでは渡せないため、「完了メッセージ」はredirectAttributes.addFlashAttribute()を使い、値を渡します。

完了画面を表示させるメソッドは、GETメソッドで受け取ることがポイントです。

	@PostMapping(value = "/task/save")
	public String saveTask(@Validated TaskForm taskForm, BindingResult bindingResult, RedirectAttributes redirectAttributes,Model model) {
		
		//バリデーションチェック
		if (bindingResult.hasErrors()) {
			// バリデーションエラーがある場合は変更画面に遷移
			return "task/edit";
		}
		
		//保存処理
		String completeMessage =taskService.save(taskForm);
		
		//redirect先に値を渡す
		redirectAttributes.addFlashAttribute("completeMessage", completeMessage);
		
		return "redirect:/task/complete";
	}
	
    @GetMapping("/task/complete")
    public String showCompletePage() {
        return "task/complete";
    }

【FYI】
新人Webエンジニア必須?の知識「PRGパターン」について
https://zenn.dev/imah/articles/3d186a6462ecc8

formクラスを書いてみよう

formクラスを書いていきます。formクラスは画面で入力された値をまとめる働きがあります。基本的に画面の入出力項目を用意し、アノテーションでバリデーションをつけていきます。

まず、form用のパッケージを作成し、TaskFormというクラス名でクラスを作成しましょう。登録画面では、タイトル、説明、デッドラインを項目として持っているのでこの変数を記述していくのですが、今回は変更画面でも使う値(タスクID,ステータス、更新日時)も一緒に書いていきます。(このタイミングで書いておくと、後々書き足す手間がなくなるので)

以下のようなアノテーションを使って、バリデーションチェックを実現させます。
必須チェック→ @NotNull
文字数チェック→@Size

【FYI】
@NotBlank@NotEmpty@NotNullの挙動の違いをSpring Boot + Thymeleafで整理する
https://qiita.com/NagaokaKenichi/items/67a63c91a7db8717fc7d

public class TaskForm {
	// タスクID
	private int taskId;
	
	// タイトルは1文字以上100文字以下
	@NotBlank
	@Size(min = 1, max = 100)
    private String title;
	
	// 説明は最大200文字
	@Size(max = 200)
    private String description;
    
	// デッドラインは必須項目
	@NotNull
    private LocalDateTime deadline; 
    
	// ステータスは1から3の範囲
	@Min(value = 0)
	@Max(value = 3)
    private int status;
	
	// 更新日時
	private LocalDateTime updatedAt;
	
	public int getTaskId() {
		return taskId;
	}
	
	public void setTaskId(int 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 int getStatus() {
		return status;
	}
	
	public void setStatus(int status) {
		this.status = status;
	}
	
	public LocalDateTime getDeadline() {
		return deadline;
	}
	
	public void setDeadline(LocalDateTime deadline) {
		this.deadline = deadline;
	}
	
	public LocalDateTime getUpdatedAt() {
		return updatedAt;
	}

	public void setUpdatedAt(LocalDateTime updatedAt) {
		this.updatedAt = updatedAt;
	}
}

バリデーションチェックを設定したので、バリデーションチェックメッセージも設定しましょう。バリデーションメッセージとは、バリデーションチェックでNGのときに、画面に表示されるメッセージのことです。

実装の仕方は、message.propertyというファイルを作ってその中にメッセージを書いていきます。画面側でバリデーションチェックNGがあったときに、thymealfでこのメッセージを取得し、画面に表示させます。

では、src/main/resouce配下に、messages.propertiesというファイルを作りましょう。必須チェック・文字数チェックのバリデーションチェックメッセージは以下の通りでした。

Titleを入力してください。
100文字以内で入力してください。
descriptionは200文字以内で入力してください。
deadlineを入力してください。

チェックと項目とメッセージを紐づけるような形で、以下のように記述します。エクリプスをエディタとして使い、messages.propertiesに以下の文言をコピペすると、文字コードの関係で日本語部分が数字とアルファベットに置き換わるかと思いますが、問題ありません。

NotBlank.taskForm.title=Titleを入力してください。
Size.taskForm.title=100文字以内で入力してください。
Size.taskForm.description=descriptionは200文字以内で入力してください。
NotNull.taskForm.deadline=deadlineを入力してください。

serviceクラスを書いてみよう

controllerクラスからserviceクラスを呼び出したのは、保存処理のときでしたね。登録画面で入力した値を保存するメッセージを書いていきましょう。

まず、taskService.javaにserviceのインターフェースを書きます。

	String save(TaskForm taskForm);

次に実装クラスの記述を行っていきます。オーバーライドのアノテーションとトランザクションのアノテーションをつけて、TaskFormを受け取るメソッドを作成します。

トランザクションのアノテーションとは、トランザクション管理を行うものです。途中でエラーが発生した場合ロールバックしてくれます。このようにトランザクション管理をすることで、データの整合性を保つことができます。

	@Override
	@Transactional
	public String save(TaskForm taskForm) {
		
		return ;
	}

【FYI】
SpringBootでトランザクション処理を実装しよう
https://qiita.com/msrx9/items/9db977df275ed2e72dfa

repositoryクラスのオブジェクトを使って、画面で入力された値を渡していきます。ここで注意するのは、repositoryクラス以降では、画面で入力された値をformクラスのオブジェクトではなく、entityクラスのオブジェクトとして扱います。そのため、Formオブジェクトからentityオブジェクトに変換するメソッドが必要になります。実装クラスにメソッドを足すため、インターフェースにもコードを追加する必要がありますね。convertToTaskというメソッドをつけ足しました。

このアプリケーションでは、フォームオブジェクト (TaskForm) とエンティティ (Task) の違いは以下のようにしています。
TaskForm: フォームからの入力データを受け取るためのオブジェクト。ユーザー入力に適した形式でデータを保持します。
Task: データベースに保存するためのエンティティオブジェクト。データベースのスキーマに一致する形式でデータを保持します。

画面で入力された値をentityクラスに入れ直したら、repositoryクラスのオブジェクトを呼び出します。

service.java
    Task convertToTask(TaskForm taskForm);
serviceImpl.java
	@Override
	@Transactional
	public String save(TaskForm taskForm) {
		
		//変換処理
		Task task = convertToTask(taskForm);
		
		//登録処理の場合
		taskRepository.save(task);
		return ;
	}

	@Override
	public Task convertToTask(TaskForm taskForm) {
	    Task task = new Task();
	    task.setTaskId(taskForm.getTaskId());
	    task.setTitle(taskForm.getTitle());
	    task.setDescription(taskForm.getDescription());
	    task.setDeadline(taskForm.getDeadline());
	    task.setStatus(taskForm.getStatus());
	    task.setUpdatedAt(taskForm.getUpdatedAt());
	    return task;
	    }

今回、serviceクラスの保存処理のメソッドでは、完了画面に表示するメッセージを共通クラスから取得し、controllerクラスに返り値として返してあげることにします。

設計より、完了画面では登録・変更・削除に応じて完了メッセージを表示します。完了メッセージは定数クラスをつくって1か所で管理します。今回は、その定数クラスの完了メッセージをserviceクラスで取得します。定数クラスをつくる理由は、定数を一元管理するためです。まとめて管理することで、定数の変更が容易になります。

では、「com.example.demo.common」というパッケージをつくり、定数クラス(Constants.java)をつくります。ここでも、登録機能以外でも使用するメッセージを全て書いてしまいます。(このタイミングで書いておくと、後々書き足す手間がなくなるので)まず、インスタンスの生成を禁止するため、引数なしのコンストラクタを記述します。

そして、それぞれのメッセージは以下のように書きます。staticをつけているのがポイントです。staticをつけると、クラスをインスタンス化せず定数にアクセスすることができます。また、変数名は大文字とアンスコでつけるのが慣習です。

	
	// インスタンスの生成禁止
	private Constants (){}
	
    public static final String REGISTER_COMPLETE = "The data was successfully saved.";
    public static final String EDIT_COMPLETE = "The data was successfully updated.";
    public static final String DELETE_COMPLETE = "The data was successfully deleted.";
    public static final String ERROR_MESSAGE = "エラーが発生しました.";
    public static final String ILLEGALARGUMENTEXCEPTION_ERROR = "タスクIDは正の整数である必要があります。";

serviceクラス側で、「REGISTER_COMPLETE("The data was successfully saved.")」を呼び出して返り値にセットしましょう。
クラス名+変数名で呼び出すことができます。

		//完了メッセージをセット
		String completeMessage = Constants.REGISTER_COMPLETE;
		return completeMessage;

repositoryクラスを書いてみよう

repositoryクラスで保存処理のメソッドを書いていきましょう。
以下のように記載します。

    public void save(Task task) {
        taskMapper.save(task);
    }

mybatisを書いてみよう

ます、mapperのインターフェースを書いていきます。

		void save(Task task);

次に、mapper.xmlの中にSQLを書いていきます。登録画面で入力された値をtaskテーブルに登録します。ただ、ここでは登録画面で入力されていない値もinsertしています。status,created_at,updated_at, deleteFlgです。

タスクを登録された時点では、タスクは未着手というステータスであるため、手動では登録しませんがシステム的に「気を利かせて」、未着手を示す「1」を登録します。created_at,updated_atは登録や更新があった日時を記録するものなので、インサートの対象です。未削除の状態のためdeleteFlgも0を登録します

登録なので、insert文を使用して以下のように書いていきます。

    <insert id="save">
        INSERT INTO task
            (title, description, deadline, status,created_at,updated_at, deleteFlg)
        VALUES
            (#{title}, #{description}, #{deadline}, 1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0);
    </insert>

htmlマージをしてみよう

次に、htmlを書いていきましょう。登録機能では、設計より以下のように画面遷移します。

画面遷移
一覧画面 ⇔ 登録画面 ⇔ 確認画面 → 完了画面 (→ 一覧画面)

まずは、一覧画面を修正します。登録画面に遷移するための「create」というボタンに修正を入れます。hrefにthymeafをあててます。

現状は以下のようになっているかと思います。

<a type="button" class="btn btn-primary btn-lg px-4 me-sm-3" href="/task/add">create</a>

hrefの部分を以下のように書きます。

<a type="button" class="btn btn-primary btn-lg px-4 me-sm-3" th:href="@{/task/add}">create</a>

次は、登録画面を書いていきましょう。thymeleafのないもととなるコードはこちらです。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>form page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>

    <nav class="navbar bg-body-tertiary">
        <div class="container-fluid">
          <span class="navbar-brand mb-0 h1">Practice</span>
        </div>
    </nav>

    <div class="container">

        <h1>Form</h1>

        <form class="px-4 pt-3 my-3" method="get" action="/task/confirm">
            <div class="form-group mb-3">
              <label>Title</label>
              <input type="text" class="form-control" placeholder="Title" name="title">
              <div class="text-danger"></div>
            </div>
            <div class="form-group mb-3">
                <label>Description</label>
                <input type="text" class="form-control" placeholder="Description" name="description">
                <div class="text-danger"></div>
            </div>
            <div class="form-group mb-3">
                <label>Deadline</label>
                <input type="datetime-local" class="form-control" name="deadline">
                <div class="text-danger"></div>
            </div>
            <!-- 送信ボタン -->
            <button type="submit" class="btn btn-primary" value="Confirm">Confirm</button>
            <a type="button" class="btn btn-outline-secondary" href="/task/list">back</a>
          </form>
          
    </div>

    <footer class="py-3 my-4">
        <p class="text-center text-body-secondary">&copy; 2023 Company, Inc</p>
    </footer>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

まず、上記コードで、edit.htmlというファイルを作ります。場所は、template>task配下です。index.htmlと同じ階層ですね。

このedit.htmlに対して、行うことは以下の通りです。

  1. タグにThymeleafの名前空間を追加する。
  2. タグの th:action 属性と th:object 属性を追加し、action 属性を修正する。
  3. 各フォームフィールドの th:value 属性を追加する
  4. エラーメッセージを表示する(各フィールドのエラーメッセージ表示に th:if 属性と th:errors 属性を追加する。)

では書いていきます。

  1. タグにThymeleafの名前空間を追加する。

以下のように追加しました。Thymeleafの名前空間を追加する理由は、HTMLテンプレートエンジンとしてThymeleafが提供する属性を認識し、正しく解釈するためです。

Thymeleafは、特定の属性を使用してHTMLテンプレートを動的に生成します。例えば、th:object、th:value、th:if、th:eachなどの属性はThymeleafでは特別な意味を持ちます。名前空間を宣言することで、Thymeleafエンジンに「これらの属性はThymeleafによる処理対象である」と伝えることができます。

<html lang="en" xmlns:th="http://www.thymeleaf.org">
  1. タグの th:action 属性と th:object 属性を追加し、action 属性を修正する。

actionをth:actionに変更し、@をつけるなどaction属性を修正しました。
th:object 属性を追加しました。これによって、フォームの入力データはtaskFormオブジェクトに自動的にマッピングされます。

<form class="px-4 pt-3 my-3" method="get" th:action="@{/task/confirm}" th:object="${taskForm}">

また、以下の例のように、th:object 属性を書くことで記述が簡潔になります。th:object 属性を使わない場合、各フィールドでは$を使いオブジェクトを毎回指定する必要があります。

<!-- th:objectを使用しない場合 -->
<form action="/submit" method="post">
  <input type="text" name="title" value="${taskForm.title}" />
  <input type="text" name="description" value="${taskForm.description}" />
  <!-- その他のフィールド -->
</form>

<!-- th:objectを使用する場合 -->
<form th:object="${taskForm}" action="/submit" method="post">
  <input type="text" th:field="*{title}" />
  <input type="text" th:field="*{description}" />
  <!-- その他のフィールド -->
</form>

  1. 各フォームフィールドの th:value 属性を追加する

th:valueを追加しました。th:valueを使うと、バリデーションエラーでフォームが再表示される際に、入力した値を表示することができます。さらに、今回登録画面と変更画面で同じHTMLファイルを使う予定です。変更画面では、登録した値を初期表示させる必要があるため、th:valueを使いました。

<input type="text" class="form-control" placeholder="Title" name="title" th:value="*{title}">
<input type="text" class="form-control" placeholder="Description" name="description" th:value="*{description}">
<input type="datetime-local" class="form-control" name="deadline" th:value="*{deadline}">
  1. エラーメッセージを表示する(各フィールドのエラーメッセージ表示に th:if 属性と th:errors 属性を追加する。)

th:if属性を使用すると、特定のフィールドにエラーがある場合にのみエラーメッセージを表示する条件を設定します。th:errors属性は、指定したフィールドに関連するエラーメッセージを取得して表示します。つまり、それぞれのフィールドでバリデーションチェックエラーがあった場合に、エラーメッセージを表示することができます。

<div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="text-danger"></div>
<div th:if="${#fields.hasErrors('description')}" th:errors="*{description}" class="text-danger"></div>
<div th:if="${#fields.hasErrors('deadline')}" th:errors="*{deadline}" class="text-danger"></div>

次は、確認画面を書いていきましょう。thymeleafのないもととなるコードはこちらです。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Confirm</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>

    <nav class="navbar bg-body-tertiary">
        <div class="container-fluid">
          <span class="navbar-brand mb-0 h1">Practice</span>
        </div>
    </nav>

    <div class="container">

        <h1>Confirm</h1>

        <div class="px-4 pt-3 my-3">
            <div class="mb-3">
                <label>Title</label>
                <p>title</p>
            </div>
            <div class="mb-3">
                <label>Description</label>
                <p>description</p>
            </div>
            <div class="mb-3">
                <label>Deadline</label>
                <p>Formatted Deadline</p>     
            </div>
        </div>
        
        <div class="d-flex justify-content-start">
            <form method="post" class="mb-3">
                <input type="hidden" name="title">
                <input type="hidden" name="description">
                <input type="hidden" name="deadline">
                <!-- 送信ボタン -->
                <button type="submit" class="btn btn-primary me-2" value="submit">submit</button>
            </form> 
                <button type="submit" class="btn btn-outline-secondary">back</button> 
        </div>
          
    </div>

    <footer class="py-3 my-4">
        <p class="text-center text-body-secondary">&copy; 2023 Company, Inc</p>
    </footer>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

まず、上記コードで、confirm.htmlというファイルを作ります。場所は、template>task配下です。index.htmlと同じ階層ですね。

このconfirm.htmlに対して、行うことは以下の通りです。

  1. タグにThymeleafの名前空間を追加する。
  2. 変数の値をテキストとして挿入するための属性を追加する。
  3. タグの th:action 属性と th:object 属性を追加し、action 属性を修正する。
  4. 登録画面で入力された値をhiddenで持つ入力フィールドを追加する

では、書いていきます。

  1. タグにThymeleafの名前空間を追加する。
  2. タグの th:action 属性と th:object 属性を追加し、action 属性を修正する。

1と3は登録画面でやったので、説明は割愛します。以下のように書きます。

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<form th:action="@{/task/save}" method="post" th:object="${taskForm}"class="mb-3">

  1. 変数の値をテキストとして挿入するための属性を追加する。

登録画面で入力された値を確認画面の各項目に表示します。確認画面では項目を閲覧するだけなので、titleなど項目はpタグで表示されています。つまり、入力がないので、th:valueは使いません。pタグやdivタグなどで入力された値を表示する場合は、th:textを使用します。Deadlineは日付フォーマットを整えましょう。

<p th:text="${taskForm.title}">title</p>
<p th:text="${taskForm.description}">description</p>
<p th:text="${#temporals.format(taskForm.deadline, 'yyyy/MM/dd HH:mm')}">Formatted Deadline</p> 
  1. 登録画面で入力された値をhiddenで持つ入力フィールドを追加する

入力値をhiddenで保持する理由は、hiddenフィールドに保存することで、ユーザーが直接変更できないようにするためです。確認画面なので。以下のように記載することで、hiddenで保持した入力値はユーザー側から画面で見えず、バックエンド側に送ることができます。

		<form th:action="@{/task/save}" method="post" th:object="${taskForm}"class="mb-3">
            <input type="hidden" name="title" th:field="*{title}">//追記部分
            <input type="hidden" name="description" th:field="*{description}">//追記部分
            <input type="hidden" name="deadline" th:field="*{deadline}">//追記部分
            <!-- 送信ボタン -->
            <button type="submit" class="btn btn-primary me-2" value="submit">submit</button>
		</form> 

次は、完了画面を書いていきましょう。コードはこちらです。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>complete</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>

    <nav class="navbar bg-body-tertiary">
        <div class="container-fluid">
          <span class="navbar-brand mb-0 h1">Practice</span>
        </div>
    </nav>

    <div class="container">
        <div class="text-center px-4 pt-3 my-3">
            <p>complete</p>
            <a type="button" class="btn btn-primary" href="/task/list">Go to List</a>
         </div>
    </div>

    <footer class="py-3 my-4">
        <p class="text-center text-body-secondary">&copy; 2023 Company, Inc</p>
    </footer>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>

まず、上記コードで、complete.htmlというファイルを作ります。場所は、template>task配下です。index.htmlと同じ階層ですね。

このcomplete.htmlに対して、行うことは以下の通りです。

  1. タグにThymeleafの名前空間を追加する。
  2. 変数の値をテキストとして挿入するための属性を追加する。
  3. href属性を追加する

1.はもうおなじみです。

<html lang="en" xmlns:th="http://www.thymeleaf.org">

2.変数の値をテキストとして挿入するための属性を追加する。

完了メッセージを表示できるように、th:textを追加します。

<p th:text="${completeMessage}">complete</p>

3.href属性を追加する

一覧画面に戻るリンクのボタンにth:hrefを追加します。

<a type="button" class="btn btn-primary" th:href="@{/task/list}">Go to List</a>

動作確認

ここまで、一覧画面から登録画面、確認画面、完了画面に遷移し、登録画面で入力した値をDBに登録するコードを書きました。

登録機能で確認すべきポイントは大きく2つです。
1.画面に沿って遷移して、登録画面で入力した値がDBに登録されるか
2.登録画面でバリデーションチェックが正しくかかるか

1.について、DBに登録されるかは、ブラウザ上で登録操作をした後に、MySQLworkbenchで以下のSQLを打ってDBに値が入っているかを確認して、チェックします。

select * from tutorialtodoapplication.task;

2.について、登録画面でバリデーションチェックにかかるように入力し「Confirm」を押下します。確認画面ではなく、バリデーションメッセージが表示されている登録画面が表示されればOKです。

次の投稿では、変更機能を実装していきます。

【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-①イントロダクション-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-②設計-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-③実装方針と環境構築-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-④一覧機能の作成-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-⑤新規登録機能の作成-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-⑥変更機能の作成-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-⑦削除機能の実装-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-⑧戻る機能の実装-
【Java】Spring Bootを使ったToDoアプリケーションを作成しよう-⑨例外処理の実装-

ここまでで書いたコードの全量

コメントも入れています。

controllerクラス

package com.example.demo.controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.demo.entity.Task;
import com.example.demo.form.TaskForm;
import com.example.demo.service.TaskService;


/**
 * Webアプリケーションのタスク関連機能を担当するControllerクラスです。
 * タスクの一覧表示、登録、変更などの機能が含まれています。
 *
 */
@Controller
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    /**
     * タスクの一覧を表示するメソッドです。
     * 
     * @param model タスク一覧をViewに渡すためのSpringのModelオブジェクト
     * @return "task/index" - タスク一覧表示用のHTMLテンプレートのパス
     */
	@RequestMapping(value = "/task/list", method = RequestMethod.GET)
	public String showTask(Model model) {
		
		//タスクの一覧を取得
		List<Task> taskList = taskService.findAll();		
		model.addAttribute("taskList", taskList);
		
		return "task/index";
	}
	
	/**
	 * タスクの新規登録画面を表示するメソッドです。
	 * 
	 * @param model タスク一覧をViewに渡すためのSpringのModelオブジェクト
	 * @return "task/edit" - タスク新規登録画面のHTMLテンプレートのパス
	 */
	@GetMapping(value = "/task/add")
	public String showForm(Model model) {
	    // タスクフォームを作成
	    TaskForm taskForm = new TaskForm();
	    
	    model.addAttribute("taskForm", taskForm);
	    return "task/edit";
	}
	
	/**
	 * タスクの確認画面を表示するメソッドです。
	 * 
	 * @param taskForm タスクのフォームデータ
	 * @param bindingResult バリデーション結果を保持するオブジェクト
	 * @param model タスク一覧をViewに渡すためのSpringのModelオブジェクト
	 * @return "task/confirm" - タスク確認画面のHTMLテンプレートのパス
	 */
	@GetMapping(value = "/task/confirm")
	public String showConfirmForm(@Validated TaskForm taskForm, BindingResult bindingResult, Model model) {
		
		// バリデーションチェックでエラーがある場合は変更画面に戻る
		if (bindingResult.hasErrors()) {
			return "task/edit";
		}
		
		model.addAttribute("taskForm", taskForm);
		return "task/confirm";
	}
	
	/**
	 * タスクを保存するメソッドです。
	 * 
	 * @param taskForm タスクのフォームデータ
	 * @param bindingResult バリデーション結果を保持するオブジェクト
	 * @param redirectAttributes リダイレクト時に属性を渡すためのSpringのRedirectAttributesオブジェクト
	 * @param model タスク一覧をViewに渡すためのSpringのModelオブジェクト
	 * @return "redirect:/task/complete" - タスク確認画面へのリダイレクト
	 */
	@PostMapping(value = "/task/save")
	public String saveTask(@Validated TaskForm taskForm, BindingResult bindingResult, RedirectAttributes redirectAttributes,Model model) {
		
		//バリデーションチェック
		if (bindingResult.hasErrors()) {
			// バリデーションエラーがある場合は変更画面に遷移
			return "task/edit";
		}
		
		//保存処理
		String completeMessage =taskService.save(taskForm);
		
		//redirect先に値を渡す
		redirectAttributes.addFlashAttribute("completeMessage", completeMessage);
		
		return "redirect:/task/complete";
	}
	
	/**
	 * タスク完了画面を表示するメソッドです。
	 * 
	 * @return "task/complete" - タスク完了画面のHTMLテンプレートのパス
	 */
    @GetMapping("/task/complete")
    public String showCompletePage() {
        return "task/complete";
    }
	
	
	
}

serviceクラス

package com.example.demo.service;

import java.util.List;

import com.example.demo.entity.Task;
import com.example.demo.form.TaskForm;

/**
 * タスク関連のサービスを提供するインターフェースです。
 */
public interface TaskService {
	
    /**
     * すべてのタスクを取得します。
     *
     * @return タスクのリスト
     */
	List<Task> findAll();
	
    /**
     * タスクを保存します。
     *
     * @param taskForm タスクのフォームデータ
     * @return 保存完了メッセージ
     */
	String save(TaskForm taskForm);
	
    /**
     * タスクのフォームデータをタスクエンティティに変換します。
     *
     * @param taskForm タスクのフォームデータ
     * @return タスクエンティティ
     */
    Task convertToTask(TaskForm taskForm);

}
package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.common.Constants;
import com.example.demo.entity.Task;
import com.example.demo.form.TaskForm;
import com.example.demo.repository.TaskRepository;

/**
 * タスク関連のビジネスロジックを担当するサービスクラスです。
 * タスクの検索、保存、更新などの機能を提供します。
 */
@Service
public class TaskServiceImpl implements TaskService{

	@Autowired
	TaskRepository taskRepository;
	
	/**
	 * タスク一覧を取得するメソッドです。
	 *
	 * @return List<Task> タスク一覧。
	 */
	@Override
	public List<Task> findAll() {
		return taskRepository.findAll();
		}
	
	
	@Override
	@Transactional
	public String save(TaskForm taskForm) {
		
		//変換処理
		Task task = convertToTask(taskForm);
		
		//登録処理の場合
		taskRepository.save(task);
		
		//完了メッセージをセット
		String completeMessage = Constants.REGISTER_COMPLETE;
		return completeMessage;
	}
	
	
	@Override
	public Task convertToTask(TaskForm taskForm) {
	    Task task = new Task();
	    task.setTaskId(taskForm.getTaskId());
	    task.setTitle(taskForm.getTitle());
	    task.setDescription(taskForm.getDescription());
	    task.setDeadline(taskForm.getDeadline());
	    task.setStatus(taskForm.getStatus());
	    task.setUpdatedAt(taskForm.getUpdatedAt());
	    return task;
	    }

}

repositoryクラス

package com.example.demo.repository;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.example.demo.entity.Task;
import com.example.demo.mapper.TaskMapper;


/**
 * タスク情報にアクセスするためのリポジトリクラスです。
 */
@Repository
public class TaskRepository {
	
	private final TaskMapper taskMapper;
	
    /**
     * コンストラクタ
     *
     * @param taskMapper タスクデータへのマッパー
     */
    public TaskRepository(TaskMapper taskMapper) {
        this.taskMapper = taskMapper;
    }

    /**
     * 全てのタスクを取得します。
     *
     * @return タスクのリスト
     */
    public List<Task> findAll() {
        return taskMapper.findAll();
    }
    
    /**
     * タスクを保存します。
     *
     * @param task 保存するタスク
     */
    public void save(Task task) {
        taskMapper.save(task);
    }

}

mapperクラス

package com.example.demo.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.demo.entity.Task;

/**
 * タスクエンティティにアクセスするための MyBatis マッパーインターフェースです。
 */
@Mapper
public interface TaskMapper {
	
    /**
     * 全てのタスクを取得します。
     *
     * @return タスクのリスト
     */
    List<Task> findAll();
    
    /**
     * タスクを保存します。
     *
     * @param task 保存するタスク
     */
    void save(Task task);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.TaskMapper">
	
	<!-- タスクの全件取得 -->
	<select id="findAll" resultType="com.example.demo.entity.Task">
        SELECT * FROM task where deleteFlg = 0;
    </select>
    
    <!-- 新規タスクの登録 -->
    <insert id="save">
        INSERT INTO task
            (title, description, deadline, status,created_at,updated_at, deleteFlg)
        VALUES
            (#{title}, #{description}, #{deadline}, 1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0);
    </insert>
    
</mapper>

formクラス

package com.example.demo.form;

import java.time.LocalDateTime;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class TaskForm {
	// タスクID
	private int taskId;
	
	// タイトルは1文字以上100文字以下
	@NotBlank
	@Size(min = 1, max = 100)
    private String title;
	
	// 説明は最大200文字
	@Size(max = 200)
    private String description;
    
	// デッドラインは必須項目
	@NotNull
    private LocalDateTime deadline; 
    
	// ステータスは1から3の範囲
	@Min(value = 0)
	@Max(value = 3)
    private int status;
	
	// 更新日時
	private LocalDateTime updatedAt;
	
	public int getTaskId() {
		return taskId;
	}
	
	public void setTaskId(int 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 int getStatus() {
		return status;
	}
	
	public void setStatus(int status) {
		this.status = status;
	}
	
	public LocalDateTime getDeadline() {
		return deadline;
	}
	
	public void setDeadline(LocalDateTime deadline) {
		this.deadline = deadline;
	}
	
	public LocalDateTime getUpdatedAt() {
		return updatedAt;
	}

	public void setUpdatedAt(LocalDateTime updatedAt) {
		this.updatedAt = updatedAt;
	}
}

contentsクラス

package com.example.demo.common;

/**
 * 定数クラス
 *
 */
public class Constants {
	
	// インスタンスの生成禁止
	private Constants (){}
	
    public static final String REGISTER_COMPLETE = "The data was successfully saved.";
    public static final String EDIT_COMPLETE = "The data was successfully updated.";
    public static final String DELETE_COMPLETE = "The data was successfully deleted.";
    public static final String ERROR_MESSAGE = "エラーが発生しました.";
    public static final String ILLEGALARGUMENTEXCEPTION_ERROR = "タスクIDは正の整数である必要があります。";

}

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0