SpringBootで日付計算処理アプリ

日付計算処理を行うアプリケーションを作成しましたので自分用のアウトプットとして投稿させていただきます。
記載が稚拙だったり誤っている箇所等あるかもしれませんがご容赦ください。
誤りについてはご指摘いただけますと大変幸いですm(_ _)m。

##制作物イメージ
・ログイン機能がある（SpringSecurityを使用した超簡潔なもので、おまけ程度のものです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1189089/507b80e3-6354-33b4-9e92-559fcb30fcbd.png)

・HTMLで表示した画面に入力した日付に対して、DBに登録した計算式から計算処理を行う
![日付計算アプリ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1189089/2302bed5-da36-8d12-06ef-396503b470b8.gif)



##環境
macOS Big Sur
Java 11
Spring Boot 2.4.5
SpringToolSuite4
gradle
MySQL
MyBatis

##ディレクトリ構成（一部抜粋）

```
.
├src/main/java/com/example/demo/
│　　　　　　　　　　　　　　　　　├config / SecurityConfig.java
│　　　　　　　　　　　　　　　　　├controller / DateCalcController.java
│　　　　　　　　　　　　　　　　　├model / DateCalc.java
│　　　　　　　　　　　　　　　　　├repository / DateCalcMapper.java
│　　　　　　　　　　　　　　　　　└service / DateCalcService.java
└ src/main/resources/
　　　　　　　　　├templates
　　　　　　　　　│  　├register.html
　　　　　　　　　│  　├top.html
　　　　　　　　　│  　└update.html
　　　　　　　　　├static / css / style.css
　　　　　　　　　├application.properties
　　　　　　　　　├data.sql
　　　　　　　　　└schema.sql
```

##各ソースコード
###application.properties

```
spring.datasource.url=jdbc:mysql://localhost:3306/[任意のDB名を記載してください]?serverTimezone=Asia/Tokyo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=[MySQLのパスワードを記載してください]

spring.datasource.sql-script-encoding=UTF-8
spring.datasource.initialization-mode=always

spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath:data.sql

#Log Level
logging.level.com.example=debug
```
MySQLの利用に必要な記載と、アプリ実行時に自動起動させるSQLファイルの記載です。
当記事ではxmlファイルでなくMapper.javaファイルにSQL文を記載するため、
xmlファイルのパス指定は書いておりません。
 #LogLevel…コンソールにSQLの処理を出力してくれます（無くてもOKです）。

###schema.sql

```sql
CREATE TABLE IF NOT EXISTS DateCalc (
	id INT(50) PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(50),
	plusyear INT(50),
	plusmonth INT(50),
	plusday INT(50)
	)
	AUTO_INCREMENT = 1;



```
作成するテーブルの内容を記載します。
AUTO_INCREMENTによりIDは自動連番としています。
計算結果の値をMap型に格納するために重要です。
AUTO_INCREMENT=1により、初期値を1としています（デフォルトの初期値は０です）。

###data.sql

```sql
INSERT IGNORE INTO DateCalc (id, name, plusyear, plusmonth, plusday) VALUES	(1, 'A', 1, 1, 1);
INSERT IGNORE INTO DateCalc (id, name, plusyear, plusmonth, plusday) VALUES	(2, 'B', 2, 2, 2);
INSERT IGNORE INTO DateCalc (id, name, plusyear, plusmonth, plusday) VALUES	(3, 'C', 3, 3, 3);
```
DateCalcテーブルに予めINSERTしておくユーザー情報を記載します。
IGNOREにより、もし無ければ追加するようにしています。

###SecurityConfig.java

