チュートリアル(Todoアプリケーション Thymeleaf編) ******************************************************************************** .. only:: html .. contents:: 目次 :depth: 3 :local: | はじめに ================================================================================ このチュートリアルで学ぶこと -------------------------------------------------------------------------------- * \ |framework_name|\による基本的なアプリケーションの開発方法 * MavenおよびSTS(Eclipse)プロジェクトの構築方法 * \ |framework_name|\の :doc:`../Overview/ApplicationLayering` に従った開発方法 | 対象読者 -------------------------------------------------------------------------------- * SpringのDIやAOPに関する基礎的な知識がある * Servlet/テンプレートエンジン(JSPなど)を使用してWebアプリケーションを開発したことがある * SQLに関する知識がある | 検証環境 -------------------------------------------------------------------------------- このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。 .. tabularcolumns:: |p{0.30\linewidth}|p{0.70\linewidth}| .. list-table:: :header-rows: 1 :widths: 30 70 * - 種別 - 名前 * - OS - Windows 11 * - JVM - \ :url_redhat_openjdk:`Java <>`\ 17 * - IDE - \ :url_spring_io:`Spring Tool Suite `\ |sts_version| (以降「STS」と呼ぶ。設定方法は\ :doc:`../Appendix/SpringToolSuite`\ を参照されたい。) * - Build Tool - \ :url_maven:`Apache Maven `\ |maven_version| (以降「Maven」と呼ぶ) * - Application Server - \ :url_tomcat:`Apache Tomcat `\ |tomcat_version| * - Web Browser - \ :url_chrome:`Google Chrome `\ |chrome_version| | 作成するアプリケーションの説明 ================================================================================ | 本チュートリアルでは、ViewとしてThymeleafを使用して開発するメリットを体感できるよう、最初にHTMLで画面デザインのみ実装したモックアップ(以降、プロトタイプと呼ぶ)を作成し、そこにアプリケーションの機能を追加していく。 | 本ガイドラインでは、HTMLで作成したプロトタイプにThymeleafの属性を付与してテンプレート化したものを「テンプレートHTML」と呼ぶ。 | .. _tutorial-todo-application-overview-label_Thymeleaf: アプリケーションの概要 -------------------------------------------------------------------------------- TODOを管理するアプリケーションを作成する。TODOの一覧表示、TODOの登録、TODOの完了、TODOの削除を行える。 .. figure:: ./images_Thymeleaf/image001.png :width: 50% | .. _app-requirement_Thymeleaf: アプリケーションの業務要件 -------------------------------------------------------------------------------- アプリケーションの業務要件は、以下の通りとする。 .. tabularcolumns:: |p{0.20\linewidth}|p{0.80\linewidth}| .. list-table:: :header-rows: 1 :widths: 20 80 * - ルールID - 説明 * - B01 - 未完了のTODOは5件までしか登録できない * - B02 - 完了済みのTODOは完了できない .. note:: 本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。 | アプリケーションの処理仕様 -------------------------------------------------------------------------------- アプリケーションの処理仕様と画面遷移は、以下の通りとする。 .. figure:: ./images_Thymeleaf/image002.png :width: 60% .. tabularcolumns:: |p{0.10\linewidth}|p{0.20\linewidth}|p{0.10\linewidth}|p{0.15\linewidth}|p{0.45\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 20 10 15 45 * - 項番 - プロセス名 - HTTPメソッド - URL - 備考 * - 1 - Show all TODO - \- - /todo/list - * - 2 - Create TODO - POST - /todo/create - 作成処理終了後、Show all TODOへリダイレクト * - 3 - Finish TODO - POST - /todo/finish - 完了処理終了後、Show all TODOへリダイレクト * - 4 - Delete TODO - POST - /todo/delete - 削除処理終了後、Show all TODOへリダイレクト | Show all TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * TODOを全件表示する * 未完了のTODOに対しては「Finish」と「Delete」用のボタンが付く * 完了のTODOは打ち消し線で装飾する * TODOの件名のみ表示する | Create TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * フォームから送信されたTODOを保存する * TODOの件名は1文字以上30文字以下であること * \ :ref:`app-requirement_Thymeleaf`\ のB01を満たさない場合はエラーコードE001でビジネス例外をスローする * 処理が成功した場合は、遷移先の画面で「Created successfully!」を表示する | Finish TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * フォームから送信された\ ``todoId``\ に対応するTODOを完了済みにする * 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする * \ :ref:`app-requirement_Thymeleaf`\ のB02を満たさない場合はエラーコードE002でビジネス例外をスローする * 処理が成功した場合は、遷移先の画面で「Finished successfully!」を表示する | Delete TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * フォームから送信された\ ``todoId``\ に対応するTODOを削除する * 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする * 処理が成功した場合は、遷移先の画面で「Deleted successfully!」を表示する | エラーメッセージ一覧 -------------------------------------------------------------------------------- エラーメッセージとして、以下の3つを定義する。 .. tabularcolumns:: |p{0.15\linewidth}|p{0.50\linewidth}|p{0.35\linewidth}| .. list-table:: :header-rows: 1 :widths: 15 50 35 * - エラーコード - メッセージ - 置換パラメータ * - E001 - [E001] The count of un-finished Todo must not be over {0}. - {0}… max unfinished count * - E002 - [E002] The requested Todo is already finished. (id={0}) - {0}… todoId * - E404 - [E404] The requested Todo is not found. (id={0}) - {0}… todoId | 環境構築 ================================================================================ 本チュートリアルでは、インフラストラクチャ層のRepositoryImplの実装として、 * データベースを使用せず\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImpl * MyBatis3を使用してデータベースにアクセスするRepositoryImpl の2種類を用意している。用途に応じていずれかを選択する。 チュートリアルの進行上、まずはインメモリ実装を試し、その後MyBatis3を選ぶのが円滑である。 | プロジェクトの作成 -------------------------------------------------------------------------------- 本チュートリアルを順序通り読み進める場合は、以下の\ ``mvn archetype:generate``\ コマンドを実行し、O/R Mapperに依存しないブランクプロジェクトを作成すること。 .. tabs:: .. group-tab:: Java Config .. code-block:: console mvn archetype:generate -B^ -DarchetypeGroupId=com.github.macchinetta.blank^ -DarchetypeArtifactId=macchinetta-web-blank-thymeleaf-archetype^ -DarchetypeVersion=1.11.1.RELEASE^ -DgroupId=com.example.todo^ -DartifactId=todo^ -Dversion=1.0.0-SNAPSHOT .. group-tab:: XML Config .. code-block:: console mvn archetype:generate -B^ -DarchetypeGroupId=com.github.macchinetta.blank^ -DarchetypeArtifactId=macchinetta-web-blank-xmlconfig-thymeleaf-archetype^ -DarchetypeVersion=1.11.1.RELEASE^ -DgroupId=com.example.todo^ -DartifactId=todo^ -Dversion=1.0.0-SNAPSHOT プロジェクトの作成についての詳細は、「\ :ref:`BlankProjectCreateBlankProject`\ 」を参照されたい。 | プロジェクトのインポート -------------------------------------------------------------------------------- 作成したブランクプロジェクトをSTSへインポートする。 プロジェクトのインポートについては、「\ :ref:`BlankProjectBlankProjectIntoSts`\ 」を参照されたい。 | プロジェクトの構成 -------------------------------------------------------------------------------- 本チュートリアルで作成するプロジェクトの構成については、「\ :ref:`BlankProjectProjectNotRelyingOnOrMapper`\ 」を参照されたい。 .. note:: \ :ref:`前節の「プロジェクト構成」 `\ ではマルチプロジェクトにすることを推奨していたが、本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。 \ **ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。**\ マルチプロジェクトの作成方法は、「\ :ref:`BlankProjectCreateBlankProject`\ 」を参照されたい。 | 設定ファイルの確認 -------------------------------------------------------------------------------- チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。 チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。 アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「\ :ref:`BlankProjectCheckConfigurationFile`\ 」を参照されたい。 .. note:: まず、手を動かしてTodoアプリケーションを作成したい場合は、設定ファイルの確認は読み飛ばしてもよいが、Todoアプリケーションを作成した後に一読して頂きたい。 | プロジェクトの動作確認 -------------------------------------------------------------------------------- Todoアプリケーションの開発を始める前に、プロジェクトの動作確認を行う。 プロジェクトの動作確認の手順については、「\ :ref:`BlankProjectVerifyOperationOfBlankProject`\ 」を参照されたい。 .. _create-prototype-of-tutorial-todo-label_Thymeleaf: Todoアプリケーションのプロトタイプ作成 ================================================================================ HTMLでTodoアプリケーションのプロトタイプを作成する。 本チュートリアルでは、ここで作成したプロトタイプにThymeleafの属性を付与して、Todoアプリケーションの画面を実装していく。 | プロトタイプ作成 -------------------------------------------------------------------------------- \ :ref:`tutorial-todo-application-overview-label_Thymeleaf`\ で示した画面をプロトタイプとして作成する。 .. figure:: ./images_Thymeleaf/image001.png :width: 40% .. note:: \ **実際のアプリケーション開発で作成するプロトタイプ**\ 実際のアプリケーション開発では、ユースケースごとに画面の状態が確認できるプロトタイプ(本チュートリアルの例では、「TODOを作成した状態」や「TODOを完了した状態」など)を作成するのが一般的だと思われるが、今回はThymeleafを使用したアプリケーションの作成を学ぶチュートリアルで、プロトタイプの正しい作り方を解説することは主眼ではないため、省略する。 また、プロトタイプをブランクプロジェクトベースで作成するかは開発プロジェクトの判断に任せるが、本チュートリアルでは、プロトタイプからアプリケーションを開発する工程を理解しやすいように、ブランクプロジェクトベースでプロトタイプを作成している。 Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Enter or select the parent folder - \ ``todo/src/main/webapp/WEB-INF/views/todo``\ * - 2 - File name - \ ``list.html``\ を入力して「Finish」する。 作成したファイルは以下のディレクトリに格納される。 .. figure:: ./images_Thymeleaf/create-list-jsp.png \ :ref:`tutorial-todo-application-overview-label_Thymeleaf`\ で示した画面をHTMLとして表示するために必要なプロトタイプの実装を行う。 .. code-block:: html Todo List

Todo List


.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 新規作成処理用のformを表示する。 | \ ``action``\ 属性には新規作成処理を実行するためのパス(\ ``/todo/create``\ )を指定する。 | 新規作成処理は更新系の処理なので、\ ``method``\ 属性には\ ``POST``\ メソッドを指定する。 * - | (2) - | 未完了のTODOに対しては「Finish」と「Delete」用のボタンを表示する。 | \ ``action``\ 属性には更新処理、削除処理を実行するためのパス(\ ``/todo/finish``\ or \ ``/todo/delete``\ )を指定する。 | 更新処理、削除処理は更新系の処理なので、\ ``method``\ 属性には\ ``POST``\ メソッドを指定する。 | なお、「Finish」と「Delete」用のボタンをインラインブロック要素(\ ``display: inline-block;``\ )としてTODOの横に表示させている。 * - | (3) - | 完了しているTODOには、打ち消し線(\ ``text-decoration: line-through;``\ )を装飾する。 | 完了しているTODOに対しては「Delete」用のボタンのみを表示する。 | 画面の静的表示の確認 -------------------------------------------------------------------------------- 作成したプロトタイプのデザインをWebブラウザで確認すると、以下のように表示される。(以降、プロトタイプやテンプレートHTMLをブラウザで直接開く事を静的表示と呼ぶ。) .. figure:: ./images_Thymeleaf/image001.png :width: 40% | CSSファイルの使用 -------------------------------------------------------------------------------- 上記例ではスタイルシートをHTMLファイルの中で直接定義していたが、実際のアプリケーションを開発する場合は、CSSファイルに定義するのが一般的である。 ここでは、スタイルシートをCSSファイルに定義する方法について説明する。 ブランクプロジェクトから提供しているCSSファイル(\ ``src/main/webapp/resources/app/css/styles.css``\ )にスタイルシートの定義を追加する。 なお、ここでは、以降で使用するスタイルシートも含めて、CSSファイルに定義している。 .. code-block:: css /* ... */ .strike { text-decoration: line-through; } .inline { display: inline-block; } .alert { border: 1px solid; margin-bottom: 5px; } .alert-error { background-color: #c60f13; border-color: #970b0e; color: white; } .alert-success { background-color: #5da423; border-color: #457a1a; color: white; } .text-error { color: #c60f13; } .alert ul { margin: 15px 0px 15px 0px; } #todoList li { margin-top: 5px; } | プロトタイプからCSSファイルを読み込む。 .. code-block:: html Todo List

Todo List


.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | HTMLからスタイルシートの定義を削除し、代わりにスタイルシートを定義したCSSファイルを読み込む。 | CSSファイルを適用すると、以下のようなレイアウトになる。 .. figure:: ./images_Thymeleaf/list-screen-css.png :width: 40% | Todoアプリケーションの作成 ================================================================================ | プロトタイプからTodoアプリケーションを作成する。作成する順は、以下の通りである。 * ドメイン層(+ インフラストラクチャ層) * Domain Object作成 * Repository作成 * RepositoryImpl作成 * Service作成 * アプリケーション層 * Controller作成 * Form作成 * View作成 | RepositoryImplの作成は、選択したインフラストラクチャ層の種類に応じて実装方法が異なる。 | ここでは、データベースを使用せず\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImplを作成する方法について説明を行う。 | データベースを使用する場合は、「:ref:`tutorial-todo_infra_Thymeleaf`」に記載されている内容で読み替えて、Todoアプリケーションを作成して頂きたい。 | ドメイン層の作成 -------------------------------------------------------------------------------- Domain Objectの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Domainオブジェクトを作成する。 Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.domain.model``\ * - 2 - Name - \ ``Todo``\ * - 3 - Interfaces - \ ``java.io.Serializable``\ を入力して「Finish」する。 .. figure:: ./images_Thymeleaf/image057.png :width: 70% 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image058_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image058_XMLConfig.png | 作成したクラスに以下のプロパティを追加する。 * ID → todoId * タイトル → todoTitle * 完了フラグ → finished * 作成日 → createdAt .. code-block:: java package com.example.todo.domain.model; import java.io.Serializable; import java.time.LocalDate; public class Todo implements Serializable { private static final long serialVersionUID = 1L; private String todoId; private String todoTitle; private boolean finished; private LocalDate createdAt; public String getTodoId() { return todoId; } public void setTodoId(String todoId) { this.todoId = todoId; } public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } public boolean isFinished() { return finished; } public void setFinished(boolean finished) { this.finished = finished; } public LocalDate getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDate createdAt) { this.createdAt = createdAt; } } .. tip:: Getter/SetterメソッドはSTSの機能を使って自動生成することができる。フィールドを定義した後、エディタ上で右クリックし、「Source」->「Generate Getters and Setters…」を選択する。 .. figure:: ./images_Thymeleaf/image059.png :width: 90% serialVersionUID以外を選択して「Generate」 .. figure:: ./images_Thymeleaf/image060.png :width: 60% | .. _TutorialTodoCreateRepository_Thymeleaf: Repositoryの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | \ ``TodoRepository``\ インタフェースを作成する。 | データベースを使用する場合は、「\ :ref:`tutorial-todo_infra_Thymeleaf`\ 」に記載されている内容で読み替えて、Repositoryを作成する。 Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.domain.repository.todo``\ * - 2 - Name - \ ``TodoRepository``\ を入力して「Finish」する。 作成したインタフェースは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image061_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image061_XMLConfig.png 作成したインタフェースに、今回のアプリケーションで必要となる以下のCRUD操作を行うメソッドを定義する。 * TODOの1件取得 → findById * TODOの全件取得 → findAll * TODOの1件作成 → create * TODOの1件更新 → update * TODOの1件削除 → delete * 完了済みTODO件数の取得 → countByFinished .. code-block:: java package com.example.todo.domain.repository.todo; import java.util.Collection; import com.example.todo.domain.model.Todo; public interface TodoRepository { Todo findById(String todoId); Collection findAll(); void create(Todo todo); boolean update(Todo todo); void delete(Todo todo); long countByFinished(boolean finished); } .. note:: ここでは、\ ``TodoRepository``\ の汎用性を上げるため、「完了済み件数を取得する」メソッド(\ ``long countFinished()``\ )ではなく、「完了状態がxxである件数を取得する」メソッド(\ ``long countByFinished(boolean)``\ )として定義している。 \ ``long countByFinished(boolean)``\ の引数として\ ``true``\ を渡すと「完了済みの件数」、\ ``false``\ を渡すと「未完了の件数」が取得できる仕様としている。 | RepositoryImplの作成(インフラストラクチャ層) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ここでは、説明を単純化するため、\ ``java.util.Map``\ を使ったインメモリ実装のRepositoryImplを作成する。 | データベースを使用する場合は、「\ :ref:`tutorial-todo_infra_Thymeleaf`\ 」に記載されている内容で読み替えて、RepositoryImplを作成する。 Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.domain.repository.todo``\ * - 2 - Name - \ ``TodoRepositoryImpl``\ * - 3 - Interfaces - \ ``com.example.todo.domain.repository.todo.TodoRepository``\ を入力して「Finish」する。 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image062_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image062_XMLConfig.png 作成したクラスにCRUD操作を実装する。 .. note:: RepositoryImplには、業務ロジックは含めず、Domainオブジェクトの保存先への出し入れ(CRUD操作)に終始することが実装ポイントである。 .. code-block:: java package com.example.todo.domain.repository.todo; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Repository; import com.example.todo.domain.model.Todo; @Repository // (1) public class TodoRepositoryImpl implements TodoRepository { private static final Map TODO_MAP = new ConcurrentHashMap<>(); @Override public Todo findById(String todoId) { return TODO_MAP.get(todoId); } @Override public Collection findAll() { return TODO_MAP.values(); } @Override public void create(Todo todo) { TODO_MAP.put(todo.getTodoId(), todo); } @Override public boolean update(Todo todo) { TODO_MAP.put(todo.getTodoId(), todo); return true; } @Override public void delete(Todo todo) { TODO_MAP.remove(todo.getTodoId()); } @Override public long countByFinished(boolean finished) { long count = 0; for (Todo todo : TODO_MAP.values()) { if (finished == todo.isFinished()) { count++; } } return count; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 80 * - 項番 - 説明 * - | (1) - | Repositoryとしてcomponent-scan対象とするため、クラスレベルに\ ``@Repository``\ アノテーションをつける。 .. note:: 本チュートリアルでは、インフラストラクチャ層に属するクラス(RepositoryImpl)をドメイン層のパッケージ(\ ``com.example.todo.domain``\ )に格納しているが、完全に層別にパッケージを分けるのであれば、インフラストラクチャ層のクラスは、\ ``com.example.todo.infra``\ 以下に作成した方が良い。 ただし、通常のプロジェクトでは、インフラストラクチャ層が変更されることを前提としていない(そのような前提で進めるプロジェクトは、少ない)。そこで、作業効率向上のために、ドメイン層のRepositoryインタフェースと同じ階層に、RepositoryImplを作成しても良い。 | Serviceの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ まず、\ ``TodoService``\ インタフェースを作成する。 Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.domain.service.todo``\ * - 2 - Name - \ ``TodoService``\ を入力して「Finish」する。 作成したインタフェースは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image063_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image063_XMLConfig.png 作成したインタフェースに以下の業務処理を行うメソッドを定義する。 * Todoの全件取得 → findAll * Todoの新規作成 → create * Todoの完了 → finish * Todoの削除 → delete .. code-block:: java package com.example.todo.domain.service.todo; import java.util.Collection; import com.example.todo.domain.model.Todo; public interface TodoService { Collection findAll(); Todo create(Todo todo); Todo finish(String todoId); void delete(String todoId); } | 次に、\ ``TodoService``\ インタフェースに定義したメソッドを実装する\ ``TodoServiceImpl``\ クラスを作成する。 Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.domain.service.todo``\ * - 2 - Name - \ ``TodoServiceImpl``\ * - 3 - Interfaces - \ ``com.example.todo.domain.service.todo.TodoService``\ を入力して「Finish」する。 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image064_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image064_XMLConfig.png .. code-block:: java package com.example.todo.domain.service.todo; import java.time.LocalDate; import java.util.Collection; import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.terasoluna.gfw.common.exception.BusinessException; import org.terasoluna.gfw.common.exception.ResourceNotFoundException; import org.terasoluna.gfw.common.message.ResultMessage; import org.terasoluna.gfw.common.message.ResultMessages; import com.example.todo.domain.model.Todo; import com.example.todo.domain.repository.todo.TodoRepository; import jakarta.inject.Inject; @Service // (1) @Transactional // (2) public class TodoServiceImpl implements TodoService { private static final long MAX_UNFINISHED_COUNT = 5; @Inject // (3) TodoRepository todoRepository; @Override @Transactional(readOnly = true) // (4) public Collection findAll() { return todoRepository.findAll(); } @Override public Todo create(Todo todo) { long unfinishedCount = todoRepository.countByFinished(false); if (unfinishedCount >= MAX_UNFINISHED_COUNT) { // (5) ResultMessages messages = ResultMessages.error(); messages.add( ResultMessage.fromText("[E001] The count of un-finished Todo must not be over " + MAX_UNFINISHED_COUNT + ".")); // (6) throw new BusinessException(messages); } // (7) String todoId = UUID.randomUUID().toString(); LocalDate createdAt = LocalDate.now(); todo.setTodoId(todoId); todo.setCreatedAt(createdAt); todo.setFinished(false); todoRepository.create(todo); return todo; } @Override public Todo finish(String todoId) { Todo todo = findOne(todoId); if (todo.isFinished()) { ResultMessages messages = ResultMessages.error(); messages.add(ResultMessage.fromText( "[E002] The requested Todo is already finished. (id=" + todoId + ")")); throw new BusinessException(messages); } todo.setFinished(true); todoRepository.update(todo); return todo; } @Override public void delete(String todoId) { Todo todo = findOne(todoId); todoRepository.delete(todo); } // (8) private Todo findOne(String todoId) { Todo todo = todoRepository.findById(todoId); if (todo == null) { ResultMessages messages = ResultMessages.error(); messages.add(ResultMessage .fromText("[E404] The requested Todo is not found. (id=" + todoId + ")")); // (9) throw new ResourceNotFoundException(messages); } return todo; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 :class: longtable * - 項番 - 説明 * - | (1) - | Serviceとしてcomponent-scanの対象とするため、クラスレベルに\ ``@Service``\ アノテーションをつける。 * - | (2) - | クラスレベルに、\ ``@Transactional``\ アノテーションをつけることで、公開メソッドをすべてトランザクション管理する。 | アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションのコミットが行われる。 | また、途中で非検査例外が発生した場合は、トランザクションがロールバックされる。 | | データベースを使用しない場合は、\ ``@Transactional``\ アノテーションは不要である。 * - | (3) - | \ ``@Inject``\ アノテーションで、\ ``TodoRepository``\ の実装をインジェクションする。 * - | (4) - | 参照のみ行う処理に関しては、\ ``readOnly=true``\ をつける。 | O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる。 | | データベースを使用しない場合は、\ ``@Transactional``\ アノテーションは不要である。 * - | (5) - | 結果メッセージを格納するクラスとして、共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.message.ResultMessage``\ を用いる。 | 今回は、エラーメッセージを例外に追加する際に、\ ``ResultMessages.error()``\ でメッセージ種別を指定して、\ ``ResultMessage``\ を追加している。 * - | (6) - | 業務エラーが発生した場合、共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.exception.BusinessException``\ をスローする。 * - | (7) - | 一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。 * - | (8) - | 1件取得は、\ ``finish``\ メソッドでも\ ``delete``\ メソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。 * - | (9) - | 対象のデータが存在しない場合、共通ライブラリで用意されている\ ``org.terasoluna.gfw.common.exception.ResourceNotFoundException``\ をスローする。 .. note:: 本節では、説明を単純化するため、エラーメッセージをハードコードしているが、メンテナンスの観点で本来は好ましくない。通常、メッセージは、プロパティファイルに外部化することが推奨される。 プロパティファイルに外部化する方法は、\ :doc:`../ArchitectureInDetail/GeneralFuncDetail/PropertyManagement`\ を参照されたい。 | アプリケーション層の作成 -------------------------------------------------------------------------------- | ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。 | 画面(テンプレートHTML)には、プロトタイプとして作成したHTMLファイルを使用する。 | Controllerの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ まずは、Todo管理業務にかかわる画面遷移を、制御するControllerを作成する。 Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.app.todo``\ * - 2 - Name - \ ``TodoController``\ を入力して「Finish」する。 .. note:: \ **上位パッケージがドメイン層と異なるので注意すること。**\ 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image065_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image065_XMLConfig.png .. code-block:: java package com.example.todo.app.todo; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller // (1) @RequestMapping("todo") // (2) public class TodoController { } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | Controllerとしてcomponent-scanの対象とするため、クラスレベルに、\ ``@Controller``\ アノテーションをつける。 * - | (2) - | \ ``TodoController``\ が扱う画面遷移のパスを、すべて\ ``/todo``\ 配下にするため、クラスレベルに\ ``@RequestMapping(“todo”)``\ を設定する。 | Show all TODOの実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 本チュートリアルで作成する画面では、 * 新規作成フォームの表示 * TODOの全件表示 を行う。 はじめに、TODOの全件表示を行うための処理を実装する。 | Formの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Formクラス(JavaBean)を作成する。 Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.app.todo``\ * - 2 - Name - \ ``TodoForm``\ * - 3 - Interfaces - \ ``java.io.Serializable``\ を入力して「Finish」する。 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/image066_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/image066_XMLConfig.png 作成したクラスに以下のプロパティを追加する。 * タイトル → todoTitle .. code-block:: java package com.example.todo.app.todo; import java.io.Serializable; public class TodoForm implements Serializable { private static final long serialVersionUID = 1L; private String todoTitle; public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } } | Controllerの実装 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 一覧画面表示処理を\ ``TodoController``\ に追加する。 .. code-block:: java package com.example.todo.app.todo; import java.util.Collection; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import com.example.todo.domain.model.Todo; import com.example.todo.domain.service.todo.TodoService; import jakarta.inject.Inject; @Controller @RequestMapping("todo") public class TodoController { @Inject // (1) TodoService todoService; @ModelAttribute // (2) public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @GetMapping("list") // (3) public String list(Model model) { Collection todos = todoService.findAll(); model.addAttribute("todos", todos); // (4) return "todo/list"; // (5) } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``TodoService``\ を、DIコンテナによってインジェクションさせるために、\ ``@Inject``\ アノテーションをつける。 | | DIコンテナの管理する\ ``TodoService``\ 型のインスタンス(\ ``TodoServiceImpl``\ のインスタンス)がインジェクションされる。 * - | (2) - | Formを初期化する。 | | \ ``@ModelAttribute``\ アノテーションをつけることで、このメソッドの返り値のformオブジェクトが、\ ``todoForm``\ という名前で\ ``Model``\ に追加される。 | これは、\ ``TodoController``\ の各処理で、\ ``model.addAttribute("todoForm", form)``\ を実装するのと同義である。 * - | (3) - | \ ``/todo/list``\ というパスに\ ``GET``\ メソッドを使用してリクエストされた際に、一覧画面表示処理用のメソッド(\ ``list``\ メソッド)が実行されるように\ ``@GetMapping``\ アノテーションを設定する。 | | クラスレベルに\ ``@RequestMapping(“todo”)``\ が設定されているため、ここでは\ ``@GetMapping("list")``\ のみで良い。 * - | (4) - | \ ``Model``\ にTodoのリストを追加して、Viewに渡す。 * - | (5) - | View名として\ ``todo/list``\ を返すと、spring-mvc.xmlまたはSpringMvcConfig.javaに定義した\ ``ViewResolver``\ の設定によりテンプレートHTMLとして\ :file:`WEB-INF/views/todo/list.html`\を利用して生成したHTMLが返される。 .. note:: \ ``@GetMapping``\ や\ ``@PostMapping``\ は、対応するHTTPメソッドにマッピングする。 詳細は、\ :ref:`controller_mapping-label`\ を参照されたい。 | テンプレートHTMLの実装 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" \ :ref:`create-prototype-of-tutorial-todo-label_Thymeleaf`\ で作成したプロトタイプにThymeleafの属性を付与してテンプレートHTMLを実装し、Controllerから渡されたModelを表示する。 \ ``list.html``\ にTODOの一覧表示エリアを表示するために必要なテンプレートHTMLの実装を行う。 .. code-block:: html Todo List

