イメージ
ディレクトリ
実装方針
OneToMany / @ManyToOne で写真を自動保存する
💡 方針
親:AdminUser
子:AdminuserPhotos
cascade = CascadeType.ALL
orphanRemoval = true
Service ではadminUserRepository.save()だけとなるように実装していきます。
application.propertiesの更新
spring.application.name=Backend
# ===============================
# DataSource
# ===============================
spring.datasource.url=jdbc:mysql://localhost:3306/reactspringDb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Tokyo
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ===============================
# JPA / Hibernate
# ===============================
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.open-in-view=false
# ★ Dialect 明示(これが無いと起動しない)
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
# ===============================
# (今は不要)MyBatis
# ===============================
# mybatis.mapper-locations=classpath:com/example/demo/mapper/*.xml
#spring.application.name=Backend
#spring.datasource.url=jdbc:mysql://localhost:3306/reactspringDb
#spring.datasource.username=root
#spring.datasource.password=pass1234
# MyBatisのXMLファイルの場所 (MyBatis Generatorを使用している場合)
#mybatis.mapper-locations=classpath:com/example/demo/mapper/*.xml
パスワードのハッシュ化クラスの作成
pom.xmlに下記を追加します。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Beanを追加します。
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean // ←これが必須
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // ここをラムダ形式に変更
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll() // /api/** は誰でもアクセス可能
.anyRequest().authenticated() // 他は認証必須
);
return http.build();
}
}
pom.xmlの修正
spring-boot-starter-securityプラグインを入れた全体のpom.xmlです。
※MyBatisのプラグインがありますが無視してください。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>Backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Backend</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>compile</scope>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--Spring Security追加-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!--MyBatis追加-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--MyBatis追加-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<includeAllDependencies>true</includeAllDependencies>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Repositoryクラスの作成
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.AdminUsersEntity;
public interface AdminUserRepository extends JpaRepository<AdminUsersEntity, String>{
}
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.AdminUserPhotosEntity;
public interface AdminUserPhotoRepository extends JpaRepository<AdminUserPhotosEntity, Integer>{
}
モデルクラスの作成
package com.example.demo.entity;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "adminusers")
public class AdminUsersEntity {
@Id
@Column(name = "id", length = 50)
private String id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "email", nullable = false, length = 255, unique = true)
private String email;
@Column(name = "password", nullable = false, length = 255)
private String password;
@Column(name = "gender", nullable = true, length = 1)
private String gender;
@Column(name = "office", nullable = true, length = 20)
private String office;
@Column(name = "adminrole", nullable = false)
private Boolean adminRole;
@OneToMany(
mappedBy = "adminUser",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<AdminUserPhotosEntity> photos = new ArrayList<>();
// ===== 子を追加するヘルパー =====
public void addPhoto(AdminUserPhotosEntity photo) {
photos.add(photo);
photo.setAdminUser(this); // setter 名を統一
}
public void removePhoto(AdminUserPhotosEntity photo) {
photos.remove(photo);
photo.setAdminUser(null);
}
// ===== Getter / Setter =====
public String getId() {
return id;
}
public void setId(String 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 getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getOffice() {
return office;
}
public void setOffice(String office) {
this.office = office;
}
public Boolean getAdminRole() {
return adminRole;
}
public void setAdminRole(Boolean adminRole) {
this.adminRole = adminRole;
}
public List<AdminUserPhotosEntity> getPhotos() {
return photos;
}
public void setPhotos(List<AdminUserPhotosEntity> photos) {
this.photos = photos;
for (AdminUserPhotosEntity photo : photos) {
photo.setAdminUser(this);
}
}
}
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "adminuser_photos")
public class AdminUserPhotosEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "adminuser_id", nullable = false)
private AdminUsersEntity adminUser;
@Column(name = "file_path", nullable = false)
private String filePath;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public AdminUsersEntity getAdminUsersEntity() {
return adminUser;
}
public void setAdminUser(AdminUsersEntity adminUser){
this.adminUser = adminUser;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
}
Serviceクラスの更新
package com.example.demo.services;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.entity.AdminUsersEntity;
import com.example.demo.repository.AdminUserPhotoRepository;
import com.example.demo.repository.AdminUserRepository;
@Service
public class AdminUserRegisterService {
private final PasswordEncoder passwordEncoder;
private final AdminUserRepository adminUserRepository;
private final AdminUserPhotoRepository photoRepository;
public AdminUserRegisterService(AdminUserRepository adminUserRepository,
AdminUserPhotoRepository photoRepository,
PasswordEncoder passwordEncoder
){
this.adminUserRepository = adminUserRepository;
this.photoRepository = photoRepository;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public int registerAdminUser(AdminUsersEntity adminUser) {
try {
adminUser.setPassword(passwordEncoder.encode(adminUser.getPassword()));
adminUserRepository.save(adminUser); // 写真も自動保存
}catch(DataIntegrityViolationException e) {
//throw new MessageService.EmailAlreadyExistsException("このメールアドレスは既に登録されています");
e.printStackTrace(); // ← rollback の原因を特定する
throw e; // 必ず再スロー
}
catch(DataAccessException e) {
//データベース接続エラー
e.printStackTrace(); // ← rollback の原因を特定する
throw e; // 必ず再スロー
}
catch(Exception e) {
e.printStackTrace(); // ← rollback の原因を特定する
throw e; // 必ず再スロー
}
return 1;
}
}
Controllerクラスの更新
package com.example.demo.controllers;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.entity.AdminUserPhotosEntity;
import com.example.demo.entity.AdminUsersEntity;
import com.example.demo.services.AdminUserRegisterService;
import com.example.demo.services.FileStorageService;
import com.example.demo.services.MessageService;
import com.example.exception.BadRequestException;
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class AdminUserController {
// MessageServiceをDI注入
private final MessageService messageService;
// ファイル保存サービスクラスをDI注入
private final FileStorageService fileStorageService;
// データベース登録サービスクラスをDI注入
private final AdminUserRegisterService adminUserRegisterService;
// コンストラクタ
public AdminUserController(MessageService messageService,
FileStorageService fileStorageService,
AdminUserRegisterService adminUserRegisterService) {
this.messageService = messageService;
this.fileStorageService = fileStorageService;
this.adminUserRegisterService = adminUserRegisterService;
}
@PostMapping(value = "/new/adminusercreate",consumes = "multipart/form-data")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<String> getAdminUser(
@RequestParam("id") String id,
@RequestParam("name") String name,
@RequestParam("email") String email,
@RequestParam("password") String password,
@RequestParam("adminrole") boolean adminRole,
@RequestParam(value="files[]", required=false) List<MultipartFile> files
) {
System.out.println("id=" + id);
System.out.println("name=" + name);
System.out.println("email=" + email);
System.out.println("files count=" + (files != null ? files.size() : 0));
//System.out.println("Received: " + body);
System.out.println("Received: " + id + name + email);
// ファイル保存 + Entity作成
List<AdminUserPhotosEntity> photoList = new ArrayList<>();
AdminUsersEntity adminUsersEntity = new AdminUsersEntity();
adminUsersEntity.setId(id);
adminUsersEntity.setName(name);
adminUsersEntity.setEmail(email);
adminUsersEntity.setPassword(password);
adminUsersEntity.setGender(null);
adminUsersEntity.setOffice(null);
adminUsersEntity.setAdminRole(adminRole);
int sortOrder = 1;
// 取得したファイル名を表示
if(files != null && !files.isEmpty()) {
files.forEach(file ->{
System.out.println("original name: " + file.getOriginalFilename());
System.out.println("content type: " + file.getContentType());
System.out.println("size: " + file.getSize());
// ファイルを保存
String savedPath = fileStorageService.saveAdminUserPhoto(id, file);
System.out.println("saved path = " + savedPath);
AdminUserPhotosEntity photo = new AdminUserPhotosEntity();
photo.setFilePath(savedPath);
photo.setSortOrder(sortOrder+1);
// 親子関係を設定
adminUsersEntity.addPhoto(photo);
});
}
// 入力チェック
if(id == null || id.isEmpty()) {
throw new BadRequestException("社員番号は必須です");
}
if(email == null || email.isEmpty() ){
throw new BadRequestException("メールアドレスは必須です");
}
if(password == null || password.isEmpty()) {
throw new BadRequestException("パスワードは必須です");
}
/*
AdminUsersEntity adminUsersEntity = new AdminUsersEntity();
adminUsersEntity.setId(id);
adminUsersEntity.setName(name);
adminUsersEntity.setEmail(email);
adminUsersEntity.setPassword(password);
adminUsersEntity.setGender(null);
adminUsersEntity.setOffice(null);
adminUsersEntity.setAdminRole(adminRole);
*/
// TODO(データベース登録処理)
System.out.println("AdminUsersEntity: "+ adminUsersEntity);
// Service 呼び出し
adminUserRegisterService.registerAdminUser(adminUsersEntity);
// 返却するメッセージを生成
String message = messageService.createMessage("admin.user.create.success", name);
return ResponseEntity.status(HttpStatus.CREATED).body(message);
}
// ファイル名を取得する処理
private String createFileName(String userId, MultipartFile file) {
return userId + file.getOriginalFilename();
}
}
技術チップ集
Spring-boot-starter-securityを入れてReactからのデータ渡しを可能にする
spring-boot-starter-security を入れると、Spring Security がデフォルトで全ての HTTP リクエストを認証必須にするため、React など外部クライアントからの POST がブロックされます。
なので、
対策1:管理画面だけ保護して API は開放する
SecurityConfig.java に下記を追加すると、特定の API だけ無認証でアクセス可能にできます。
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean // ←これが必須
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // ここをラムダ形式に変更
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll() // /api/** は誰でもアクセス可能
.anyRequest().authenticated() // 他は認証必須
);
return http.build();
}
}
✅ ポイント:
1.csrf().disable():Reactなどのフロントからの POST を受け付けるときは必要
2./api/** をpermitAll()にして、バックエンドのREST APIは認証なしで使える
3.他の管理画面などはauthenticated()のままにできる
対策2:CSRF を React 側で処理する(推奨)
もしCSRFを無効化したくない場合、React からPOST時にX-CSRF-TOKENを送る必要がありますが、まずは簡単に動かすならcsrf().disable()でOKです。
サイト
Spring公式サイト
Spring Boot公式プロジェクトページ
Spring公式ブログ(最新のリリース情報)
トラブルシューティング
DB登録できないエラー
下記のエラーが表示された。
org.springframework.dao.DataIntegrityViolationException:
not-null property references a null or transient value for entity com.example.demo.entity.AdminUsersEntity.gender
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:186)
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:131)
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.translateExceptionIfPossible(HibernateExceptionTranslator.java:105)
これはgenderがnullになっているのに、JPA のEntity側でnullable=falseになっているため、Hibernate が保存できず例外を投げています。
解決方法
Entity の修正(nullable=true)
@Column(name = "gender", length = 1, nullable = true)
private String gender;
@Column(name = "office", length = 20, nullable = true)
private String office;
💡 ポイント
■DataIntegrityViolationException の内容を必ず確認
■Hibernate の「not-null property references a null value」はEntity側のnullable=falseとnullの値の衝突
■DB のカラムnullableとEntityのnullableは両方確認する
No qualifying bean of type 'org.springframework.security.crypto.password.PasswordEncoder' available
Spring がPasswordEncoderを DI しようとしたけど Bean が登録されていない と言っています。
原因
SecurityConfig.java では下記のように書いていました:
@Configuration
public class SecurityConfig {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
⚠️ @Bean アノテーションが ついていません
Spring はこれをBeanとして認識できません。
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean // ←これが必須
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
これで Spring がPasswordEncoderをDIできるようになります。
AdminUserRegisterService 側での使用例
//(略)
adminUser.setPassword(passwordEncoder.encode(adminUser.getPassword()));
adminUserRepository.save(adminUser);
✅ これで保存時にパスワードが自動で BCrypt 暗号化されます。
Column 'adminuser_id' is duplicated in mapping
このエラーは JPAの親子マッピングで最もよくある&原因がはっきりしているもの です。
👉 AdminUserPhotosEntity で
同じadminuser_idカラムを「2回マッピング」している
🔍 何が起きているか
AdminUserPhotosEntity に、次の2つが同時に存在していました👇
// ① 外部キーを直接持っている
@Column(name = "adminuser_id")
private String adminuserId;
// ② 親Entityとの関連
@ManyToOne
@JoinColumn(name = "adminuser_id")
private AdminUsersEntity adminUser;
✅ 正解パターン(おすすめ)
⭐ パターンA:JPAらしい正解(推奨)
外部キーの String フィールドを削除する
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "adminuser_photos")
public class AdminUserPhotosEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "adminuser_id", nullable = false)
private AdminUsersEntity adminUser;
@Column(name = "file_path", nullable = false)
private String filePath;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public AdminUsersEntity getAdminUsersEntity() {
return adminUser;
}
public void setAdminUser(AdminUsersEntity adminUser){
this.adminUser = adminUser;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
}