```java
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	//パスワードのハッシュ化
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	/* BCryptPasswordEncoder
	 * bcryptアルゴリズムを使用したエンコーダーにより
	 * パスワードのハッシュ化を提供しているクラス
	 */
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			//認証リクエストの設定 authorize=認可リクエスト　認証→認可の流れ
			.authorizeRequests()
			
				//認証の必要があるよう設定
				.anyRequest()//いかなるリクエストも
				.authenticated()//認証が必要
				
			//フォームベースの設定
			.and().formLogin();
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			//インメモリ認証を設定　インメモリorDB認証のどちらか
			.inMemoryAuthentication()
			
			//"user"を追加
			.withUser("user")
			//"password"をBCryptで暗号化
			.password(passwordEncoder().encode("1"))
			//権限＝ロールを設定
			.authorities("ROLE_USER");
	}

}

```
書籍等を参考に、ただ実装しただけのログイン機能です。
SpringSecurityに用意されているログイン画面を表示させているので、htmlファイルはありません。
ユーザー名とパスワードは以下のように設定しています。
ユーザー名：user
パスワード：1


###top.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>日付計算アプリtop</title>
</head>
<body>
	<h1>日付を入力してください(top.html)</h1>
	
	<form method="post" th:action="@{/calc}">
		<label>基準となる日付：<input type="date" name="inputdate" value="2020-01-01" max="9999-12-31"required></label><br>
		<button>計算実行</button><br>
	</form>
	</label>
	
	
	<a th:href="@{/register}">
		<button>計算式の新規登録へ</button>
	</a>
	<!-- ↓↓下記の通りregister用のボタンを生成しても一応動く↓↓
	<form method="get" th:action="@{/register}">
		<button>計算式の新規登録へ</button><br>
	</form>
	-->
	
	<p th:if="${inputdate} == null">（日付を入力したらここに表示されます）</span></p>
	<p th:unless="${inputdate} == null">（入力した日付は<span th:text="${inputdate}"></span>）</p>
	
	<table border=1 align="left">
		<thead>
		<tr>
			<th>計算式ID</th>
			<th>計算式名</th>
			<th>加減年</th>
			<th>加減月</th>
			<th>加減日</th>
			<th>計算結果(map)</th>
			<th>更新ボタン</th>
			<th>削除ボタン</th>
		</tr>
		</thead>
		
		<tbody th:each="dateCalc:${dateCalc}" th:object="${dateCalc}">
		<tr>
			<td th:text="*{id}"></td>
			<td th:text="*{name}"></td>
			<td th:text="*{plusyear}"></td>
			<td th:text="*{plusmonth}"></td>
			<td th:text="*{plusday}"></td>
			<td th:text="${resultdateMap.get(__*{id}__)}"></td>
			<td><a th:href="@{/update/id={id}(id=*{id})}"><button>更新</button></a></td>
			<td><form method="post" th:action="@{/delete/id={id}(id=*{id})}"><button>削除</button></form></td>                   
		</tr>
		</tbody>
	</table>
	
	<table border=1>
		<thead>
		<tr>
			<th>計算結果(配列)</th>
		</tr>
		</thead>
		<tbody th:each="resultdateArray:${resultdateArray}">
		<tr>
			<td th:text="${resultdateArray}"></td>
		</tr>
		</tbody>
	</table>
	
