Day-to-day the memorandum

やったことのメモ書きです。

Spring Bootで問い合わせフォームを作ってみる

またしても自分用メモです。

今回はSpring Bootで問い合わせフォームを作ってみます。
全3回に分けて投稿します。構成は以下の通りです。


1. Spring Bootで問い合わせフォームを作ってみる ←イマココ!
2. 1で作った問い合わせフォームをSeleniumを使ってテストしてみる
3. Jenkinsを使って自動デプロイ及び自動テストできるようにする

はじめに

こんな感じの問い合わせフォームを作ってみます。
問い合わせフォームと言いつつ、受け取った値をメールで飛ばすのではなくデータベースに保存します。
f:id:ZYPRESSEN:20170331143048p:plain
f:id:ZYPRESSEN:20170331143058p:plain

データベースを用意する

データベースを用意します。
今回はVagrant+Ansibleで用意しました。
Vagrantfileがあるフォルダで vagrant up してください。
github.com
やっていることとしては、以下の通りです。

  • IPアドレスに192.168.33.10を設定。
  • MySQLのrootユーザにパスワードを設定する。
  • アプリケーションで使うユーザを作成する。(myappユーザ。パスワードはmyapp)
  • 今回使うデータベースを作成する。(データベース名:sample)
  • 使うデータベースに権限などを付与する。(sampleデータベースにどのIPからアクセスでもmyappユーザですべての権限を使用可能)

問い合わせフォーム作成

ここからは問い合わせフォームを作っていこうと思います。

バージョンなど

java version

1.8

spring boot version

1.5.2

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クラスの作成

以下参考URL
https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/htmlsingle/#boot-features-spring-data-jpa-repositories

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
お疲れ様でした。

さいごに

githubのっけておきます。
github.com

次回は、ここで作った問い合わせフォームをSeleniumを使ってテストしてみるです。