Spring Bootで問い合わせアプリ作成

今回もSpring Bootで、勉強も兼ねてアプリ作成しました。
随時機能追加する予定です。


######2020/06/06：削除機能追加


#使用環境
・Windows10 (64bit)
・spring-boot：2.2.6
・Eclipse：4.9.0
・H2
・Bootstrap

#完成図
簡単な問い合わせアプリです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/29340e64-05ae-2432-6bbe-f03a70dbfcf7.png)

#Entity

```java:Inquiry.java
import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.springframework.format.annotation.DateTimeFormat;

@Entity
public class Inquiry {

	@Id
	@GeneratedValue
	private int id;

	private String name;
	private String email;
	private String contents;

	@DateTimeFormat(pattern="yyyy-MM-dd")
	private LocalDateTime created;

	public Inquiry() {
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getContents() {
		return contents;
	}

	public void setContents(String contents) {
		this.contents = contents;
	}

	public LocalDateTime getCreated() {
		return created;
	}

	public void setCreated(LocalDateTime created) {
		this.created = created;
	}
}
```

#Repository

```java:InquiryDao.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface InquiryDao extends JpaRepository<Inquiry, Integer> {

}
```

#Service

```java:InquiryService.java
import java.util.List;

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

@Service
public class InquiryService {

	@Autowired
	InquiryDao inquiryDao;

	//保存処理
	public Inquiry save(Inquiry inquiry) {

		return inquiryDao.saveAndFlush(inquiry);
	}

	//検索処理
	public List<Inquiry> find() {

		return inquiryDao.findAll();
	}

	//削除処理
	public void delete(int id) {

		inquiryDao.deleteById(id);
	}
}
```

#Form
入力した値を保持するためにFormクラスを用意します。
バリデーションチェックもここで設定します。

```java:InquiryForm.java
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class InquiryForm {

	@Size(min=1, max=20, message="名前の入力チェックエラー")
	private String name;

	@NotEmpty(message="未入力です")
	private String email;

	@NotEmpty(message="未入力です")
	private String contents;

	public  InquiryForm() {}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getContents() {
		return contents;
	}
	public void setContents(String contents) {
		this.contents = contents;
	}
}
```

#Controller

```java:InquiryController.java
import java.time.LocalDateTime;
import java.util.List;

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.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/inquiry")
public class InquiryController {

	@Autowired
	InquiryService inquiryService;

	//一覧画面処理
	@GetMapping()
	public String index(Model model) {

		//全件検索処理
		List<Inquiry> list = inquiryService.find();
		model.addAttribute("list", list);
		model.addAttribute("title", "一覧画面");
		return "index_boot";
	}

	//問い合わせ入力処理
	@GetMapping("form")
	public String form(@ModelAttribute("inquiryForm") InquiryForm inquiryForm, Model model, @ModelAttribute("complete") String complete) {

		model.addAttribute("title", "問い合わせフォーム");
		return "form_boot";
	}

	//確認ページから「戻る」ボタン押下で飛んできた処理
	@PostMapping("form")
	public String formGoBack(@ModelAttribute("inquiryForm") InquiryForm inquiryForm, Model model) {

		model.addAttribute("title", "問い合わせフォーム");
		return "form_boot";
	}

	/*入力内容を打ち込んで「確認ページ」押下した際の処理
	 * @Validatedでフォームクラスの入力チェックを実施して、
	 * チェック結果をBindingResultに格納する
	*/
	@PostMapping("confirm")
	public String confirm(@ModelAttribute("inquiryForm") @Validated InquiryForm inquiryForm, BindingResult res, Model model) {

		//BindingResultの結果、エラーならエラーメッセージを出力する
		if(res.hasErrors()) {
			model.addAttribute("title", "問い合わせフォーム");
			return "form_boot";
		}
		model.addAttribute("title", "確認ページ");
		return "confirm_boot";
	}

	//「保存」ボタン押下した際の処理
	@PostMapping("complete")
	public String complete(@ModelAttribute("inquiryForm") @Validated InquiryForm inquiryForm, BindingResult res, Model model, RedirectAttributes redirectAttributes) {

		if(res.hasErrors()) {
			return "form_boot";
		}

		//InquiryFormの入れ物からInquiryに詰めなおす作業
		Inquiry inquiry = new Inquiry();
		inquiry.setName(inquiryForm.getName());
		inquiry.setEmail(inquiryForm.getEmail());
		inquiry.setContents(inquiryForm.getContents());
		inquiry.setCreated(LocalDateTime.now());

		//保存処理
		inquiryService.save(inquiry);

		redirectAttributes.addFlashAttribute("complete", "保存完了しました");
		return "redirect:/inquiry/form";
	}

	//削除処理
	@GetMapping("delete")
	public String delete(Model model) {

		List<Inquiry> deleteList = inquiryService.find();
		model.addAttribute("title", "削除ページ");
		model.addAttribute("list", deleteList);
		return "delete_boot";
	}

	//URLのidを@PathVariableで受け取って、引数として利用する
	@GetMapping("dell/{id}")
	public String deletes(@PathVariable("id") int id, Model model) {

		inquiryService.delete(id);
		return "redirect:/inquiry";
	}

}
```