</body>
</html>
```
ログイン後に表示されるトップ画面です。
日付入力欄はinputタグのtype="date"により、日付以外入力できないようにしています。
またデフォルトだとなぜか年に６桁まで入力できるため、max="9999-12-31"により疑似バリデーションしています。

計算結果の表示を、JavaのMap型と配列の２通りで行っています。
それぞれ一長一短であり、改善策を模索中です。
>
Map型
　計算式IDをキーとして、一つのテーブルで表示できます。
　しかし、delete処理などにより計算式IDが連番でなくなると、上手く結果が表示されなくなります。
配列
　計算式IDが連番でなくても、順番に計算結果を表示してくれます。
　しかし、配列の表示のために「th:each」を用いる必要があり、一つのテーブルで表示できません。

（追記）
改善できました！！→[「SpringBootで日付計算処理アプリ」の続き](https://qiita.com/hhujk5/items/20d2a362b031b298b60a)

###register.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>日付計算アプリregister</title>
	<link th:href="@{/css/style.css}" rel="stylesheet" type="text/css">
</head>
<body>
	<h1>新規登録する加減算式を入力してください(register.html)</h1>
	<form method="post" th:action="@{/register}" th:object="${dateCalc}">
		<table border=1>
		<thead>
			<tr>
				<th>計算式名</th>
				<th>加減年</th>
				<th>加減月</th>
				<th>加減日</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td><input type="text" th:field="*{name}" placeholder="任意の文字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span></td>
					
				<td><input type="text" th:field="*{plusyear}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusyear')}">入力値エラー</span></td>
					
				<td><input type="text" th:field="*{plusmonth}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusmonth')}">入力値エラー</span></td>
				
				<td><input type="text" th:field="*{plusday}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusday')}">入力値エラー</span></td>
			</tr>
		</tbody>
		</table>
		<button>新規登録</button>
	</form>
	
	<form>

</body>
</html>

```
th:if="${#fields.hasErrors()}により、特定の値以外が入力された際にエラーが表示されるようにしています。
「計算式名」はth:errorsにより、Entityクラスに@NotBlankで設定したエラーメッセージを表示しています。
それ以外は、「入力値エラー」と表示しています。
エラー分を赤字表示するため、class="form-error"を作成し、cssファイルで設定しています。

↓エラーメッセージ表示はこんな感じです↓
![スクリーンショット 2021-05-09 18.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1189089/82f97472-e0bb-e840-593f-3c3f71ec06cc.png)


###update.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>日付計算アプリupdate</title>
	<link th:href="@{/css/style.css}" rel="stylesheet" type="text/css">
</head>
<body>
	<h1>更新後の加減算式を入力してください(update.html)</h1>
	<form method="post" th:action="@{/update/id={id}(id=*{id})}" th:object="${dateCalc}">
	
		<table border=1>
		<thead>
			<tr>
				<th>計算式ID</th>
				<th>計算式名</th>
				<th>加減年</th>
				<th>加減月</th>
				<th>加減日</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td><p>計算式ID：<span border="1" th:text="*{id}"></span></p></td>
				<td><input type="text" th:field="*{name}" placeholder="任意の文字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span></td>
					
				<td><input type="text" th:field="*{plusyear}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusyear')}">入力値エラー</span></td>
					
				<td><input type="text" th:field="*{plusmonth}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusmonth')}">入力値エラー</span></td>
				
				<td><input type="text" th:field="*{plusday}" placeholder="半角数字"><br>
					<span class="form-error" th:if="${#fields.hasErrors('plusday')}">入力値エラー</span></td>
			</tr>
		</tbody>
		</table>
		
		<button>更新</button>
		
	</form>
	
	<form>

</body>
</html>
```

register.htmlとほとんど同じ構成です。
違いとして、formタグ内のth:actionで指定するURL内で、更新対象の計算式のIDを動的処理により表示しています。

###style.css

```css
.form-error {
	color:red;
}
```

エラーメッセージを赤字にしているだけです＼(^o^)／


###DateCalc.java

```java
package com.example.demo.entity;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import lombok.Data;

@Data
public class DateCalc {
	
	private int id;
	
	
	@NotBlank(message = "(Entityクラスに設定したオリジナルメッセージ)：入力してください")
	private String name;
	
	@NotNull
	private int plusyear;
	
	
	@NotNull
	private int plusmonth;
	
	
	@NotNull
	private int plusday;

}
```
DBのデータを格納するためのEntityクラスです。
それぞれのフィールドにアノテーションでバリデーション処理を実装しています。
@NotNullの場合は(message = "")でエラーメッセージを変更できませんでした。（そういう仕様…？）

###DateCalc.Mapper.java

```java
package com.example.demo.repository;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.example.demo.entity.DateCalc;

@Mapper
public interface DateCalcMapper {
	
	//select全件
	@Select("select * from DateCalc")
	public List<DateCalc> selectAll();
	
	//select１件
	@Select("SELECT * FROM DateCalc where id = #{id}")
	public DateCalc selectOne(int id);
	
