この記事の目的
まず大前提にこの記事を書いた理由として自分自身の学習のOUTPUTです。
そもそも自分自身Springを勉強し始めて約1ヶ月程度で有り、そこまで詳しくありません。
そのためSpring初学者が見て勉強するための記事ではあまり無いかも知れないです。
なるべく自分がやったことをわかりやすく書いたつもりですが、ある程度Springの知識が無いと読みづらい記事になっています。
成果物
今回最終的に作成した成果物は以下になります。
今後、この成果物にさらなる機能を実装した記事を投稿する予定です。
実装した機能
今回、実装した機能は大きく2つあります。
1. 画面から入力された値をDBへ取り込み画面に表示(登録機能)
2. 既に登録されている値を編集してDBへ取り込み再び画面に表示(更新機能)
この2つの機能実装をメインにプログラムを作成します。
前提条件
1. 今回作成に利用したソフトのバージョンは以下の通りです。
(1) Spring Boot ver 2.7.5
(2) MySQL ver8.0.31
・MySQLのバージョン確認はMySQL Shellで \s と入力すれば調べられます。
2. 設定した依存関係は以下の通りです。(今回はGradleを利用します。)
・O/RマッパーにはSpring Data JDBCを選択しました。
データベース
今回使用したデータベースは以下になります。
今後、このデータベースも少しずつ変更していきます。
Sampleテーブル
ID | NAME | GENDER | AGE | |
---|---|---|---|---|
1 | 1 | Saito | 1 | 20 |
2 | 2 | Suzuki | 1 | 21 |
3 | 3 | Tanaka | 1 | 22 |
CREATE TABLE Sample (
Id INT PRIMARY KEY,
Name VARCHAR2(50) NOT NULL,
Gender INT NOT NULL,
Age INT NOT NULL
);
INSERT INTO Sample VALUES(1, 'Saito', 1, 20); // 0='男性', 1='女性'
INSERT INTO Sample VALUES(2, 'Suzuki', 1, 21); // 0='男性', 1='女性'
INSERT INTO Sample VALUES(3, 'Tanaka', 1, 22); // 0='男性', 1='女性'
・性別に関しては、今後のプログラミングで0=男性、1=女性として紐付けます
・INSERT文であらかじめデータを入力しておきます
プログラムの作成
では、実際にプログラムの作成に入って行きますが、今回は以下の通りに進めます。
1. 接続設定
2. インストラクチャ層(データベースの操作処理)の設定
3. ドメイン層(サービス処理)の設定
4. アプリケーション層(ビュー、コントローラー)の設定
の流れで進めて行きます。
1. 接続設定
ここでは既にデータベースを作成しているので、パッケージエクスプローラー内にあるapplication.propertiesに情報を記述するのみで完了します。
spring.datasource.url=jdbc:mysql://localhost:3306
spring.datasource.username=root
spring.datasource.password=********
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
データベース名、ユーザー名、パスワードには各自必要な情報を入力します。
2. インストラクチャ層(データベースの操作処理)の設定
ここでは、DBからデータを取り込み、プログラム内で操作できるように設定していきます。
ここでの流れは以下のように進めて行きます。
(1) Entityクラスの作成
(2) Repositoryインターフェースの作成
(1) Entityクラスの作成
Entityはデータベース内のテーブル1行に対応するクラスです。コードは以下になります。
package com.example.demo.entity;
import org.springframework.data.annotation.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data // Getter,Setterが不要になる
@NoArgsConstructor // デフォルトコンストラクターの自動生成
@AllArgsConstructor // 全フィールドに対する初期化値を引数に取るコンストラクタを自動生成
public class Sample {
@Id // 主キーに当たるフィールドに付与する(今回はid)に付与
private Integer id;
private String name;
private Integer gender;
private Integer age;
}
(2) Repositoryインターフェースの作成
次にRepositoryはデータベースへのデータ操作を行うクラスです。
Repositroyを作成する場合には、必ずインターフェースを定義した上で実装します。コードは以下になります。
package com.example.demo.repository;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import com.example.demo.entity.Sample;
public interface SampleCrudRepository extends CrudRepository<Sample, Integer> {
@Modifying
@Query(value = "INSERT INTO sample VALUES(:id, :name, :gender, :age)")
void InsertData(@Param("id") Integer id,
@Param("name") String name,
@Param("gender") Integer gender,
@Param("age") Integer age);
}
Repositoryは「CrudRepository」を継承したモノを作成します。
型引数にはEntityの型である「Sample」とEntityクラス作成の際に@Idアノテーションを付与したフィールドの型(今回@Idを付与したフィールドidはInteger型であるため、Integer記述)を順に指定します。
「Spring Data」が提供する「CrudRepository」を継承することで自動的にCRUDメソッドをSampleCrudRepositoryインターフェース内に記述しなくても使用できます。以下に継承することで使用できるメソッドを乗せておきます。
ただし、「CrudRepository」に提供されていないCRUDメソッドを使用したい場合、自分自身で記述しなければなりません。
今回は「InsertData」メソッドを自分で記述しました。
自分自身で記述した理由として、「CrudRepository」を継承することで利用できるメソッドの中に「save」メソッドがあります。このメソッドは先ほどEntityクラス作成時に利用した@Idが付与されているフィールドがnullの場合Insert、nullでない場合Updateに処理が変わります。
例を出して説明すると、@Idが付与されたフィールド「a」、それ以外のフィールドb, cをデータベース内に挿入したい場合
save(1, 2, 3) // (1)a=1, b=2, c=3とした場合 Update文
save(null, 2, 3) // (1)a=null, b=2, c=3とした場合 Insert文
最初のメソッドの処理としては、a=1のb,cそれぞれの値を2,3に変更するUpdate文を実行するメソッドです。
2番目のメソッドの処理としては、新たにa=1,b=2,c=3のデータを挿入するInsert文を実行するメソッドです。
しかし、@Idはテーブルの主キーとなるフィールドに付与しました。その「a」がnullの場合、このメソッドはエラーになってしまいます。
そのため、Insert文のみを実行できるように自分自身でメソッドを作成しました。
注意点としては、自分で作成したメソッドが今回のようにInsert、Update、Delete処理をするものだった場合、@Modifyingアノテーションを付与しないと「クリエは結果を返しませんでした。」のようなエラーが発生してしまいます。
↑はTeratailで私が投稿した質問
長くなりましたが、(2) Repositoryインターフェースの作成
のポイントとしては2つです。
①基本的には、「CrudRepository」を継承することで自動的に利用できるCRUDメソッドを使用する。
②「CrudRepository」にないメソッドは自分自身で作成する
3. ドメイン層(サービス処理)の設定
ここでは、実際に使用する処理内容を記述していきます。ここでの流れは以下のように進めて行きます。
(1) SampleServiceインターフェースの作成
(2) SampleServiceImplクラスの作成(SampleServiceインターフェースの実装クラス)
(1) SampleServiceインターフェースの作成
ここでは今回のシステムで使用するの抽象メソッドのみを記述します。
理由としてはSpringフレームワークの特徴であるDIを活かすためです。
package com.example.demo.service;
import java.util.Optional;
import com.example.demo.entity.Sample;
public interface SampleService {
/* 全件取得 */
Iterable<Sample> SelectAll();
/**
* id(主キー)をキーにして1件取得する
* 戻り値型はOptional型を使用
* isPresent()を使用でき、値がある場合はtrueを返す
*/
Optional<Sample> SlectOneById(Integer id);
/* 取得したデータをDBにInsertする */
void InsertSample(Sample sample);
/* データを更新する */
void UpdateSample(Sample sample);
}
(2) SampleServiceImplクラスの作成(SampleServiceインターフェースの実装クラス)
ここでは先ほど記述したSampleServiceインターフェースの抽象メソッドを実装していきます。
先ほど作成したSampleCrudRepositoryインターフェースを利用したいので@Autowiredアノテーションを書いておきます。
@Service
@Transactional
public class SampleServiceImpl implements SampleService {
@Autowired
SampleCrudRepository repository;
@Override
public Iterable<Sample> SelectAll() {
return repository.findAll();
}
@Override
public Optional<Sample> SlectOneById(Integer id) {
return repository.findById(id);
}
@Override
public void InsertSample(Sample sample) {
repository.InsertData(sample.getId(),
sample.getName(),
sample.getGender(),
sample.getAge());
}
@Override
public void UpdateSample(Sample sample) {
repository.save(sample);
}
}
ここではクラスに@Transactionalアノテーションを付与しています。
このクラスにアノテーションを付与する理由として、このクラスがサービス処理の入口と捉えることができるからです。
これを付与することでクラス内のメソッド全てにトランザクション制御をかけることができます。このアノテーションはメソッド単位で付与できます。トランザクション設定が必要なのは挿入・更新・削除などのDLM文ですが、漏れをなくすためにクラスに付与することが推奨されています。
4. アプリケーション層(ビュー、コントローラー)の設定
最後にアプリケーション層の作成を行っていきます。ここでは以下のように進めて行きます。
(1) SampleFormクラスの作成
(2) show.htmlの作成
(3) SampleControllerクラスの作成
(1) SampleFormクラスの作成
ここではView(show.html)から入力値を受け取るプログラムを作成します。
このFormクラスでValidationを記述することにより、意図しない値がViewから入力されることを防ぐことができます。
コードは以下になります。
package com.example.demo.form;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import lombok.Data;
@Data
public class SampleForm {
@NotNull
@Min(0) // 0以上の値を入力可能
private Integer id;
@NotBlank
@Length(min=1, max=100) // 1~100文字の範囲で入力可能
private String name;
private Integer gender;
@NotNull
@Range(min=0, max=120) // 1~120歳の範囲で入力可能
private Integer age;
/* 「登録」か「更新」判定用 */
private Boolean NewSample;
}
性別「gender」に関しては、「0」を受け取ったら男性、「1」を受け取ったら女性とします。
値が「0」か「1」の2択のため、入力はラジオボタンでできるようにします。(3) SampleControllerクラスの作成の設定で「gender」は「0」で初期値を設定するため、バリデーションを付けなくても意図しない値が入ることはありません。
判定用「NewSample」も「gender」と同様に初期値の設定を行うので、ここではバリデーションの必要はありません。
また、エラーメッセージに関しては、別のファイルにまとめて記述するためここでは書きません。(今回の記事ではエラーメッセージは記述しません)
(3) show.htmlの作成
次にViewとなるshow.htmlを作成します。コードは以下になります。
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サンプル</title>
</head>
<body>
<table border="1" th:unless="${#lists.isEmpty(list)}" style="table-layout: fixed;">
<tr>
<th>ID</th>
<th>名前</th>
<th>性別</th>
<th>年齢</th>
<th>編集</th>
</tr>
<tr th:each="obj : ${list}" align="center">
<td th:text="${obj.id}"></td>
<td th:text="${obj.name}"></td>
<td th:text="${obj.gender} == 0? '男性' : '女性'"></td>
<td th:text="${obj.age}"></td>
<!-- 編集ボタン -->
<td>
<form method="get" th:action="@{sample/{id}(id=${obj.id})}">
<input type="submit" value="編集">
</form>
</td>
</tr>
</table>
<!-- 新規登録 -->
<!-- newSampleがtrueの場合、新規登録処理、そうではない場合、更新処理にそれぞれ変更します -->
<form method="post"
th:action="${sampleForm.NewSample}? @{/sample/insert} : @{/sample/update}"
th:object="${sampleForm}">
<hr>
<label>ID :</label>
<input type="number" th:field="*{id}">
<br>
<label>名前:</label>
<input type="text" th:field="*{name}">
<br>
<label>男性:</label>
<input type="radio" value=0 th:field="*{gender}">
<label>女性:</label>
<input type="radio" value=1 th:field="*{gender}">
<br>
<label>年齢:</label>
<input type="number" th:field="*{age}">
<br>
<input type="submit" value="送信">
</form>
<!-- 追加部分 -->
<p th:unless="${sampleForm.newSample}">
<a href="#" th:href="@{/sample}">一覧画面へ戻る</a>
</p>
</body>
ポイントとしては、
- th:unless="${#lists.isEmpty(~~~)}"と記述することで、データが1件でもあれば、一覧を表示します。
- th:text="${obj.gender} == 0? '男性' : '女性'"と記述することで、値が1の時は「男性」、それ以外の時は「女性」と表示します。
- th:action="@{sample/{id}(id=${obj.id})}"と記述することで、URLの一部を${obj.id}から取得した値に変更することができます。
- th:action="${sampleForm.NewSample}? @{/sample/insert} : @{/sample/update}"と記述することで、NewSampleの値によって登録・更新処理を分けることができます。これにより入力フォーム1つだけで2つの処理を行うことができます。
- th:field=と記述することで、id=, name=, value=と記述した際と同様になります。
(3) SampleControllerクラスの作成
最後に画面遷移やデータの受け渡しに関する内容を記述します。コードは以下になります。
コードが少し多いため、簡単な説明はコメントアウトした内容に記述しますが、詳しい内容は別途コード下に記述します。
package com.example.demo.controller;
import java.util.Optional;
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;
import com.example.demo.entity.Sample;
import com.example.demo.form.SampleForm;
import com.example.demo.service.SampleService;
@RequestMapping("/sample")
@Controller
public class SampleController {
@Autowired
SampleService service;
/* 「form-backing bean」の初期化 */
@ModelAttribute
public SampleForm setUpForm() {
SampleForm form = new SampleForm();
form.setGender(0);
return form;
}
/* 一覧表示 */
@GetMapping
public String ShowList(SampleForm sampleForm, Model model) {
sampleForm.setNewSample(true); // 新規登録設定(初期値をtrueとしておく)
Iterable<Sample> sample = service.SelectAll(); // DB内のデータを全件取得
model.addAttribute("list", sample);
return "show";
}
/* 登録機能作成 */
@PostMapping("/insert")
public String insert(@Validated SampleForm sampleForm, BindingResult bindingResult,
Model model, RedirectAttributes redirectAttributes) {
/* FormからEntityに詰め替え */
Sample sample = new Sample();
sample.setId(sampleForm.getId());
sample.setName(sampleForm.getName());
sample.setGender(sampleForm.getGender());
sample.setAge(sampleForm.getAge());
/* 入力チェック */
if( !bindingResult.hasErrors()) {
service.InsertSample(sample);
return "redirect:/sample";
} else {
/* エラーがある場合は、一覧表示処理を呼ぶ */
return ShowList(sampleForm, model);
}
}
/* 更新用画面の表示 */
@GetMapping("/{id}")
public String ShowUpdate(SampleForm sampleForm, @PathVariable Integer id, Model model) {
/* 表示する行を取得 */
Optional<Sample> sampleOpt = service.SlectOneById(id);
/* SampleFormに入れ直す */
Optional<SampleForm> sampleFormOpt = sampleOpt.map(t -> makeSampleForm(t));
/* SampleFormがnullでなければ中身を取り出す */
if(sampleFormOpt.isPresent()) {
sampleForm = sampleFormOpt.get();
}
makeUpdateModel(sampleForm, model);
return "show";
}
/* 更新用のModelを作成する */
private void makeUpdateModel(SampleForm sampleForm, Model model) {
model.addAttribute("id", sampleForm.getId());
sampleForm.setNewSample(false);
model.addAttribute("sampleForm", sampleForm);
}
/* idをキーにしてデータを更新する */
@PostMapping("/update")
public String update(@Validated SampleForm sampleForm, BindingResult result,
Model model, RedirectAttributes redirectAttributes) {
Sample sample = MakeSample(sampleForm);
if(!result.hasErrors()) {
service.UpdateSample(sample);
return "redirect:/sample/" + sample.getId();
} else {
makeUpdateModel(sampleForm, model);
return "show";
}
}
private Sample MakeSample(SampleForm sampleForm) {
Sample sample = new Sample();
sample.setId(sampleForm.getId());
sample.setName(sampleForm.getName());
sample.setGender(sampleForm.getGender());
sample.setAge(sampleForm.getAge());
return sample;
}
/**
* makeQuizFormはquiz(エンティティクラス(DB関連クラス))からQuizForm(Formクラス)へ
* 値を代入しformを返している。
*/
private SampleForm makeSampleForm(Sample sample) {
SampleForm form = new SampleForm();
form.setId(sample.getId());
form.setName(sample.getName());
form.setGender(sample.getGender());
form.setAge(sample.getAge());
form.setNewSample(false);
return form;
}
}
@ModelAttribute
public SampleForm setUpForm() {
SampleForm form = new SampleForm();
form.setGender(0);
return form;
}
/* 「form-backing bean」の初期化 */
に関して
バリデーションを行う際に「form-backing bean」の設定が必要になります。
HTMLの「formタグ」にバインドする「Formクラス」インスタンスを「form-backing bean」と呼び、@ModelAttributeアノテーションを使うことで結びつけることができます。
「form-backing bean」の初期化は、@ModelAttributeアノテーションを付与したメソッドを作成し、HTMLの「formタグ」にバインドしたい「Fromクラス(今回はSampleFormクラス)」を初期化して戻り値で返します。
また、今回は初期化の際に、性別を「0」(男性)にしておきます。
@GetMapping
public String ShowList(SampleForm sampleForm, Model model) {
sampleForm.setNewSample(true); // 新規登録設定(初期値をtrueとしておく)
Iterable<Sample> sample = service.SelectAll(); // DB内のデータを全件取得
model.addAttribute("list", sample);
return "show";
}
/* 一覧表示 */
に関して
http://localhost:8080/sample と入力した際に「form-backing bean」処理の実行後、上記の処理が行われます。
ここではDBから取得したデータをViewのThymeleafと連携するための「Modelインターフェース」の「addAtribute」メソッドに格納します。また、「setNewSample」の初期値を「true」としておきます。
@PostMapping("/insert")
public String insert(@Validated SampleForm sampleForm, BindingResult bindingResult,
Model model, RedirectAttributes redirectAttributes) {
/* FormからEntityに詰め替え */
Sample sample = new Sample();
sample.setId(sampleForm.getId());
sample.setName(sampleForm.getName());
sample.setGender(sampleForm.getGender());
sample.setAge(sampleForm.getAge());
/* 入力チェック */
if( !bindingResult.hasErrors()) {
service.InsertSample(sample);
return "redirect:/sample";
} else {
/* エラーがある場合は、一覧表示処理を呼ぶ */
return ShowList(sampleForm, model);
}
}
/* 登録機能作成 */
に関して
SampleControllerに登録処理を記述します。
@Validatedアノテーションを単項目チェックアノテーションを設定しているFormクラス(SampleForm)に付与することでバリデーションを実行します。
実行した結果(エラー情報)がBindingResultインターフェースに保持され、bindingResult.hasErrors()メソッドの戻り値でエラーの有無を確認することができます。
bindingResult.hasErrors() == true → エラー有り
bindingResult.hasErrors() == false → エラー無し
エラーがある場合は、ShowListメソッドを呼び、再びshow.htmlに戻ります。
エラーがない場合は、入力された値がEntityクラスに挿入され、再びshow.htmlに戻ります。
/* 変更前 */
if( !bindingResult.hasErrors()) {
service.InsertSample(sample);
return "redirect:/sample";
} else {
return ShowList(sampleForm, model);
}
}
}
/* 変更後 */
if( !bindingResult.hasErrors()) {
service.InsertSample(sample);
}
return ShowList(sampleForm, model);
}
再び画面に戻る処理は、変更前のように記述しましたが、変更後のような書き方をしても大丈夫です。
ただし、記事の始めの方でも説明しましたが、このプログラムは後々改良を加えていくためそれを加味して変更前のような書き方をしています。
@GetMapping("/{id}")
public String ShowUpdate(SampleForm sampleForm, @PathVariable Integer id, Model model) {
/* 表示する行を取得 */
Optional<Sample> sampleOpt = service.SlectOneById(id);
/* SampleFormに入れ直す */
Optional<SampleForm> sampleFormOpt = sampleOpt.map(t -> makeSampleForm(t));
/* SampleFormがnullでなければ中身を取り出す */
if(sampleFormOpt.isPresent()) {
sampleForm = sampleFormOpt.get();
}
/* 更新用のModelを作成する */
makeUpdateModel(sampleForm, model);
return "show";
}
private SampleForm makeSampleForm(Sample sample) {
SampleForm form = new SampleForm();
form.setId(sample.getId());
form.setName(sample.getName());
form.setGender(sample.getGender());
form.setAge(sample.getAge());
form.setNewSample(false);
return form;
}
/* 更新用画面の表示 */
に関して
ここでは選択したデータを更新するためのページを表示します。
@GetMapping("/{id}")の{id}はプレースホルダーであり、URLに埋め込まれた値が格納されます。
ShowUpdateメソッド内の引数「@PathVariable Integer id」により、@PathVariableを付けたプレースホルダーと同じ変数名を指定することで、プレースホルダーに格納されている値が@PathVariableを付けた変数に格納されます。
ex) {id(== 1)}が格納されている場合、@PathVariable Integer id = 1
Optional sampleOpt = service.SlectOneById(id);により、「id」と一致する値を取得します。
Optional sampleFormOpt = sampleOpt.map(t -> makeSampleForm(t));
ここで使用されているmapメソッドはOptionalメソッドで、値があるときだけ値を「何か」に詰め直します。
map(t -> makeSampleForm(t))ではラムダ式を使用していますが、makeSampleFormメソッドを使用して「Sample」から「SampleForm」に値を詰め直しています。
その結果、SampleFormがnull(sampleFormOpt.isPresent == true)で無ければ、sampleFormOptから値を取り出します。
最終的に更新用のModelを作成し、画面(show.html)に戻ります。
private void makeUpdateModel(SampleForm sampleForm, Model model) {
model.addAttribute("id", sampleForm.getId());
sampleForm.setNewSample(false);
model.addAttribute("sampleForm", sampleForm);
}
ここまでが編集ボタンをクリックしてから画面に値を表示するまでの処理です。
最後に更新を行う処理は、登録を行う処理と変わらないので簡単な解説に留めます。
画面から入力された値をMakeSampleメソッドを用いてFormクラスからEntityクラスへ値を詰め替えし、エラーがないか入力チェックを行います。
@PostMapping("/update")
public String update(@Validated SampleForm sampleForm, BindingResult result,
Model model, RedirectAttributes redirectAttributes) {
Sample sample = MakeSample(sampleForm);
if(!result.hasErrors()) {
service.UpdateSample(sample);
return "redirect:/sample/" + sample.getId();
} else {
makeUpdateModel(sampleForm, model);
return "show";
}
}
private Sample MakeSample(SampleForm sampleForm) {
Sample sample = new Sample();
sample.setId(sampleForm.getId());
sample.setName(sampleForm.getName());
sample.setGender(sampleForm.getGender());
sample.setAge(sampleForm.getAge());
return sample;
}
これらはupdateメソッドからMakeSampleメソッドを読んでいますが、1つにまとめることもできます。
@PostMapping("/update")
public String update(@Validated SampleForm sampleForm, BindingResult result,
Model model, RedirectAttributes redirectAttributes) {
Sample sample = new Sample();
sample.setId(sampleForm.getId());
sample.setName(sampleForm.getName());
sample.setGender(sampleForm.getGender());
sample.setAge(sampleForm.getAge());
if(!result.hasErrors()) {
service.UpdateSample(sample);
return "redirect:/sample/" + sample.getId();
} else {
makeUpdateModel(sampleForm, model);
return "show";
}
}
参考文献
最後に今回参考にした参考書を貼って置きます。
後書き
ここまで読んでくれた人はいないとおもいますが、読んでくれた人はありがとうございます。
改良版は随時書いていきたいと思っているので、良ければ自分のOUTPUTに付き合ってください。