Todo List


  • Send a e-mail
  • Have a lunch
  • Read a book
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | Thymeleaf独自の属性を使用するため、\ ````\ タグにThymeleafのネームスペースを付与する。 * - | (2) - | \ ````\ タグに\ ``th:href``\ 属性を付与する。 | \ ``th:href``\ 属性値には、リンクURL式 \ ``@{}``\ を用いている。 | リンクURL式に"\ ``/``\ "(スラッシュ)から始まるパスを指定することで、コンテキストルートからの相対パスが出力される。 * - | (3) - | 最初の子要素をThymeleafのテンプレートとして利用し、2番目以降の子要素は静的表示時のみに表示するために、Thymeleafの\ ``th:remove``\ 属性を使用する。 | \ ``th:remove``\ 属性に\ ``all-but-first``\ を指定することで、Thymeleafでの処理時には、指定したタグにおける最初の子要素以外の要素が削除される。 * - | (4) - | \ ``th:each``\ 属性の右項にはControllerでModelに追加したコレクション\ ``todos``\ を指定し、左項にはコレクションの要素オブジェクトを格納する変数名\ ``todo``\ を指定している。 | これにより、\ ``th:each``\ 属性を付与した配下の要素が\ ``todos``\ の要素数分繰り返し出力される。 * - | (5) - | \ ``th:class``\ 属性を使用することで、動的に\ ``class``\ 属性を設定できる。 | \ ``th:text``\ 属性と同様に、変数式を利用してModelに登録した変数や\ ``th:each``\ 属性で定義した変数を参照できる。 | ここではEL式を利用して、\ ``th:each``\ 属性で取り出した\ ``Todo``\ 型オブジェクト\ ``todo``\ の\ ``finished``\ プロパティを参照して打ち消し線(\ ``text-decoration: line-through;``\ )を装飾するかどうかを判断する。 * - | (6) - | \ ``th:text``\ 属性を使用することで、記述した要素のコンテンツを属性値で上書きする。 | \ **文字列値を出力する際は、XSS対策のため、必ずth:text属性を使用してHTMLエスケープを行うこと。**\ | XSS対策についての詳細は、\ :ref:`xss_how_to_use_ouput_escaping`\ を参照されたい。 * - | (7) - | \ ``th:if``\ 属性は条件に応じて、要素を出力するかどうか制御するための属性であり、\ ``todo``\ の\ ``finished``\ プロパティを参照して「Finish」ボタンの生成を判断する。 .. note:: Thymeleafの\ ``th:object``\ 属性を用いると、オブジェクト名を省略してプロパティを指定することが出来る。 list.htmlの\ ``
  • ``\ タグの部分は、\ ``th:object``\ 属性を用いることで以下のように記述量を減らすことが出来る。 * \ ``list.html``\ .. code-block:: html
  • Send a e-mail
  • .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``th:object``\ 属性にオブジェクトを変数式\ ``${}``\ で指定する。 * - | (2) - | オブジェクトのプロパティを選択変数式\ ``*{}``\ で指定する。これは、変数式を用いて\ ``th:class="${todo.finished} ? 'strike'"``\ や\ ``th:text="${todo.todoTitle}"``\ と指定するのと同じ結果になる。 | | STSで「todo」プロジェクトを右クリックし、「Run As」→「Run on Server」でWebアプリケーションを起動する。 | ブラウザで\ :url_localhost:`/todo/todo/list`\ にアクセスすると、以下のような画面が表示される。 .. figure:: ./images_Thymeleaf/image067.png :width: 25% なお、表示されている「Create Todo」ボタンについては、「Create TODO」の実装が終了していないため、表示はされるが機能しない。 | .. note:: 上記で表示されている画面には、TODOが1件も登録されていないため、TODOの一覧は出力されない。 以下のように、ドメイン層の作成で作成したTodoRepositoryImplを一時的に修正し初期データを登録することで、TODOの一覧が出力されることを確認できる。 なお、次節「\ :ref:`CreateTodoImplementation_Thymeleaf`\ 」で実際にTODOを登録できるようになるため、一覧の出力が確認できたら削除して構わない。 * \ ``TodoRepositoryImpl.java``\ .. code-block:: java package com.example.todo.domain.repository.todo; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Repository; import com.example.todo.domain.model.Todo; @Repository public class TodoRepositoryImpl implements TodoRepository { private static final Map TODO_MAP = new ConcurrentHashMap(); static { Todo todo1 = new Todo(); todo1.setTodoId("1"); todo1.setTodoTitle("Send a e-mail"); Todo todo2 = new Todo(); todo2.setTodoId("2"); todo2.setTodoTitle("Have a lunch"); Todo todo3 = new Todo(); todo3.setTodoId("3"); todo3.setTodoTitle("Read a book"); todo3.setFinished(true); TODO_MAP.put(todo1.getTodoId(), todo1); TODO_MAP.put(todo2.getTodoId(), todo2); TODO_MAP.put(todo3.getTodoId(), todo3); } // omitted 以下のように画面に出力される。 .. figure:: ./images_Thymeleaf/show-all-todo-note.png :width: 30% | .. _CreateTodoImplementation_Thymeleaf: Create TODOの実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 次に、一覧表示画面から「Create TODO」ボタンを押した後の、新規作成処理を実装する。 | マッパーインタフェースの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Beanマッピングのマッパーインタフェースを作成する。 Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Package - \ ``com.example.todo.app.todo``\ * - 2 - Name - \ ``TodoMapper``\ を入力して「Finish」する。 作成したクラスは以下のディレクトリに格納される。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_Thymeleaf/create-bean-mapper_JavaConfig.png .. group-tab:: XML Config .. figure:: ./images_Thymeleaf/create-bean-mapper_XMLConfig.png 作成したクラスに以下の\ ``@Mapper``\ アノテーションを付与したBeanマッピングメソッドを追加する。 * Todo map(TodoForm form) * \ ``@Mapping``\ アノテーションによるマッピング除外項目定義 * createdAt * finished .. code-block:: java package com.example.todo.app.todo; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import com.example.todo.domain.model.Todo; @Mapper public interface TodoMapper { @Mapping(target = "createdAt", ignore = true) @Mapping(target = "finished", ignore = true) Todo map(TodoForm form); } .. note:: マッパーインタフェース追加後、以下のようなビルドエラーが発生する場合がある。 .. code-block:: console Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.todo.app.todo.TodoMapper' この場合は、プロジェクト名を右クリックし、「Run As」->「Maven build」をクリックする。 Goalsに「compile」を指定し「Run」をクリックする。 .. figure:: ./images_Thymeleaf/mvnBuild.png :width: 40% ビルドが成功した後、プロジェクト名を右クリックし、「Run As」->「Maven install」をクリックする。 | Controllerの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 新規作成処理を\ ``TodoController``\ に追加する。 .. code-block:: java package com.example.todo.app.todo; import java.util.Collection; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.terasoluna.gfw.common.exception.BusinessException; import org.terasoluna.gfw.common.message.ResultMessage; import org.terasoluna.gfw.common.message.ResultMessages; import com.example.todo.domain.model.Todo; import com.example.todo.domain.service.todo.TodoService; import jakarta.inject.Inject; import jakarta.validation.Valid; @Controller @RequestMapping("todo") public class TodoController { @Inject TodoService todoService; // (1) @Inject TodoMapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @GetMapping("list") public String list(Model model) { Collection todos = todoService.findAll(); model.addAttribute("todos", todos); return "todo/list"; } @PostMapping("create") // (2) public String create(@Valid TodoForm todoForm, BindingResult bindingResult, // (3) Model model, RedirectAttributes attributes) { // (4) // (5) if (bindingResult.hasErrors()) { return list(model); } // (6) Todo todo = beanMapper.map(todoForm); try { todoService.create(todo); } catch (BusinessException e) { // (7) model.addAttribute(e.getResultMessages()); return list(model); } // (8) attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Created successfully!"))); return "redirect:/todo/list"; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 :class: longtable * - 項番 - 説明 * - | (1) - | FormオブジェクトをDomainObjectに変換するために、\ ``TodoMapper``\ インタフェースをインジェクションする。 * - | (2) - | \ ``/todo/create``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、新規作成処理用のメソッド(\ ``create``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。 * - | (3) - | フォームの入力チェックを行うため、Formの引数に\ ``@Valid``\ アノテーションをつける。入力チェック結果は、その直後の引数\ ``BindingResult``\ に格納される。 * - | (4) - | 正常に作成が完了した後にリダイレクトし、一覧画面を表示する。 | リダイレクト先への情報を格納するために、引数に\ ``RedirectAttributes``\ を加える。 * - | (5) - | 入力エラーがあった場合、一覧画面に戻る。 | Todo全件取得を再度行う必要があるので、\ ``list``\ メソッドを再実行する。 * - | (6) - | \ ``Mapstruct``\ を用いて、\ ``TodoForm``\ オブジェクトから\ ``Todo``\ オブジェクトを作成する。 * - | (7) - | 業務処理を実行して、\ ``BusinessException``\ が発生した場合、結果メッセージを\ ``Model``\ に追加して、一覧画面に戻る。 * - | (8) - | 正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。 | リダイレクトすることにより、ブラウザを再読み込みして、再び新規登録処理が\ ``POST``\ されることがなくなる。(詳しくは、「\ :ref:`DoubleSubmitProtectionAboutPRG`\ 」を参照されたい) | なお、今回は成功メッセージであるため、\ ``ResultMessages.success()``\ を使用している。 | Formの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 入力チェックのルールを定義するため、Formオブジェクトにアノテーションを追加する。 .. code-block:: java package com.example.todo.app.todo; import java.io.Serializable; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class TodoForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull // (1) @Size(min = 1, max = 30) // (2) private String todoTitle; public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 80 * - 項番 - 説明 * - | (1) - | \ ``@NotNull``\ アノテーションを使用して必須チェックを有効化する。 * - | (2) - | \ ``@Size``\ アノテーションを使用して文字数チェックを有効化する。 | テンプレートHTMLの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" \ ``list.html``\ にTODOを新規作成するために必要なテンプレートHTMLの実装を行う。 TODOを新規作成するため、テンプレートHTMLに以下の実装を追加する。 * TODOの入力フォームにThymeleafの属性を付与する * 入力チェックエラーを表示するエリアを追加する * 結果メッセージを表示するエリアを追加する .. code-block:: html Todo List

    Todo List

    • Created successfully!
    size must be between 1 and 30

    • Send a e-mail
    • Have a lunch
    • Read a book
    .. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 80 * - 項番 - 説明 * - | (1) - | 新規作成処理の結果メッセージを表示する。 | \ ``th:if``\ 属性を使用し、ServiceやControllerで\ ``resultMessages``\ オブジェクトがModelに登録されている場合のみ、結果メッセージを表示している。 | また、\ ``th:class``\ 属性を使用することで、\ ``ResultMessages``\ に設定されたメッセージタイプ(例:\ ``info``\ ,\ ``error``\ )に応じた\ ``class``\ 属性を設定している。 .. note:: 一般的にThymeleafを利用して画面を実装する場合、HTMLファイルを直接ブラウザで表示することを考慮し、Thymeleafのテンプレートとしては不要だがHTML表示時に必要となる属性や文字列(コード例における\ ``class="alert alert-success"``\ や\ ``Created successfully!``\ )を記述する。 * - | (2) - | 新規作成処理用のformを実装する。 | \ ``th:action``\ 属性には、リンクURL式 \ ``@{}``\ を用いて新規作成処理を実行するためのパス(\ ``/todo/create``\ )を指定する。 * - | (3) - | \ ````\ タグでフォームのプロパティをバインドする。 | \ ``th:field``\ 属性値を\ ````\ タグに適用すると、\ ``id``\ 属性、\ ``name``\ 属性、\ ``value``\ 属性が付加される。 * - | (4) - | \ ``th:errors``\ 属性を付与することで、指定したプロパティに対する入力エラーがあった場合に表示される。\ ``th:errors``\ 属性の値は、\ ````\ タグの\ ``th:field``\ 属性と合わせる。 | フォームに適切な値を入力してsubmitすると、以下のように、成功メッセージが表示される。 .. figure:: ./images_Thymeleaf/image068.png :width: 40% .. figure:: ./images_Thymeleaf/image069.png :width: 40% なお、TODOの横に表示されている「Finish」、「Delete」ボタンについては、「Finish TODO」、「Delete TODO」の実装が終了していないため、表示はされるが機能しない。 未完了のTODOが5件登録済みの場合は、業務エラーとなり、エラーメッセージが表示される。 .. figure:: ./images_Thymeleaf/image070.png :width: 60% 入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。 .. figure:: ./images_Thymeleaf/image071.png :width: 65% | Finish TODOの実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 「Finish」ボタンにTODOを完了させるための処理を追加する。 | Formの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 完了処理用のFormについても、\ ``TodoForm``\ を使用する。 | \ ``TodoForm``\ に\ ``todoId``\ プロパティを追加する必要があるが、単純に追加してしまうと、新規作成処理でも\ ``todoId``\ プロパティのチェックが実行されてしまう。 | 一つのFormクラスを使用して複数のformから送信されるリクエストパラメータをバインドする場合は、\ ``groups``\ 属性を使用して、入力チェックルールをグループ化する。 Formクラスに以下のプロパティを追加する。 * ID → todoId .. code-block:: java package com.example.todo.app.todo; import java.io.Serializable; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class TodoForm implements Serializable { // (1) public static interface TodoCreate { }; public static interface TodoFinish { }; private static final long serialVersionUID = 1L; // (2) @NotEmpty(groups = {TodoFinish.class}) private String todoId; // (3) @NotNull(groups = {TodoCreate.class}) @Size(min = 1, max = 30, groups = {TodoCreate.class}) private String todoTitle; public String getTodoId() { return todoId; } public void setTodoId(String todoId) { this.todoId = todoId; } public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 入力チェックルールをグループ化するためのインタフェースを作成する。 | 入力チェックルールのグループ化については、\ :doc:`../ArchitectureInDetail/WebApplicationDetail/Validation`\ を参照されたい。 | | ここでは、新規作成処理用のインタフェースとして\ ``TodoCreate``\ を、完了処理用のインタフェースとして\ ``TodoFinish``\ を作成している。 * - | (2) - | \ ``todoId``\ は完了処理で使用するプロパティである。 | そのため、\ ``@NotEmpty``\ アノテーションの\ ``groups``\ 属性には、完了処理用の入力チェックルールである事を示す\ ``TodoFinish``\ インタフェースを指定する。 * - | (3) - | \ ``todoTitle``\ は新規作成処理で使用するプロパティである。 | そのため、\ ``@NotNull``\ アノテーションと\ ``@Size``\ アノテーションの\ ``groups``\ 属性には、新規作成処理用の入力チェックルールである事を示す\ ``TodoCreate``\ インタフェースを指定する。 | Controllerの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 完了処理を\ ``TodoController``\ に追加する。 グループ化した入力チェックルールを適用するためには、\ **@Valid アノテーションの代わりに、@Validated アノテーションを使用すること**\ に注意する。 .. code-block:: java package com.example.todo.app.todo; import java.util.Collection; 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.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.terasoluna.gfw.common.exception.BusinessException; import org.terasoluna.gfw.common.message.ResultMessage; import org.terasoluna.gfw.common.message.ResultMessages; import com.example.todo.app.todo.TodoForm.TodoCreate; import com.example.todo.app.todo.TodoForm.TodoFinish; import com.example.todo.domain.model.Todo; import com.example.todo.domain.service.todo.TodoService; import jakarta.inject.Inject; import jakarta.validation.groups.Default; @Controller @RequestMapping("todo") public class TodoController { @Inject TodoService todoService; @Inject TodoMapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @GetMapping("list") public String list(Model model) { Collection todos = todoService.findAll(); model.addAttribute("todos", todos); return "todo/list"; } @PostMapping("create") public String create(@Validated({Default.class, // (1) TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } Todo todo = beanMapper.map(todoForm); try { todoService.create(todo); } catch (BusinessException e) { model.addAttribute(e.getResultMessages()); return list(model); } attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Created successfully!"))); return "redirect:/todo/list"; } @PostMapping("finish") // (2) public String finish(@Validated({Default.class, // (3) TodoFinish.class}) TodoForm form, BindingResult bindingResult, Model model, RedirectAttributes attributes) { // (4) if (bindingResult.hasErrors()) { return list(model); } try { todoService.finish(form.getTodoId()); } catch (BusinessException e) { // (5) model.addAttribute(e.getResultMessages()); return list(model); } // (6) attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Finished successfully!"))); return "redirect:/todo/list"; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | グループ化した入力チェックルールを適用するために、\ ``@Valid``\ アノテーションを\ ``@Validated``\ アノテーションに変更する。 | \ ``value``\ 属性には、適用する入力チェックルールのグループ(グループインタフェース)を指定する。 | \ ``Default.class``\ は、グループ化されていない入力チェックルールを適用するために用意されているグループインタフェースである。 * - | (2) - | \ ``/todo/finish``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、完了処理用のメソッド(\ ``finish``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。 * - | (3) - | 適用する入力チェックのグループとして、完了処理用のグループインタフェース(\ ``TodoFinish``\ インタフェース)を指定する。 * - | (4) - | 入力エラーがあった場合、一覧画面に戻る。 * - | (5) - | 業務処理を実行して、\ ``BusinessException``\ が発生した場合は、結果メッセージを\ ``Model``\ に追加して、一覧画面に戻る。 * - | (6) - | 正常に作成が完了した場合は、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。 .. note:: 新規作成処理用と完了処理用を別々のFormクラスとして作成しても良い。別々のFormクラスにした場合、入力チェックルールをグループ化する必要がないため、入力チェックルールの定義はシンプルになる。 ただし、処理毎にFormクラスを作成した場合、 * クラス数が増える * プロパティが重複するため入力チェックルールを一元管理できない ため、仕様変更が発生した場合に修正コストが高くなる可能性があるという点に注意してほしい。 また、\ ``@ModelAttribute``\ メソッドを使用して複数のFormを初期化した場合、毎回すべてのFormが初期化されるため、不要なインスタンスが生成されることになる。 | テンプレートHTMLの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 完了処理用のformを実装する。 .. code-block:: html Todo List

    Todo List

    • Created successfully!
    size must be between 1 and 30

    • Send a e-mail
    • Have a lunch
    • Read a book
    .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``th:if``\ 属性を使用し、TODOが未完了の場合は、TODOを完了させるためのリクエストを送信するformを表示する。 | \ ``th:action``\ 属性にはリンクURL式 \ ``@{}``\ を用いて完了処理を実行するためのパス(\ ``/todo/finish``\ )を指定する。 * - | (2) - | リクエストパラメータとして\ ``todoId``\ を送信する。 | \ ``th:value``\ 属性を使用して、\ ``todo``\ オブジェクトの\ ``todoId``\ プロパティを値に設定している。 | TODOを新規作成した後に、「Finish」ボタン押下すると、以下のように打ち消し線が入り、完了したことがわかる。 .. figure:: ./images_Thymeleaf/image075.png :width: 40% .. figure:: ./images_Thymeleaf/image076.png :width: 40% | Delete TODOの実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 「Delete」ボタンにTODOを削除するための処理を追加する。 | Formの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 削除処理用のFormについても、\ ``TodoForm``\ を使用する。 .. code-block:: java package com.example.todo.app.todo; import java.io.Serializable; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class TodoForm implements Serializable { public static interface TodoCreate { }; public static interface TodoFinish { }; // (1) public static interface TodoDelete { } private static final long serialVersionUID = 1L; // (2) @NotEmpty(groups = {TodoFinish.class, TodoDelete.class}) private String todoId; @NotNull(groups = {TodoCreate.class}) @Size(min = 1, max = 30, groups = {TodoCreate.class}) private String todoTitle; public String getTodoId() { return todoId; } public void setTodoId(String todoId) { this.todoId = todoId; } public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 削除処理用の入力チェックルールをグループ化するためのインタフェースとして\ ``TodoDelete``\ を作成する。 * - | (2) - | 削除処理では\ ``todoId``\ プロパティを使用する。 | そのため、\ ``todoId``\ の\ ``@NotEmpty``\ アノテーションの\ ``groups``\ 属性には、削除処理用の入力チェックルールである事を示す\ ``TodoDelete``\ インタフェースを指定する。 | Controllerの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 削除処理を\ ``TodoController``\ に追加する。完了処理とほぼ同じである。 .. code-block:: java package com.example.todo.app.todo; import java.util.Collection; 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.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.terasoluna.gfw.common.exception.BusinessException; import org.terasoluna.gfw.common.message.ResultMessage; import org.terasoluna.gfw.common.message.ResultMessages; import com.example.todo.app.todo.TodoForm.TodoCreate; import com.example.todo.app.todo.TodoForm.TodoDelete; import com.example.todo.app.todo.TodoForm.TodoFinish; import com.example.todo.domain.model.Todo; import com.example.todo.domain.service.todo.TodoService; import jakarta.inject.Inject; import jakarta.validation.groups.Default; @Controller @RequestMapping("todo") public class TodoController { @Inject TodoService todoService; @Inject TodoMapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @GetMapping("list") public String list(Model model) { Collection todos = todoService.findAll(); model.addAttribute("todos", todos); return "todo/list"; } @PostMapping("create") public String create(@Validated({Default.class, TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } Todo todo = beanMapper.map(todoForm); try { todoService.create(todo); } catch (BusinessException e) { model.addAttribute(e.getResultMessages()); return list(model); } attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Created successfully!"))); return "redirect:/todo/list"; } @PostMapping("finish") public String finish(@Validated({Default.class, TodoFinish.class}) TodoForm form, BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } try { todoService.finish(form.getTodoId()); } catch (BusinessException e) { model.addAttribute(e.getResultMessages()); return list(model); } attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Finished successfully!"))); return "redirect:/todo/list"; } @PostMapping("delete") // (1) public String delete(@Validated({Default.class, TodoDelete.class}) TodoForm form, BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } try { todoService.delete(form.getTodoId()); } catch (BusinessException e) { model.addAttribute(e.getResultMessages()); return list(model); } attributes.addFlashAttribute( ResultMessages.success().add(ResultMessage.fromText("Deleted successfully!"))); return "redirect:/todo/list"; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ``/todo/delete``\ というパスに\ ``POST``\ メソッドを使用してリクエストされた際に、削除処理用のメソッド(\ ``delete``\ メソッド)が実行されるように\ ``@PostMapping``\ アノテーションを設定する。 | テンプレートHTMLの修正 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 削除処理用のformを実装する。 .. code-block:: html Todo List

    Todo List

    • Created successfully!
    size must be between 1 and 30

    • Send a e-mail
    • Have a lunch
    • Read a book
    .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``th:action``\ 属性にはリンクURL式 \ ``@{}``\ を用いて削除処理を実行するためのパス(\ ``/todo/delete``\ )を指定する。 * - | (2) - | \ ``type="hidden"``\ 属性を使用して、リクエストパラメータとして\ ``todoId``\ を送信する。 | 未完了状態のTODOの「Delete」ボタンを押下すると、以下のようにTODOが削除される。 .. figure:: ./images_Thymeleaf/image077.png :width: 40% .. figure:: ./images_Thymeleaf/image078.png :width: 40% | .. _tutorial-todo_infra_Thymeleaf: データベースアクセスを伴うインフラストラクチャ層の作成 ================================================================================ ここでは、Domainオブジェクトをデータベースに永続化するためのインフラストラクチャ層の実装方法について説明する。 本チュートリアルでは、MyBatis3を使用したインフラストラクチャ層の実装方法について説明する。 | .. _TutorialCreateORMapperBlankProject_Thymeleaf: O/R Mapperに依存したブランクプロジェクトの作成 -------------------------------------------------------------------------------- ここでは、O/R Mapperに依存したブランクプロジェクトの作成を行う。 まず、使用するO/R Mapperに応じてプロジェクトを作成し直す。 \ :ref:`BlankProjectCreateBlankProject`\ 次に、\ :ref:`tutorial-todo_infra_Thymeleaf`\ までで作成した\ :file:`src`\ フォルダ以下のうち、\ **TodoRepositoryImplクラス以外のファイルを新規作成したプロジェクトにコピーする**\ 。 \ **ただし、コピーするファイルは新規作成したファイル・変更を加えたファイルに限り、修正を加えていないファイルはコピーしないこと**\ 。 | .. _Tutorial_Setup_Database_Thymeleaf: データベースのセットアップ -------------------------------------------------------------------------------- ここでは、データベースのセットアップを行う。 本チュートリアルでは、データベースのセットアップの手間を省くため、H2 Databaseを使用する。 | todo-infra.propertiesの修正 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ APサーバ起動時にH2 Database上にテーブルが作成されるようにするために、\ :file:`src/main/resources/META-INF/spring/todo-infra.properties`\ の設定を変更する。 .. code-block:: properties database=H2 # (1) database.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1;INIT=create table if not exists todo(todo_id varchar(36) primary key, todo_title varchar(30), finished boolean, created_at timestamp) database.username=sa database.password= database.driverClassName=org.h2.Driver # connection pool cp.maxActive=96 cp.maxIdle=16 cp.minIdle=0 cp.maxWait=60000 .. tabularcolumns:: |p{0.10\linewidth}|p{0.80\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 80 * - 項番 - 説明 * - | (1) - | 接続URLのINITパラメータに、テーブルを作成するDDL文を指定する。 .. note:: INITパラメータに設定しているDDL文をフォーマットすると、以下の様なSQLとなる。 .. code-block:: sql create table if not exists todo ( todo_id varchar(36) primary key, todo_title varchar(30), finished boolean, created_at timestamp ) | .. _using_MyBatis3_Thymeleaf: MyBatis3を使用したインフラストラクチャ層の作成 -------------------------------------------------------------------------------- ここでは、MyBatis3を使用してインフラストラクチャ層のRepositoryImplを作成する方法について説明する。 || TodoRepositoryの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | \ ``TodoRepository``\ は、O/R Mapperを使用しない場合と同じ方法で作成する。 | 作成方法は、「\ :ref:`TutorialTodoCreateRepository_Thymeleaf`\ 」を参照されたい。 | TodoRepositoryImplの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | MyBatis3を使用する場合、RepositoryImplはRepositoryインタフェース(Mapperインタフェース)から自動生成される。 | そのため、\ ``TodoRepositoryImpl``\ の作成は不要である。作成した場合は削除すること。 | Mapperファイルの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \ ``TodoRepository``\ インタフェースのメソッドが呼び出された際に実行するSQLを定義するためのMapperファイルを作成する。 Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、 .. tabularcolumns:: |p{0.10\linewidth}|p{0.30\linewidth}|p{0.50\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 30 50 * - 項番 - 項目 - 入力値 * - 1 - Enter or select the parent folder - \ ``todo/src/main/resources/com/example/todo/domain/repository/todo``\ * - 2 - File name - \ ``TodoRepository.xml``\ を入力して「Finish」する。 作成したファイルは以下のディレクトリに格納される。 .. figure:: ./images_Thymeleaf/create-mapper-for-mybatis3.png \ ``TodoRepository``\ インタフェースに定義したメソッドが呼び出された際に実行するSQLを記述する。 .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``mapper``\ 要素の\ ``namespace``\ 属性に、Repositoryインタフェースの完全修飾クラス名(FQCN)を指定する。 * - | (2) - | \ ````\ 要素に、検索結果(\ ``ResultSet``\ )とJavaBeanのマッピング定義を行う。 | マッピングファイルの詳細は\ :doc:`../ArchitectureInDetail/DataAccessDetail/DataAccessMyBatis3`\ を参照されたい。 * - | (3) - | \ ``todoId``\ (PK)が一致するレコードを1件取得するSQLを実装する。 | \ ````\ 要素の\ ``resultMap``\ 属性に、適用するマッピング定義のIDを指定する。 | アプリケーションの要件には記載がないが、最新のTODOが先頭に表示されるようにレコードを並び替えている。 * - | (5) - | 引数に指定されたTodoオブジェクトを挿入するSQLを実装する。 | \ ````\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。 * - | (6) - | 引数に指定されたTodoオブジェクトを更新するSQLを実装する。 | \ ````\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。 * - | (7) - | 引数に指定されたTodoオブジェクトを削除するSQLを実装する。 | \ ````\ 要素の\ ``parameterType``\ 属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。 * - | (8) - | 引数に指定された\ ``finished``\ に一致するTodoの件数を取得するSQLを実装する。 | 以上で、MyBatis3を使用したインフラストラクチャ層の作成が完了となる。 APサーバーを起動し、Todoの表示を行うと、以下のようなSQLログやトランザクションログが出力される。 .. code-block:: console date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[START CONTROLLER] TodoController.list(Model) date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Creating new transaction with name [com.example.todo.domain.service.todo.TodoServiceImpl.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Acquired Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver] for JDBC transaction date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:c.e.t.d.repository.todo.TodoRepository.findAll message:==> Preparing: SELECT todo_id, todo_title, finished, created_at FROM todo date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:c.e.t.d.repository.todo.TodoRepository.findAll message:==> Parameters: date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:c.e.t.d.repository.todo.TodoRepository.findAll message:<== Total: 0 date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Initiating transaction commit date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Committing JDBC transaction on Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver] date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Releasing JDBC Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver] after transaction date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[END CONTROLLER ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@4e5a28b2, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors} date:2022-12-01 16:49:29 thread:http-nio-8080-exec-2 X-Track:d3ef4363deef452d8823a00db0fbc71a level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[HANDLING TIME ] TodoController.list(Model)-> 205,791,900 ns | おわりに ================================================================================ このチュートリアルでは、以下の内容を学習した。 * \ |framework_name|\による基本的なアプリケーションの開発方法 * MavenおよびSTS(Eclipse)プロジェクトの構築方法 * \ |framework_name|\のアプリケーションのレイヤ化に従った開発方法 * POJO(+ Spring)を使用したドメイン層の実装 * POJO(+ Spring MVC)とThymeleafを使用したアプリケーション層の実装 * MyBatis3を使用したインフラストラクチャ層の実装 * O/R Mapperを使用しないインフラストラクチャ層の実装 本チュートリアルで作成したTODO管理アプリケーションには、以下の改善点がある。アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。 * プロパティ(未完了TODOの上限数)を外部化する → \ :doc:`../ArchitectureInDetail/GeneralFuncDetail/PropertyManagement`\ * メッセージを外部化する → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/MessageManagement`\ * ページネーション機能を追加する → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/Pagination`\ * 例外ハンドリングを加える → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/ExceptionHandling`\ * 二重送信を防止する(トランザクショントークンチェックを追加する) → \ :doc:`../ArchitectureInDetail/WebApplicationDetail/DoubleSubmitProtection`\ * システム日時の取得元を変更する → \ :doc:`../ArchitectureInDetail/GeneralFuncDetail/SystemDate`\ | .. raw:: latex \newpage