はじめに
自身の知識のアウトプットも兼ねて、新人研修用に作成した記事となります。Spring Bootを学び始めた方を対象とした内容になっています。
前回までの記事で、企業情報を管理するシンプルなCRUDアプリケーションを作成しました。
今回はここに企業情報だけでなく、社員情報も登録できるように機能を追加していきます。
概要
前回までで作成した企業情報の登録機能を土台に、社員情報も一緒に登録できるように機能を追加していきます。
完成イメージ
企業情報登録フォームに社員情報の登録欄を追加します。
任意の企業情報と社員情報を入力し、登録ボタンを押すと登録処理が走ります。
正常に登録が完了すると企業情報一覧画面へとリダイレクトされ、先ほど登録した企業情報が一覧に追加されます。(ここは前回までから変更なし)
データベースを確認すると、企業テーブルに登録した企業情報が正常に追加されていることが確認できます。
また、社員テーブル(※)には先ほど登録した人数分の社員情報が、企業IDに紐づいた形で登録されていることが確認できます。
※社員テーブルは新規に作成します。
ER図
パッケージ構成
赤枠で囲ったパッケージ、クラス、HTMLファイルを今回作成していきます。また、青枠で囲ったクラスを編集していきます。
Entityクラスの作成
Entityクラスでは、@OneToMany
と @ManyToOne
アノテーションを使って、Company
とEmployee
の間の一対多の関係を設定していきます。
Company
は複数のEmployee
を持ち、各Employee
は必ず一つのCompany
に属します。親子関係が設定されているため、親の操作が子に影響を与えるようになっています。これにより、データベースの整合性が保たれ、関連するデータの管理が容易になります。
企業エンティティ
先ずは、company
テーブルとemployee
テーブルを関連付ける為に、Company.java
にフィールドを追加していきます。
配置先:src > main > java > com > example > demo > entity > Company.java
package com.example.demo.entity;
import java.util.Date;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Company {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long companyId; // 企業ID
@NotBlank
private String name; // 会社名
@NotNull
private int employees; // 従業員数
@Temporal(TemporalType.DATE)
private Date establishmentDate; // 設立日
// ここを追加
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL) //1対多
private List<Employee> employee;
}
@OneToMany
このアノテーションは、一つの会社(Company
)が複数の従業員(Employee
)を持つことを示しています。
mappedBy = "company"
Employee
エンティティの中にあるcompany
フィールドが、このリレーションシップのオーナーであることを示しています。つまり、Employee
がCompany
に対して外部キーを持っているということです。
cascade = CascadeType.ALL
これは、親エンティティ(Company
)に対する操作(保存、更新、削除など)が、子エンティティ(Employee
)にも伝播することを意味します。例えば、Company
を削除すると、その関連するEmployee
もすべて削除されます。
社員エンティティ
次に子エンティティにあたるEmployee.java
を新たに作成します。
このEntityクラスを作成することで、アプリケーション起動時に自動でクラスの定義に基づいたemployee
テーブルがDB上に作成されます。
配置先:src > main > java > com > example > demo > entity > Employee.java
package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 社員ID
private String name; // 氏名
private String position; // 役職
@ManyToOne //多対1
@JoinColumn(name = "company_id", nullable = false)
private Company company; // 外部キー (企業ID)
}
@ManyToOne
このアノテーションは、複数の従業員(Employee
)が一つの会社(Company
)に属することを示しています。つまり、複数のEmployee
が同じCompany
を参照することができます。
@JoinColumn(name = "company_id", nullable = false)
これは、Employee
テーブルにおいて、company_id
というカラムが外部キーとして使用されることを示しています。このカラムはCompany
テーブルの主キーを参照します。
また、nullable = false
は、このカラムが必ず値を持つ必要があることを示しています。つまり、Employee
は必ずどこかのCompany
に属していなければならないということです。
Repositoryクラスの作成
社員情報のデータ管理用に新しくRepositoryクラスを作成します。
配置先:src > main > java > com > example > demo > repository > EmployeeRepository.java
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.Employee;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
Formクラスを作成
社員情報の管理用に新しくFormオブジェクトを作成します。
配置先:src > main > java > com > example > demo > controller > form > EmployeeForm.java
package com.example.demo.controller.form;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeForm {
@Size(max = 255)
@Pattern(regexp="^[ぁ-んァ-ヶア-ン゙゚一-龠]*$", message="名前は漢字、ひらがな、またはカタカナで入力してください。")
private String name; // 名前
@Size(max = 255)
@Pattern(regexp="^[ぁ-んァ-ヶア-ン゙゚一-龠]*$", message="役職は漢字、ひらがな、またはカタカナで入力してください。")
private String position; // 役職
}
また、CompanyForm
に社員リストのフィールドを追加します。
配置先:src > main > java > com > example > demo > controller > form > CompanyForm.java
package com.example.demo.controller.form;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CompanyForm {
private Long companyId; // 企業ID
@NotBlank(message = "会社名は必須です")
@Size(max = 255)
private String companyName; // 会社名
@NotBlank(message = "従業員数は必須です")
@Min(value = 0, message = "従業員数は0以上の数字を入力してください")
private String employees; // 従業員数
@NotBlank(message = "設立日は必須です")
private String est; // 設立日
// ここを追加
@Valid
private List<EmployeeForm> employeeList; // 社員リスト
}
List<EmployeeForm>
をフィールドとして持つことで、1つのフォーム(CompanyForm
)を通じて、企業と社員のデータを一括して扱えるようになります。
また、リストで持つことで、1つの企業に関連する複数の社員情報(EmployeeForm
インスタンス)を一緒に扱えるようになります。
更に、List<EmployeeForm>
を CompanyForm
に含めることで、Thymeleafを使って社員情報を入力フォームに対応させ、送信後にそのデータを employeeList
フィールドに自動的にバインドできるようになります。
Serviceクラスの修正
登録処理部分のビジネスロジックを修正します。
企業情報の登録と一緒に、社員情報も登録できるように処理を追加していきます。
配置先:src > main > java > com > example > demo > service > CompanyService.java
@Service
public class CompanyService {
// 企業情報の登録処理
@Transactional
public Company createCompany(CompanyForm form) {
// 企業情報の登録処理 (変更はないため省略)
// ここを追加
/* 社員情報をセット START */
// フォームの社員情報リストをエンティティのリストに変換
List<Employee> employeeList = new ArrayList<>();
for (EmployeeForm empForm : form.getEmployeeList()) {
// Employeeオブジェクトを作成してフォームの情報を設定
Employee employee = new Employee();
employee.setName(empForm.getName());
employee.setPosition(empForm.getPosition());
employee.setCompany(entity); // 親エンティティ(企業)を設定
employeeList.add(employee); // リストに追加
}
// エンティティに社員リストをセット
entity.setEmployee(employeeList);
/* 社員情報をセット END */
// 企業情報と社員情報をまとめてDB登録
return repository.save(entity);
}
}
ここでは、社員情報リストの中身を1つずつ取り出して、empForm
という名前で参照しながら処理を行うようにしています。
Controllerクラスの修正
社員情報も登録できるように、各メソッドに処理を追加していきます。
配置先:src > main > java > com > example > demo > controller > CompanyController.java
登録フォームの表示
先ずは登録フォーム表示時に、社員情報の登録フォームも表示されるように処理を追加します。
@Controller
public class CompanyController {
@Autowired
CompanyService companyService;
// 登録フォームの表示
@GetMapping("company/register")
public String showForm(Model model) {
// 社員リストを初期化
CompanyForm companyForm = new CompanyForm();
// 5人分だけ空の EmployeeForm を用意する
int employeeCount = 5;
List<EmployeeForm> employeeList = new ArrayList<>();
// 従業員数分だけ空の EmployeeForm をリストに追加
for (int i = 0; i < employeeCount; i++) {
employeeList.add(new EmployeeForm());
}
// companyForm にセット
companyForm.setEmployeeList(employeeList);
model.addAttribute("companyForm", companyForm);
return "register";
}
}
ここでは、必要な従業員数分(今回は5人分)だけ EmployeeForm
の空インスタンスをリストに追加し、最後にこのリストを companyForm
の employeeList
プロパティにセットしています。
新規登録処理
次に、登録処理のメソッドについても修正していきます。
今回は企業情報と社員情報を個別に処理するのではなく、同じフォームデータ (CompanyForm
) として、1つの単位としてまとめて管理している為、コードの修正はほとんど必要ありません。
ただし、このまま登録しようとすると社員情報の入力フォームが空欄だった際にも、データベースに空データとして登録されてしまいます。その為、空の社員情報を削除する処理を追加します。
@Controller
public class CompanyController {
// 企業情報の新規登録
@PostMapping("company/register")
public String addCompany(@ModelAttribute @Valid CompanyForm companyForm,
BindingResult bindingResult, Model model) {
// バリデーションチェック
if(bindingResult.hasErrors()) {
return "register";
}
/* 空の社員情報を削除する処理 START */
List<EmployeeForm> validEmployees = new ArrayList<>();
for (EmployeeForm emp : companyForm.getEmployeeList()) {
// 名前または役職が空の場合はスキップ
if (emp.getName().isBlank() || emp.getPosition().isBlank()) {
continue;
}
validEmployees.add(emp); // 有効な社員情報をリストに追加
}
// 有効な社員情報だけをセット
companyForm.setEmployeeList(validEmployees);
/* 空の社員情報を削除する処理 END */
// エラーがない場合、企業情報と社員情報を保存
companyService.createCompany(companyForm);
// 一覧画面へリダイレクト
return "redirect:/company/lists";
}
}
ここでは、フォームから渡ってきた社員情報リストの中身を確認し、名前
もしくは 役職
が空欄でない有効な社員情報を保存する新しいリスト (validEmployees
) を companyForm
の社員リストとして設定しています。
こうすることで、必要なデータのみをデータベースに登録することができます。
Viewの修正
社員情報の入力欄を追加します。
配置先:src > main > resources > templates > register.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>新規登録画面</title>
</head>
<body>
<h1>企業情報登録フォーム</h1>
<a th:href="@{/company/lists}">一覧画面へ戻る</a>
<form th:action="@{/company/register}" th:object="${companyForm}" method="post">
<table>
<!-- 企業情報の入力欄 (省略) -->
</table>
<br>
<!-- 社員情報の入力欄追加 START -->
<table>
<tr>
<th>社員情報</th>
</tr>
<tr>
<th></th>
<th>社員名</th>
<th>役職</th>
</tr>
<tr th:each="employee, iterStat : *{employeeList}">
<td><strong>社員 [[${iterStat.index + 1}]]</strong></td>
<td>
<input type="text" th:field="*{employeeList[__${iterStat.index}__].name}" />
</td>
<td>
<input type="text" th:field="*{employeeList[__${iterStat.index}__].position}" />
</td>
<td>
<div th:if="${#fields.hasErrors('employeeList[__${iterStat.index}__].name')}" th:errors="*{employeeList[__${iterStat.index}__].name}">Name Error</div>
<div th:if="${#fields.hasErrors('employeeList[__${iterStat.index}__].position')}" th:errors="*{employeeList[__${iterStat.index}__].position}">Position Error</div>
</td>
</tr>
</table>
<!-- 社員情報の入力欄追加 END -->
<input type="submit" value="登録する" />
</form>
</body>
</html>
th:each="employee, iterStat : *{employeeList}"
Thymeleaf
のテンプレートエンジンで繰り返し処理を行うための構文です。このコードを分解して解説します。
th:each
-
繰り返し処理を行うための属性です。
-
配列やリストなどのコレクションをループで処理できます。
employee, iterStat : *{employeeList}
-
*{employeeList}
は、現在のオブジェクト (companyForm
) のemployeeList
プロパティ (社員情報のリスト) を参照しています。 -
employee
は、employeeList
の各要素を1つずつ取り出した際の一時的な変数名です。ループ内で、現在の社員情報をemployee
で参照できます。 -
iterStat
は、ループの状態を示すステータス変数 (iteration status variable
) です。この変数を使うと、次の情報を取得できます:
ステータス変数の後につけるプロパティ | 意味 |
---|---|
index | 現在のループのインデックス(0開始) |
count | 現在のループの回数(1開始) |
size | ループするリストの全体の要素数 |
current | 現在のループ処理で使っている要素値 |
odd | 現在のループが奇数回かどうかの真偽値 |
even | 現在のループが偶数回かどうかの真偽値 |
first | 現在のループが最初かどうかの真偽値 |
last | 現在のループが最後かどうかの真偽値 |
自分で決めたステータス変数名.プロパティ
の形式で色々と応用が利きます。
データの流れ
社員情報に焦点を当てて考えた場合、登録処理時の大まかなデータの流れは以下の通りです。
- ユーザーが社員情報を入力します
- フォームが送信され、サーバー側でバリデーションと空欄チェックが行われます
- 空欄の社員情報はリストから削除され、保存処理に進みます
- 空欄の社員情報が削除された状態で、企業情報と有効な社員情報のみが保存されます
参考サイト