2026年5月26日火曜日

SpringBoot でリクエスト情報を DTO に使うサンプルコード

SpringBoot でリクエスト情報を DTO に使うサンプルコード

概要

前回 SpringBoot から MySQL に接続する方法を紹介しました
その際はリクエストの情報をそのまま Entity に渡していましたがそれだといろいろと脆弱です (マスアサインメントなど)

なので通常は DTO(Data Transfer Object) と呼ばれる内部で扱うクラスに変換してから Entity に渡します

今回は DTO を使ったサンプルコードを紹介します
更に応用として Service レイヤーを追加して Repository とまとめる方法も紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
  • jasypt-spring-boot 4.0.4
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0

バリデーション用のライブラリ追加

spring-boot-starter-validation を使うので以下を追加します
バージョンは plugins 側で制御しています

  • vim build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '4.0.6'
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
}

DTO 用のクラスを作成する

基本的にはここでリクエスト値などのバリデーションを行います
今回はバリデーション用のライブラリにある基本的なバリデーションのみを追加します

  • vim src/main/java/com/example/demo/dto/UserRequest.java
package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public class UserRequest {

    @NotBlank
    private String name;

    @Email
    private String email;

    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;
    }
}

コントローラの修正

コントローラ側ではリクエスト情報を先程の RequestUser クラスに自動的に変換するように修正します
@Valid, @RequestBody を使います

  • vim src/main/java/com/example/demo/MainController.java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.demo.dto.UserRequest;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;

import jakarta.validation.Valid;

@Controller
@RequestMapping(path = "/demo")
public class MainController {
	@Autowired
	private UserRepository userRepository;

	@PostMapping(path = "/add")
	public @ResponseBody String addNewUser(
			@Valid @RequestBody UserRequest req) {
		User user = new User();
		user.setName(req.getName());
		user.setEmail(req.getEmail());

		userRepository.save(user);
		return "Saved";
	}

	@GetMapping(path = "/all")
	public @ResponseBody Iterable<User> getAllUsers() {
		return userRepository.findAll();
	}
}

とりあえずここで動作確認

まずはここでちゃんと動作するか確認します

  • ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'

application.properties を暗号化しているので jasypt.encryptor.password を指定していますが暗号化していなければ不要です

curl で動作確認しましょう

  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "someemail@someemailprovider.com"}' -H 'content-type: application/json'

正常系は上記です
バリデーションエラーの確認をする場合は以下のようにリクエストしてみましょう

  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "error_email"}' -H 'content-type: application/json'
{"timestamp":"2026-05-23T00:54:34.921Z","status":400,"error":"Bad Request","path":"/demo/add"}

コンソールのエラーメッセージには以下のように表示されると思います

2026-05-23T09:54:34.907+09:00  WARN 69601 --- [demo] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.demo.MainController.addNewUser(com.example.demo.dto.UserRequest): [Field error in object 'userRequest' on field 'email': rejected value [error_email]; codes [Email.userRequest.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userRequest.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@2601ac31,.*]; default message [電子メールアドレスとして正しい形式にしてください]] ]

更に Service レイヤーを追加する

これでも十分ですが通常は更に Service レイヤーを追加します
Service レイヤーでは Repository を扱い受け取った UserRequest を使用します
基本的には何かしらのビジネスルールなどを実装する必要があります

  • vim src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;

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

import com.example.demo.dto.UserRequest;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

	public void createUser(UserRequest req) {

		// 例:ビジネスルール
		if (userRepository.existsByEmail(req.getEmail())) {
			throw new IllegalArgumentException("既に登録されています");
		}

		User user = new User();
		user.setName(req.getName());
		user.setEmail(req.getEmail());

		userRepository.save(user);
	}

	public Iterable<User> getAllUsers() {
		return userRepository.findAll();
	}
}

UserRepository.java 修正

ビジネスロジック用の処理を追加します

  • vim src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;

import org.springframework.data.repository.CrudRepository;

import com.example.demo.entity.User;

public interface UserRepository extends CrudRepository<User, Integer> {
	boolean existsByEmail(String email);
}

MainController の修正

MainController で Repository は扱わずに Service を扱うように修正します

  • vim src/main/java/com/example/demo/MainController.java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.demo.dto.UserRequest;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;

import jakarta.validation.Valid;

@Controller
@RequestMapping(path = "/demo")
public class MainController {

	@Autowired
	private UserService userService;

	@PostMapping(path = "/add")
	public @ResponseBody String addNewUser(@Valid @RequestBody UserRequest req) {
		userService.createUser(req);
		return "Saved";
	}

	@GetMapping(path = "/all")
	public @ResponseBody Iterable<User> getAllUsers() {
		return userService.getAllUsers();
	}
}

動作確認

  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "someemail2@someemailprovider.com"}' -H 'content-type: application/json'
  • curl http://localhost:8080/demo/all

同じメールアドレスで登録しようとするとエラーになります
現状は特にエラーを指定していないので500エラーになります

  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "someemail@someemailprovider.com"}' -H 'content-type: application/json'
{"timestamp":"2026-05-23T01:50:23.001Z","status":500,"error":"Internal Server Error","path":"/demo/add"}

最後に

SpringBoot で DTO を使って直接リクエスト情報を扱わない方法を紹介しました
Service レイヤーを入れる場合にはビジネスロジックも実装するようにしましょう

個人的にメールアドレスの重複チェックはどちらかと言えばバリデーションに近いのではと思うのですがデータベースを使うバリデーションはビジネスロジックになるようでその場合は Service でバリデーションするのが良いようです

レスポンスに関しても DTO を使うのが定石なので次回はレスポンス用の DTO を作成し使うように修正します

参考サイト

0 件のコメント:

コメントを投稿