#index_boot.html

```html:index_boot.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Jekyll v4.0.1">
    <title th:text="${title}">Starter Template · Bootstrap</title>

    <link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/starter-template/">

    <!-- Bootstrap core CSS -->
<link href="/docs/4.5/dist/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

    <!-- Favicons -->
<link rel="apple-touch-icon" href="/docs/4.5/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/docs/4.5/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/docs/4.5/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/docs/4.5/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="/docs/4.5/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c">
<link rel="icon" href="/docs/4.5/assets/img/favicons/favicon.ico">
<meta name="msapplication-config" content="/docs/4.5/assets/img/favicons/browserconfig.xml">
<meta name="theme-color" content="#563d7c">


    <style>
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
      }

      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }
    </style>
    <!-- Custom styles for this template -->
    <link href="starter-template.css" rel="stylesheet" th:href="@{/css/starter-template.css}">
  </head>
  <body>
    <div th:replace="~{header::headerA}"></div>

<main role="main" class="container">

  <div class="starter-template">
    <h1 th:text="${title}"></h1>
    <p class="lead">問い合わせ一覧画面になります</p>
  </div>
<!-- テーブルの一覧項目 -->
<table class="table table-striped">
  <thead>
    <tr>
      <th scope="col">id</th>
      <th scope="col">氏名</th>
      <th scope="col">Eメール</th>
      <th scope="col">内容</th>
      <th scope="col">日付</th>
    </tr>
  </thead>
  <tbody>
  <!--th:eachで値を繰り返し出力する  -->
    <tr th:each="list:${list}">
      <th scope="row" th:text="${list.id}">1</th>
	  <td th:text="${list.name}">
	  <td th:text="${list.email}">
	  <td th:text="${list.contents}">
      <td th:text="${list.created}">
    </tr>
  </tbody>
</table>
</main><!-- /.container -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
      <script>window.jQuery || document.write('<script src="/docs/4.5/assets/js/vendor/jquery.slim.min.js"><\/script>')</script><script src="/docs/4.5/dist/js/bootstrap.bundle.min.js" th:src="@{/js/bootstrap.bundle.min.js}" integrity="sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd" crossorigin="anonymous"></script></body>
</html>
```

#form_boot.html

```html:form_boot.html
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../../../favicon.ico">

    <title>Starter Template for Bootstrap</title>

    <!-- Bootstrap core CSS -->
    <link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="starter-template.css"  th:href="@{/css/starter-template.css}" rel="stylesheet">
  </head>

  <body>


	<div th:replace="~{header::headerA}"></div>
    <main role="main" class="container">

      <div class="starter-template">
        <h1 th:text="${title}">Bootstrap starter template</h1>
        <p class="lead">問い合わせ内容をここで入力できます</p>
      </div>


	<div th:unless="${#strings.isEmpty(complete)}" >
		<div th:text="${complete}" class="alert alert-success" role="alert">
		  A simple success alert?check it out!
		</div>
	</div>
	<!-- 「戻る」ボタンが押下された時、この入力画面で入れた値を保持するために
			th:valueを使用する -->
	<!--th:ifの結果trueの場合、th:errorsが適用される(各項目に設定したバリデーションが出力される)  -->
	<form method="post" action="#" th:action="@{/inquiry/confirm}" th:object="${inquiryForm}">
	  <div class="form-group">
	    <label for="name">Name</label>
	    <input type="text" name="name" class="form-control" id="name" th:value="*{name}">
	  </div>
	  <div class="text-danger mb-4" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
	  <div class="form-group">
	    <label for="email">Email</label>
	    <input type="text" name="email" class="form-control" id="email"  th:value="*{email}">
	  </div>
	  <div class="text-danger mb-4" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
	  <div class="form-group">
	    <label for="contents">Inquiry</label>
	    <textarea  name="contents" class="form-control" id="detail" rows="3" th:field="*{contents}"></textarea>
	  </div>
	  <div class="text-danger mb-4" th:if="${#fields.hasErrors('contents')}" th:errors="*{contents}"></div>
	  <button type="submit" class="btn btn-primary">確認</button>
	</form>
    </main><!-- /.container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script>window.jQuery || document.write('<script src="@{/js/jquery-slim.min.js}"><\/script>')</script>
    <script src="../../assets/js/vendor/popper.min.js"  th:src="@{/js/popper.min.js}"></script>
    <script src="../../dist/js/bootstrap.min.js"  th:src="@{/js/bootstrap.min.js}"></script>
  </body>
```

#confirm_boot.html

