Spring Bootで問い合わせフォームを作ってみる
またしても自分用メモです。
今回はSpring Bootで問い合わせフォームを作ってみます。
全3回に分けて投稿します。構成は以下の通りです。
1. Spring Bootで問い合わせフォームを作ってみる ←イマココ!
2. 1で作った問い合わせフォームをSeleniumを使ってテストしてみる
3. Jenkinsを使って自動デプロイ及び自動テストできるようにする
はじめに
こんな感じの問い合わせフォームを作ってみます。
問い合わせフォームと言いつつ、受け取った値をメールで飛ばすのではなくデータベースに保存します。
データベースを用意する
データベースを用意します。
今回はVagrant+Ansibleで用意しました。
Vagrantfileがあるフォルダで vagrant up してください。
github.com
やっていることとしては、以下の通りです。
問い合わせフォーム作成
ここからは問い合わせフォームを作っていこうと思います。
フォルダ構成
以下URLを参考に作成しました。
https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/htmlsingle/#using-boot-locating-the-main-class
Gradleの設定
以下のURLを参考にbuild.gradleを書いていきます。
https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/htmlsingle/#using-boot-gradle
・build.gradle
plugins { id 'org.springframework.boot' version '1.5.2.RELEASE' id 'java' id 'idea' } jar { baseName = 'spring-boot-web' } repositories { jcenter() } dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-freemarker") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.hsqldb:hsqldb") compile("mysql:mysql-connector-java") testCompile("org.springframework.boot:spring-boot-starter-test") }
Gradleで簡単にSpring Bootを実行できるプラグインを入れつつ、データベース関係でJPA,MySQL connetorなどを設定しています。
JPAを使うのためにHSQLDBが必要です。
また、今回テンプレートエンジンはFreeMarkerを使用しています。
Application.ymlの設定
以下のURLを参考にデータベース接続設定などをしていきます。
propertiesファイルではなくyamlファイルで書いていきます。
https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/htmlsingle/#boot-features-connect-to-production-database
・src/main/resources/application.yml
spring: datasource: url: jdbc:mysql://192.168.33.10/sample?characterEncoding=UTF-8 #characterEncodingを設定しないと文字化けを起こす。 username: myapp password: myapp driverClassName: com.mysql.jdbc.Driver jpa: hibernate: ddl-auto: update
dll-autoの値は、テーブルがなければ新規に作ります。 他にはcreate,create-drop,validate,noneがあります。
Entityクラスの作成
以下のURLを参考にしつつ
https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/htmlsingle/#boot-features-entity-classes
データを保存するテーブルの表現するクラスを作成していきます。
以下のテーブル構造です。
mysql> desc sample.inquiry; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | content | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec)
・src/main/java/com/example/project/domain/InquiryEntity.java
package com.example.project.domain; import javax.persistence.*; @Entity @Table(name="inquiry") public class InquiryEntity { @Id @GeneratedValue private long id; @Column(nullable = false) private String name; @Column(nullable = false) private String email; @Column(nullable = false) private String content; // getter/setter 省略 }
@Tableアノテーションでテーブルの名前を指定することができます。
名前を指定しなかった場合、クラス名をlower snake caseにしたテーブル名が指定されます。
@Idアノテーションで主キーの指定。
@GeneratedValueアノテーションで値を自動生成することを指定しています。
それぞれのカラムにNull許可をいいえに設定しています。
Repositoryクラスの作成
・src/main/java/com/example/project/domain/InquiryRepository.java
package com.example.project.domain; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface InquiryRepository extends JpaRepository<InquiryEntity, Long> { }
@RepositoryアノテーションでRepositoryであることを明示し、Dependency Injectionの仕組みを使ってこのRepositoryクラスを使うときにインスタンス生成せずに使うことができます。
JpaRepositoryクラスにあるデータベースに保存するメソッドなどを使いたかったので継承しています。
ジェネリクスの第1引数はEntityクラス、第2引数はそのEntityのIdの型を指定します。
Formクラスの作成
フォームを表現するクラスを作成します。
・src/main/java/com/example/project/web/InquiryForm.java
package com.example.project.web; import org.hibernate.validator.constraints.NotBlank; public class InquiryForm { @NotBlank private String name; @NotBlank private String email; @NotBlank private String content; // getter/setter 省略 }
view側のformのname属性と変数の名前を合わせます。
@NotBlankアノテーションで空白を禁止しています。ほかには@NotEmptyや@NotNullなどがあります。
本来ならこのクラスにフォームのバリデーションを書いたりすると思いますが、今回は空白ではないことしか定義していません。
Serviceクラスの作成
・src/main/java/com/example/project/service/InquiryService.java
package com.example.project.service; import com.example.project.domain.InquiryEntity; import com.example.project.domain.InquiryRepository; import com.example.project.web.InquiryForm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class InquiryService { @Autowired private InquiryRepository inquiryRepository; /** * 入力された値をデータベースに保存します。 * * @param inquiryForm 送信されたフォーム */ public void save(InquiryForm inquiryForm) { InquiryEntity inquiryEntity = new InquiryEntity(); inquiryEntity.setName(inquiryForm.getName()); inquiryEntity.setEmail(inquiryForm.getEmail()); inquiryEntity.setContent(inquiryForm.getContent()); inquiryRepository.saveAndFlush(inquiryEntity); } }
多分、ここにビジネスロジックなどを書くのでしょうが、データベースに保存する処理を書いています。
@ServiceアノテーションでServiceであることを明示し、Dependency Injectionの仕組みを使ってこのServiceクラスを使うときにインスタンス生成せずに使うことができます。
@Autowiredアノテーションを使ってRepositoryクラスを使えるように宣言しています。
Viewの作成
共通部分のViewを作成
・src/main/resources/templates/_wrapper.ftl
<#ftl strip_whitespace=true> <#macro main title=""> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>${title}</title> <!-- 省略 --> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="content"> <#nested/> </div> </body> </html> </#macro>
#macroで囲い、#nestedのところに呼び出した先のviewを表示します。
フォームの作成
・src/main/resources/templates/form.ftl
<#import "_wrapper.ftl" as wrapper> <@wrapper.main title = "お問い合わせフォーム"> <div class="container"> <Form action="/confirm" method="post"> <div class="form-group row"> <label for="name" class="col-xs-offset-3 col-xs-2 col-form-label">名前</label> <div class="col-xs-4"> <input type="text" id="name" name="name" class="form-control" value="${inquiryForm.name!}"> </div> </div> <div class="form-group row"> <label for="email" class="col-xs-offset-3 col-xs-2 col-form-label">メールアドレス</label> <div class="col-xs-4"> <input type="email" id="email" name="email" class="form-control" value="${inquiryForm.email!}"> </div> </div> <div class="form-group row"> <label for="content" class="col-xs-offset-3 col-xs-2 col-form-label">内容</label> <div class="col-xs-4"> <textarea id="content" name="content" class="form-control" rows="3">${inquiryForm.content!}</textarea> </div> </div> <div class="col-xs-offset-5 col-xs-7"> <button type="submit" class="btn btn-primary">送信</button> </div> </Form> </div> </@wrapper.main>
#importで指定したファイルを読み込みます。
@wrapper.mainで囲まれていることろが_wrapper.ftlの#nestedのところに表示されます。
確認画面から戻るボタンを押したときに入力した値がフォームのテキストボックスに入るようにしていますが、変数をそのまま使うとエラーが出るので、
以下URLを参考に、変数が定義されていなかったらデフォルト値を表示するというのを使っています。
http://freemarker.org/docs/dgui_template_exp.html#dgui_template_exp_missing_default
確認画面
・src/main/resources/templates/confirm.ftl
<#import "_wrapper.ftl" as wrapper> <@wrapper.main title = "お問い合わせ確認"> <div class="container"> <Form action="/save" method="post"> <div class="form-group row"> <label for="name" class="col-xs-offset-3 col-xs-2 col-form-label">名前</label> <div class="col-xs-4"> <p class="">${inquiryForm.name}</p> </div> </div> <div class="form-group row"> <label for="email" class="col-xs-offset-3 col-xs-2 col-form-label">メールアドレス</label> <div class="col-xs-4"> <p class="">${inquiryForm.email}</p> </div> </div> <div class="form-group row"> <label for="content" class="col-xs-offset-3 col-xs-2 col-form-label">内容</label> <div class="col-xs-4"> <p class="">${inquiryForm.content?replace('\n', '<br/>')}</p> </div> </div> <div class="col-xs-offset-4"> <input type="hidden" name="name" value="${inquiryForm.name}"> <input type="hidden" name="email" value="${inquiryForm.email}"> <input type="hidden" name="content" value="${inquiryForm.content}"> <button type="submit" class="btn btn-primary" name="save">送信</button> <button type="submit" class="btn btn-primary" name="back">戻る</button> </div> </Form> </div> </@wrapper.main>
ここでの肝は送信と戻るボタンです。
name属性をそれぞれ指定しておき、Controllerのほうでその値をもとに振り分けています。
以下URLを参考に、テキストエリアに入力された値を表示するとき、改行コードを<br/>タグにするためreplaceを利用しています。
http://freemarker.org/docs/ref_builtins_string.html#ref_builtin_replace
Controllerクラスの作成
リクエストによって表示するViewを分けるControllerを作成します。
・src/main/java/com/example/project/web/FormController.java
package com.example.project.web; import com.example.project.service.InquiryService; import org.springframework.beans.factory.annotation.Autowired; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller public class FormController { @Autowired private InquiryService inquiryService; //問い合わせフォームの表示 @RequestMapping("/form") public String form(InquiryForm inquiryForm, Model model){ return "form"; } //確認画面 @RequestMapping(value = "/confirm", method = RequestMethod.POST) public String confirm(@Validated InquiryForm inquiryForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model){ if (bindingResult.hasErrors()) { //バリデーションエラー時の処理 redirectAttributes.addFlashAttribute(inquiryForm); return "redirect:form"; } model.addAttribute(inquiryForm); return "confirm"; } //送信ボタンを押した @RequestMapping(value = "/save", params = "save", method = RequestMethod.POST) public String save(@Validated InquiryForm inquiryForm, BindingResult bindingResult, Model model){ inquiryService.save(inquiryForm); return "complete"; } //戻るボタンを押した @RequestMapping(value = "/save", params = "back", method = RequestMethod.POST) public String back(InquiryForm inquiryForm, RedirectAttributes redirectAttributes){ redirectAttributes.addFlashAttribute(inquiryForm); return "redirect:form"; } }
@Controllerアノテーションを付けてControllerクラスであることを明示しています。
@RequestMappingアノテーションでどういうリクエストで来たかを判断し、該当するメソッドを呼び出します。
valueでパス、methodでGETなのかPOSTなのかなど、paramsで送られてきたパラメータ、など、一致する条件を指定できます。
また、確認画面で値をフォームに表示させるために、
redirectAttributes.addFlashAttribute()を使ってリダイレクト先に値を送っています。
以上で終わりです。
アクセス
あとはGradleでbootRunタスクを実行するだけ。
以下のURLにアクセスすると表示されると思います。
http://localhost:8080/form
お疲れ様でした。