0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Spring Boot入門】#9 1対多の関係テーブルにおける登録機能を作成する

Posted at

はじめに

自身の知識のアウトプットも兼ねて、新人研修用に作成した記事となります。Spring Bootを学び始めた方を対象とした内容になっています。

前回までの記事で、企業情報を管理するシンプルなCRUDアプリケーションを作成しました。

今回はここに企業情報だけでなく、社員情報も登録できるように機能を追加していきます。

概要

前回までで作成した企業情報の登録機能を土台に、社員情報も一緒に登録できるように機能を追加していきます。

完成イメージ

企業情報登録フォームに社員情報の登録欄を追加します。

image.png

任意の企業情報と社員情報を入力し、登録ボタンを押すと登録処理が走ります。

image.png

正常に登録が完了すると企業情報一覧画面へとリダイレクトされ、先ほど登録した企業情報が一覧に追加されます。(ここは前回までから変更なし)

image.png

データベースを確認すると、企業テーブルに登録した企業情報が正常に追加されていることが確認できます。

image.png

また、社員テーブル(※)には先ほど登録した人数分の社員情報が、企業IDに紐づいた形で登録されていることが確認できます。
※社員テーブルは新規に作成します。

image.png

ER図

image.png

パッケージ構成

赤枠で囲ったパッケージ、クラス、HTMLファイルを今回作成していきます。また、青枠で囲ったクラスを編集していきます。

image.png

Entityクラスの作成

Entityクラスでは、@OneToMany@ManyToOne アノテーションを使って、CompanyEmployeeの間の一対多の関係を設定していきます。

Companyは複数のEmployeeを持ち、各Employeeは必ず一つのCompanyに属します。親子関係が設定されているため、親の操作が子に影響を与えるようになっています。これにより、データベースの整合性が保たれ、関連するデータの管理が容易になります。

企業エンティティ

先ずは、companyテーブルとemployeeテーブルを関連付ける為に、Company.javaにフィールドを追加していきます。

配置先:src > main > java > com > example > demo > entity > Company.java

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フィールドが、このリレーションシップのオーナーであることを示しています。つまり、EmployeeCompanyに対して外部キーを持っているということです。

cascade = CascadeType.ALL

これは、親エンティティ(Company)に対する操作(保存、更新、削除など)が、子エンティティ(Employee)にも伝播することを意味します。例えば、Companyを削除すると、その関連するEmployeeもすべて削除されます。

社員エンティティ

次に子エンティティにあたるEmployee.javaを新たに作成します。

このEntityクラスを作成することで、アプリケーション起動時に自動でクラスの定義に基づいたemployeeテーブルがDB上に作成されます。

配置先:src > main > java > com > example > demo > entity > Employee.java

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

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

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

CompanyForm
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

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

登録フォームの表示

先ずは登録フォーム表示時に、社員情報の登録フォームも表示されるように処理を追加します。

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 の空インスタンスをリストに追加し、最後にこのリストを companyFormemployeeList プロパティにセットしています。

新規登録処理

次に、登録処理のメソッドについても修正していきます。

今回は企業情報と社員情報を個別に処理するのではなく、同じフォームデータ (CompanyForm) として、1つの単位としてまとめて管理している為、コードの修正はほとんど必要ありません。

ただし、このまま登録しようとすると社員情報の入力フォームが空欄だった際にも、データベースに空データとして登録されてしまいます。その為、空の社員情報を削除する処理を追加します。

CompanyController.java
@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

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 現在のループが最後かどうかの真偽値

自分で決めたステータス変数名.プロパティ の形式で色々と応用が利きます。

データの流れ

社員情報に焦点を当てて考えた場合、登録処理時の大まかなデータの流れは以下の通りです。

  1. ユーザーが社員情報を入力します
  2. フォームが送信され、サーバー側でバリデーションと空欄チェックが行われます
  3. 空欄の社員情報はリストから削除され、保存処理に進みます
  4. 空欄の社員情報が削除された状態で、企業情報と有効な社員情報のみが保存されます

参考サイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?