	//新規登録
	@Insert("insert into DateCalc  (name, plusyear, plusmonth, plusday) values (#{name}, #{plusyear}, #{plusmonth}, #{plusday})")
	public void insertOne(DateCalc dateCalc);
	
	//更新
	@Update("update DateCalc set name = #{name}, plusyear = #{plusyear}, plusmonth = #{plusmonth}, plusday = #{plusday} where id = #{id}")
	public void updateOne(DateCalc dateCalc);
	
	//削除
	@Delete("delete from DateCalc where id = #{id}")
	public void deleteOne(DateCalc dateCalc);
	
}


```
MyBatisにおいてSQL文を記載しているファイルです。
SQL文が複雑だったり数が多い訳ではないので、今回は別途xmlファイルを作成せずMapperファイルに書くことにしました。


###DateCalcService.java

```java
package com.example.demo.service;

import java.util.List;

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

import com.example.demo.entity.DateCalc;
import com.example.demo.repository.DateCalcMapper;


@Service
public class DateCalcService {
	
	@Autowired
	DateCalcMapper mapper;
	
	//select全件
	public List<DateCalc> selectAll() {
		return mapper.selectAll();
	}
	
	//select１件
	public DateCalc selectOne(int id) {
		return mapper.selectOne(id);
	}
	
	
	//insert
	public void insertOne(DateCalc dateCalc) {
		mapper.insertOne(dateCalc);
	}
	
	//update
	public void updateOne(DateCalc dateCalc) {
		mapper.updateOne(dateCalc);
	}
	
	//delete
	public void deleteOne(DateCalc dateCalc) {
		mapper.deleteOne(dateCalc);
	}
	

}


```

###DateCalcController.java

```java
package com.example.demo.controller;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;

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 com.example.demo.entity.DateCalc;
import com.example.demo.service.DateCalcService;

@Controller
public class DateCalcController {
	
	@Autowired
	DateCalcService service;
	
	//top.html表示
	@GetMapping("/")
	public String loginSuccess(Model model) {
		List<DateCalc> dateCalc = service.selectAll();
		
		LocalDate[] resultdateArray = new LocalDate[0];
		Map<Integer, LocalDate> resultdateMap = new HashMap<>();
		
		model.addAttribute("dateCalc", dateCalc);
		model.addAttribute("resultdateArray", resultdateArray);
		model.addAttribute("resultdateMap", resultdateMap);
		return "top";
	}
	
	//計算結果をtop.htmlに表示
	@PostMapping("/calc")
	public String calc(Model model, @ModelAttribute("inputdate") String inputdateHTML) {//@ModelAttributeでhtml画面に入力された値を受けっ取っている
		List<DateCalc> dateCalc = service.selectAll();//DBから全データをselectしDateCalc型のListに格納
		LocalDate inputdate = LocalDate.parse((inputdateHTML), DateTimeFormatter.ofPattern("yyyy-MM-dd"));//htmlファイルに入力された数値をデータ型に変換
		
		//確認用
//		System.out.println("inputdateは " + inputdate);
//		System.out.println("End");
	
		//加減年・月・日格納用の配列を作成
		int[] plusyear = new int[dateCalc.size()];
		int[] plusmonth = new int[dateCalc.size()];
		int[] plusday = new int[dateCalc.size()];
		
		//計算結果格納用のLocalDate型の配列を作成
		LocalDate[] resultdateArray = new LocalDate[dateCalc.size()];
		
		//上記の配列を用いて、DB内の値の数だけ繰り返し処理し、各計算式に対する計算結果resultdateを作成
		for(int i = 0; i < dateCalc.size(); i++) {
			plusyear[i] = dateCalc.get(i).getPlusyear();
			plusmonth[i] = dateCalc.get(i).getPlusmonth(); 
			plusday[i]	 = dateCalc.get(i).getPlusday();
			resultdateArray[i] = inputdate.plusYears(plusyear[i]).plusMonths(plusmonth[i]).plusDays(plusday[i]);
		}
		
		//確認用
//		System.out.println("計算結果日は↓");
//		for(LocalDate value : resultdateArray) {
//			System.out.println(value);
//		}
//		System.out.println("End");
//		System.out.println("resultdateArray[0]は：" + resultdateArray[0]);
		
		//Mapに格納
		Map<Integer, LocalDate> resultdateMap = new HashMap<>();
		for(int i = 0; i < dateCalc.size(); i++) {
			resultdateMap.put(i+1, resultdateArray[i]);
		}
		
		//確認用
//		System.out.println("resultdateMap確認用");
//		for(Integer key : resultdateMap.keySet()) {
//			LocalDate value = resultdateMap.get(key);
//			System.out.println("idが" + key + "のresultdateは" + value);
//		}
//		System.out.println("id1のresultdateMapは" + resultdateMap.get(1));
//		System.out.println("End");
		
		//Thymeleafへファイルへ各値を渡す
		model.addAttribute("dateCalc", dateCalc);
		model.addAttribute("inputdate", inputdate);
		model.addAttribute("resultdateArray", resultdateArray);
		model.addAttribute("resultdateMap", resultdateMap);
		
		//top.htmlを表示
		return "top";
	}
	