```html:confirm_boot.html
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../../../favicon.ico">

    <title>Starter Template for Bootstrap</title>

    <!-- Bootstrap core CSS -->
    <link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="starter-template.css"  th:href="@{/css/starter-template.css}" rel="stylesheet">
  </head>

  <body>

	<div th:replace="~{header::headerA}"></div>
    <main role="main" class="container">

      <div class="starter-template">
        <h1 th:text="${title}">Bootstrap starter template</h1>
        <p class="lead">この画面で入力内容のご確認が可能です。</p>
      </div>

<!--inquiryFormに詰めている各値を取り出す  -->
<div th:object="${inquiryForm}">
<div class="mb-5">
<ul class="list-group">
  <li th:text="*{name}" class="list-group-item"></li>
  <li th:text="*{email}" class="list-group-item"></li>
  <li th:text="*{contents}"class="list-group-item"></li>
</ul>
</div>
<!--hiddenを使用すると、ボタンを押下することで、値を送信することができる  -->
<form method="post" th:action="@{/inquiry/form}">
	<input type="hidden" name="name" th:value="*{name}">
	<input type="hidden" name="email" th:value="*{email}">
	<input type="hidden" name="contents" th:value="*{contents}">
	<button type="submit" class="btn btn-primary">戻る</button>
</form>
<form method="post" th:action="@{/inquiry/complete}" th:object="${inquiryForm}">
	<input type="hidden" name="name" th:value="*{name}">
	<input type="hidden" name="email" th:value="*{email}">
	<input type="hidden" name="contents" th:value="*{contents}">
	<button type="submit" class="btn btn-primary">投稿</button>
</form>
</div>

    </main><!-- /.container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script>window.jQuery || document.write('<script src="@{/js/jquery-slim.min.js}"><\/script>')</script>
    <script src="../../assets/js/vendor/popper.min.js"  th:src="@{/js/popper.min.js}"></script>
    <script src="../../dist/js/bootstrap.min.js"  th:src="@{/js/bootstrap.min.js}"></script>
  </body>
</html>
```

#delete_boot.html

```html.delete_boot.html

<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../../../favicon.ico">

    <title>Starter Template for Bootstrap</title>

    <!-- Bootstrap core CSS -->
    <link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="starter-template.css"  th:href="@{/css/starter-template.css}" rel="stylesheet">
  </head>

  <body>


	<div th:replace="~{header::headerA}"></div>
    <main role="main" class="container">

      <div class="starter-template">
        <h1 th:text="${title}">Bootstrap starter template</h1>
        <p class="lead">削除したい項目を選択してください</p>
      </div>


	<div th:unless="${#strings.isEmpty(complete)}" >
		<div th:text="${complete}" class="alert alert-success" role="alert">
		  A simple success alert?check it out!
		</div>
	</div>
	<table class="table table-striped">
	  <thead>
	    <tr>
	      <th scope="col">id</th>
	      <th scope="col">氏名</th>
	      <th scope="col">Eメール</th>
	      <th scope="col">内容</th>
	      <th scope="col">日付</th>
	      <th scope="col">削除</th>
	    </tr>
	  </thead>
	  <tbody>
	  <!--th:eachで値を繰り返し出力する  -->
	    <tr th:each="list:${list}">
	      <th scope="row" th:text="${list.id}"></th>
		  <td th:text="${list.name}">
		  <td th:text="${list.email}">
		  <td th:text="${list.contents}">
	      <td th:text="${list.created}">
	      <td><a th:href="@{/inquiry/dell/{id}(id=${list.id})}">削除</a>
	    </tr>
	  </tbody>
	</table>
    </main><!-- /.container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script>window.jQuery || document.write('<script src="@{/js/jquery-slim.min.js}"><\/script>')</script>
    <script src="../../assets/js/vendor/popper.min.js"  th:src="@{/js/popper.min.js}"></script>
    <script src="../../dist/js/bootstrap.min.js"  th:src="@{/js/bootstrap.min.js}"></script>
  </body>
```


#処理パターン
ヘッダーの「問い合わせフォーム」押下してフォーム画面に遷移します。
氏名、メール、内容を入力して、「確認」ボタン押下します。
確認画面に遷移しますが、「戻る」ボタン押下して、前の画面に戻ります。
内容等を変更して、再度「確認」ボタンを押下します。
確認画面で「投稿」ボタンを押下すると、内容が保存されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/ce9354c3-5f44-522c-9cfc-27a75af2b8fb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/33771212-e6f7-4a31-1709-c37bd114c802.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/9cbe9fa3-d9af-89ef-93a2-dba3f309e5f5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/a0911760-32a7-e7b8-b72f-bea0e3572611.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/15d95248-21fd-1def-fba2-c65380c56a4b.png)


##削除パターン
納豆太の「削除」ボタン押下します。
すると、削除されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/ff44298e-5723-d0cd-a8d8-f6acc2a971b7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631044/e56f16f9-d181-8335-ce9b-43f01f56f44f.png)



#参考
[Spring入門](https://www.udemy.com/course/java_spring_beginner/)