概要
画面から送信されたデータをDBに登録する機能を実装します。
入力データはバリデーションによってチェックされることとします。
サーバサイドは以下のように各層で処理分けされます。
<Contoroller>
View(画面)からパラメータを受け取り、ドメイン側(Service)へ処理を委譲します。
バリデーションチェックはここで行われます。
<Service>
ビジネスロジックを実装します。
今回の登録機能はDBにデータを登録するだけなので、永続層(Repository)にModelを渡します。
<Repository>
データの永続処理を実装します。
実装はMybatis(XML)によるINSERT文です。
Seiviceから受け取ったModelをDBに登録します。
バリデーションチェック
今回の登録機能では以下のチェックを行います。
・必須チェック
・桁数チェック
・正規表現チェック
・フォーマットチェック
実装
BookFormクラスにアノテーションで各チェックを実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package com.example.demo.controller.form; import java.text.SimpleDateFormat; import java.util.Date; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import org.springframework.format.annotation.DateTimeFormat; import com.example.demo.domain.model.Book; import lombok.Data; @Data public class BookForm { private int id; @Size(min=1, max=100) private String name; @Pattern(regexp = "^[0-9]{13}$") private String isbn; private String description; @Size(max=100) private String publisher; @NotNull @Min(0) private Integer price; @DateTimeFormat(pattern="yyyy-MM-dd") private Date publication_date; private int version; public BookForm() {} public BookForm(Book book) { this.id = book.getId(); this.name = book.getName(); this.isbn = book.getIsbn(); this.description = book.getDescription(); this.publisher = book.getPublisher(); this.publication_date = book.getPublication_date(); this.price = book.getPrice(); this.version = book.getVersion(); } } |
・@Size(min=1, max=100)
最小桁数=1、最大桁数=100を指定します。
・@Pattern(regexp = “^[0-9]{13}$”)
「regexp」に指定した正規表現と一致することのチェックを行います。
・@NotNull
Nullの場合、エラーと判定します。
・@Min(0)
最小値=0を指定します。
・@DateTimeFormat(pattern=”yyyy-MM-dd”)
「pattern」で受け取る値のフォーマットを定義することで、受けとった値を日付フォーマットに変換します。
BookControllerでエラー判定を行い、エラーがあればViewに返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@PostMapping("book/create") public String postCreate(@Validated BookForm form, BindingResult result, Model model) { // ISBNコードは登録されているデータ内で一意である必要があるため、 // 既に登録済みであることをチェックしています。 if(service.existsIsbn(form.getIsbn())) { // ISBNコードが既に使われていれば、 // エラーとなるフィールド(isbn)とエラーコード(existsIsbn)を設定します。 result.rejectValue("isbn", "existsIsbn"); } // エラーがあれば登録フォームを再表示させます。 if(result.hasErrors()) { return getAdd(form, model); } // formで受け取った値をエンティティであるBookに格納します。 Book book = new Book(form); // 登録処理をServiceに委譲します。 service.save(book); // 検索一覧画面に遷移します。 return "book/list"; } |
・@Validated BookForm form
画面からリクエストされた値を受け取るとともに、バリデーションチェックを行います。
formのフィールド名と画面のname属性が対応するため、同名同士で値がマッピングされます。
(例)name属性(isbn) ⇒ フィールド名(isbn)
・BindingResult result
formのバリデーションにエラーがある場合に、エラー内容が格納されます。
エラーメッセージの定義
application.propertiesに、messages.propertiesの設定を追記します。
配置場所:src/main/ersource/application.properties
1 2 3 |
# messages.propertiesの設定 spring.messages.basename=messages spring.messages.encoding=UTF-8 |
messages.propertiesを作成します。
配置場所:src/main/ersource/messages.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
## BindingResultに格納されているオブジェクト名で指定する bookForm.name=書籍名 Size.bookForm.name={0}は1~100文字で入力して下さい。 bookForm.isbn=ISBNコード Pattern.bookForm.isbn={0}は数字13桁で入力して下さい。 existsIsbn.bookForm.isbn=既に存在します。 bookForm.publisher=出版社 Size.bookForm.publisher={0}は1~100文字で入力して下さい。 bookForm.price=価格 Min.bookForm.price={0}は0円以上で入力して下さい。 NotNull.bookForm.price={0}を入力して下さい。 bookForm.publication_date=出版年月日 NotNull.bookForm.publication_date={0}を入力して下さい。 updated.bookForm.id=既に他のユーザによって更新されています。 |
エラーメッセージの表示
add.htmlの以下の部分がエラー時のメッセージ表示処理です。
1 |
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span> |
Model
エンティティであるBook.javaを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package com.example.demo.domain.model; import java.text.SimpleDateFormat; import java.util.Date; import com.example.demo.controller.form.BookForm; import lombok.Data; @Data public class Book { // MybatisのresultMap用にデフォルトコンストラクタを用意しておきます。 public Book() {} // BookFormの値を格納するためのコンストラクタ // (Controller内でSetterを使って格納するよりこちらの方がスマート) public Book(BookForm form) { this.id = form.getId(); this.name = form.getName(); this.isbn = form.getIsbn(); this.description = form.getDescription(); this.publisher = form.getPublisher(); this.price = form.getPrice(); this.publication_date = form.getPublication_date(); this.version = form.getVersion(); } private int id; private String name; private String isbn; private String description; private String publisher; private int price; private Date publication_date; private String created_user; private Date created_at; private String updated_user; private Date updated_at; private String deleted_user; private Date deleted_at; private int version; } |
・@Data
Lombokのアノテーション(Setter/Getterを書かなくても使えるようにしてくれます)
Service
登録処理とISBNコードの存在チェックを行う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package com.example.demo.service; import java.util.ArrayList; import java.util.Date; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.controller.form.BookForm; import com.example.demo.domain.model.Book; import com.example.demo.domain.repository.BookRepository; @Service @Transactional public class BookService { @Autowired BookRepository repository; public void save(Book book) { String userId = "loglog"; Date now = new Date(); book.setCreated_user(userId); book.setCreated_at(now); book.setUpdated_user(userId); book.setUpdated_at(now); repository.save(book); } public boolean existsIsbn(String isbn) { return repository.existsIsbn(isbn) == null ? false : true; } } |
・@Service
Serviceクラスであることを指定します。
・@Transactional
DB操作のトランザクション範囲を定義できる。
アノテーションを付けたメソッドを単位として1トランザクションとなる。
今回のようにクラスに着けた場合、クラス内のすべてのメソッドにアノテーションを付けたことになる。
Repository
RepositoryではDB操作を行うインターフェースを提供します。
実際のDB操作はXMLでSQLを記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.example.demo.domain.repository; import java.util.ArrayList; import java.util.Map; import org.apache.ibatis.annotations.Mapper; import com.example.demo.domain.model.Book; @Mapper public interface BookRepository { public int save(Book book); public Book existsIsbn(String isbn); } |
・@Mapper
MybatisのためのMapper実装だということを示します。
配置先は、Repositoryインターフェースのパッケージ階層と同じになるように配置します。
配置先:/src/main/resources/com/example/demo/domain/repository/BookRepository.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?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.domain.repository.BookRepository"> <resultMap type="com.example.demo.domain.model.Book" id="book"> <id column="id" property="id"></id> <result column="name" property="name"></result> <result column="isbn" property="isbn"></result> <result column="description" property="description"></result> <result column="publisher" property="publisher"></result> <result column="price" property="price"></result> <result column="publication_date" property="publication_date"></result> <result column="created_user" property="created_user"></result> <result column="created_at" property="created_at"></result> <result column="updated_user" property="updated_user"></result> <result column="updated_at" property="updated_at"></result> <result column="deleted_user" property="deleted_user"></result> <result column="deleted_at" property="deleted_at"></result> <result column="version" property="version"></result> </resultMap> <insert id="save"> INSERT INTO book (name, isbn, description, publisher, price, publication_date, created_user, created_at, updated_user, updated_at) VALUES (#{name}, #{isbn}, #{description}, #{publisher}, #{price}, #{publication_date}, #{created_user}, #{created_at}, #{updated_user}, #{updated_at}) </insert> <select id="existsIsbn" resultMap="book"> SELECT * FROM book WHERE isbn = #{isbn} AND deleted_at IS NULL </select> </mapper> |
・<insert id=”save”>
INSERT文を実行する定義です。
Repositoryに渡したエンティティのフィールド名と#{XXXX}が対応します。
(例)Book.name ⇒ #{nane}
・<select id=”existsIsbn” resultMap=”book”>
SELECT文を実行する定義です。
取得結果をresultMap=”book”でマッピング先として定義します。
”book”は、<resultMap type=”com.example.demo.domain.model.Book” id=”book”>のidに対応します。
このマッピングにより、「com.example.demo.domain.model.Book」のエンティティに取得結果がマッピングされたオブジェクトを取得できます。
取得できない場合、bookオブジェクトはNULLです。
動作確認
・画面からの値がControllerにリクエストされていること(System.out.printで確認)
・エラーとなる値はバリデーションエラーとなること。
また、登録画面を表示し、エラーメッセージも表示されること。
・ISBNコードを既に登録済みのデータを登録しようとするとエラーとなること。
また、登録済みでない(論理削除されているものも含む)のであればエラーとならないこと。
・登録成功後、MySQL Workbenchで登録データを確認し、それが画面で入力した通りであること。
また、検索一覧画面に遷移すること。
まとめ
・画面からリクエストされた値はFormでバリデーションする。
・バリデーションエラーの場合、エラーメッセージを画面に表示して登録処理を中断する。
・Contoroller⇒Service⇒Repositoryと各層で適切な処理を担当させる。
・MybatisはXMLでSQLを記述する。
・XML内では、受け取ったエンティティの値をマッピングしたり、取得結果をエンティティにマッピングしている。
コメント