	//新規登録
	@GetMapping("/register")
	public String displayRegister(@ModelAttribute DateCalc dateCalc, Model model) {
		model.addAttribute("dateCalc", dateCalc);
		return "register";
	}
	@PostMapping("/register")
	public String runRegister(Model model, @Validated @ModelAttribute DateCalc dateCalc, BindingResult bindingresult) {
		if(bindingresult.hasErrors()) {
			System.out.println("エラー発生！BindingResult内容↓↓↓");
			System.out.println(bindingresult);
			System.out.println("エラー発生！BindingResult内容↑↑↑");
			model.addAttribute("dateCalc", dateCalc);
			return "/register";
		}
		service.insertOne(dateCalc);
		return "redirect:/";
	}
	
	
	//更新
	@GetMapping("/update/id={id}")
	public String displayUpdate(Model model, @PathVariable("id") int id) {
		model.addAttribute("dateCalc", service.selectOne(id));
		return "update";
	}
	@PostMapping("/update/id={id}")
	public String runUpdate(Model model, @Validated @ModelAttribute DateCalc dateCalc, BindingResult bindingresult) {
		if(bindingresult.hasErrors()) {
			System.out.println("エラー発生！BindingResult内容↓↓↓");
			System.out.println(bindingresult);
			System.out.println("エラー発生！BindingResult内容↑↑↑");
			model.addAttribute("dateCalc", dateCalc);
			return "/update";
		}
		
		service.updateOne(dateCalc);
		return "redirect:/";
	}
	
	
	//削除
	@PostMapping("delete/id={id}")
	public String deleteOne(@ModelAttribute DateCalc dateCalc) {
		service.deleteOne(dateCalc);
		return "redirect:/";
	}
	

}

```

ところどころ、コンソールで確認するための記載はコメントアウトしています。
計算結果の日付について、配列はresultdateArray、Map型はresultdateMapへ格納しています。

バリデーション処理実装時に最も詰まったのが「Controllerクラスにおいて変数名をどう設定するか」です。
当初はHTMLファイル内のth:if="${#fields.hasErrors('')}が常にfalseとなりエラーメッセージが表示されませんでした。
原因は、DateCalcの変数名を「dc」と設定していたためです。
BindingResultのエラーメッセージを見ると分かるのですが、BindingResultは以下のような形式で自動的にEntityクラスの名前を生成しエラーを探しているようです。
　例）DateCalc　→　dateCalc
よって、「dateCalc」という変数名を設定しHTMLファイルに渡す必要があります。

以下のteratailを参考にさせていただきました。ありがとうございました！
https://teratail.com/questions/171010


あとはブラウザに（ http://www.localhost:8080/ ）
と入力すればログインページが表示されるはずです。

お読みいただきありがとうございました！