📘

Spring Bootで予約機能を実装してみた|バリデーション・例外処理

に公開

はじめに

現在、Spring Bootを使って「イベント予約管理システム」の開発を進めています。
今回の記事では、「予約機能」の実装についてまとめました。

予約機能では以下のようなことを行いました。

  • Service層・Controller層 の実装
  • Formによるバリデーション設定(入力チェック)
  • バリデーションエラーや存在しないIDなどの Exception(例外)処理
  • APIの POST / GET を使った登録・一覧取得の動作確認

また、今後の開発に向けて、Spring BootでのMVC構成の基本も一緒に振り返りながら、
コードの一部をピックアップして解説していきます。

これまでの記事も併せてご覧ください。

今回の実装内容

今回の開発では、「予約機能」を中心に以下の内容を実装しました。
対象となるドメインは以下の3つです。

  • Event(イベント)
  • Reservation(予約)
  • User(ユーザー)

これらのドメインに対して、以下の層を実装しています。

  • Form(入力値のバリデーション設定)
  • Service層(ビジネスロジックの実装)
  • Controller層(APIの受け口)
  • Exception(例外処理)

MVCアーキテクチャの簡単な説明

Spring Boot では、アプリケーションの構成に「MVCアーキテクチャ」がよく使われます。
MVCとは、以下の3つの役割に分けて機能を整理する考え方です。

  • Model(モデル)
    アプリケーションのデータやその処理を担当します。
    今回の開発では、Entity や Repository がこれにあたります。

  • View(ビュー)
    画面の見た目や表示部分を担当します。
    今回はバックエンド API のみ実装しており、View はまだありません。

  • Controller(コントローラー)
    ユーザーからのリクエストを受け取り、適切な処理(Serviceなど)に振り分けます。

予約APIのリクエストの流れ

ユーザーが「予約を登録」したいときの処理は、次のように流れていきます。

  1. ユーザーからのリクエスト(例:POST /reservations)
    フロントエンドなどからAPIへリクエストが送られます。

  2. Controller
    リクエストを受け取り、入力内容(Form)のチェックを行います。
    バリデーションが通れば、Service層に処理を渡します。
    ※バリデーションエラーがあればここでエラーレスポンス(Exception)を返します。

  3. Service
    ビジネスロジックを担当する層です。
    例えば「イベントが存在するか?」「すでに予約済みではないか?」などの確認を行います。
    必要なデータは Repository を通じてデータベースから取得します。

  4. Repository
    データベースと直接やり取りを行う層です。
    データの検索・保存・更新などを行います。

  5. Entity
    データベースのテーブル構造に対応するクラスです。
    Repositoryが取得したデータは Entity として Service に返されます。

MVC構成を意識することで、機能ごとの責任が明確になり、コードの見通しも良くなります。

ReservationのForm解説

予約登録の際、フロントエンドから送られてくるデータを受け取るために ReservationForm を作成しています。
このクラスでは、ユーザーIDとイベントIDの2つの値を受け取り、入力チェック(バリデーション) を行っています。

ReservationFormのコード

package com.example.event_reservation_app.form;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public class ReservationForm {
    @NotNull(message = "ユーザーIDは必須です")
    @Positive(message = "ユーザーIDは正の数である必要があります")
    private Long userId;

    @NotNull(message = "イベントIDは必須です")
    @Positive(message = "イベントIDは正の数である必要があります")
    private Long eventId;

    // --- Getter / Setter ---
    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getEventId() {
        return eventId;
    }

    public void setEventId(Long eventId) {
        this.eventId = eventId;
    }
}

バリデーションの説明

  • @NotNull
    値が null(空)でないことをチェックします。
    → ユーザーID・イベントIDはどちらも必須の入力項目です。

  • @Positive
    0より大きい正の数であることをチェックします。
    → IDは通常、1以上の連番で管理されているため、不正な値(0やマイナス)を防ぐ目的で使用しています。

これらのバリデーションは、spring-boot-starter-validation という依存関係を pom.xml に追加することで有効になります。
フォームの入力に対して制約違反があった場合は、Controller で 自動的にエラーレスポンス(400 Bad Request) が返されます。

バリデーション機能を有効にするには?

現状だとバリデーションが有効でなかったので、以下の依存関係を pom.xml に追加しました

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

ReservationControllerの解説

予約機能のAPIの入り口となる ReservationController では、予約登録と予約一覧の取得を担当しています。

package com.example.event_reservation_app.controller;

import com.example.event_reservation_app.entity.Reservation;
import com.example.event_reservation_app.form.ReservationForm;
import com.example.event_reservation_app.service.ReservationService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/reservations")
public class ReservationController {

    @Autowired
    private ReservationService reservationService;

    // 予約登録(POST)
    @PostMapping
    public Reservation createReservation(@RequestBody @Valid ReservationForm form) {
        return reservationService.createReservation(form);
    }

    // 予約一覧取得(GET)
    @GetMapping
    public List<Reservation> getAllReservations() {
        return reservationService.getAllReservations();
    }
}

ポイント解説

  • @RestController
    → REST API用のコントローラーであることを示しています。
    → メソッドの戻り値は自動的にJSON形式で返されます。

  • @RequestMapping("/reservations")
    → このクラスのAPIのベースURLを /reservations に設定しています。

予約登録(POST)メソッド

  • @PostMapping でPOSTリクエストを受け取ります。
  • @RequestBody でリクエストのJSONを ReservationForm にマッピングします。
  • @Valid によりFormのバリデーションが有効になります。
  • バリデーションがOKなら reservationService.createReservation(form) を呼び出し、処理結果の Reservation を返します。

予約一覧取得(GET)メソッド

  • @GetMapping でGETリクエストを受け取ります。
  • reservationService.getAllReservations() を呼び出し、予約リストを取得して返します。

ReservationServiceの解説

予約のビジネスロジックを担当するサービス層です。

package com.example.event_reservation_app.service;

import com.example.event_reservation_app.entity.Event;
import com.example.event_reservation_app.entity.Reservation;
import com.example.event_reservation_app.entity.User;
import com.example.event_reservation_app.form.ReservationForm;
import com.example.event_reservation_app.repository.EventRepository;
import com.example.event_reservation_app.repository.ReservationRepository;
import com.example.event_reservation_app.repository.UserRepository;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class ReservationService {

    private final ReservationRepository reservationRepository;
    private final UserRepository userRepository;
    private final EventRepository eventRepository;

    // コンストラクタインジェクションでリポジトリを注入
    public ReservationService(
            ReservationRepository reservationRepository,
            UserRepository userRepository,
            EventRepository eventRepository) {
        this.reservationRepository = reservationRepository;
        this.userRepository = userRepository;
        this.eventRepository = eventRepository;
    }

    // 予約登録処理
    public Reservation createReservation(ReservationForm form) {
        // ユーザーIDからユーザー情報を取得。なければ400エラーを返す
        User user = userRepository.findById(form.getUserId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "指定されたユーザーが存在しません"));

        // イベントIDからイベント情報を取得。なければ400エラーを返す
        Event event = eventRepository.findById(form.getEventId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "指定されたイベントが存在しません"));

        // 予約エンティティを作成して日時をセット
        Reservation reservation = new Reservation();
        reservation.setUser(user);
        reservation.setEvent(event);
        reservation.setReservedAt(LocalDateTime.now());

        // DBに保存して結果を返す
        return reservationRepository.save(reservation);
    }

    // 全予約の一覧取得処理
    public List<Reservation> getAllReservations() {
        return reservationRepository.findAll();
    }

}

ポイント解説

  • コンストラクタインジェクション
    リポジトリをコンストラクタで受け取り、サービスクラス内で使います。
    → テストしやすく、依存関係が明確になります。

  • createReservationメソッド

    • フォームのユーザーID・イベントIDが正しいかDBからチェックします。
    • 存在しなければ ResponseStatusException を投げて400 Bad Requestを返します。
    • 問題なければ予約エンティティを作成し、現在日時を予約日時としてセットします。
    • 予約情報をDBに保存して返します。
  • getAllReservationsメソッド
    予約テーブルの全データを取得して返します。

例外処理の解説(GlobalExceptionHandler)

Spring Bootでのバリデーションエラーなどを一括で処理するクラスです。

package com.example.event_reservation_app.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    // バリデーションエラー発生時の処理
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();

        // フィールドごとのエラーメッセージを取得してMapに格納
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            errors.put(fieldName, message);
        });

        // 400 BAD REQUEST とエラーメッセージを返す
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

ポイントまとめ

  • @ControllerAdvice で全コントローラーに対して例外処理を適用

  • @ExceptionHandler で特定の例外をキャッチし、レスポンスをカスタマイズ

  • バリデーションエラーはフィールドごとのエラーメッセージをまとめて返す

  • クライアントは400エラーとして受け取り、どこが問題かを把握しやすくなる

おわりに

今回は、Spring Boot を使った「イベント予約機能」の実装についてご紹介しました。
Formバリデーション、サービス層でのビジネスロジック、コントローラーでのAPI実装、例外処理など、MVCアーキテクチャの流れを意識しながら開発を進めることができました。

次回は、フロントエンド開発(React) に入っていく予定です。
IntelliJでReactの開発環境を整えつつ、まずはイベント登録画面から作っていきたいと思います。

次に予定している開発ステップ

  • イベント登録画面(POST送信)
  • イベント一覧表示画面(GET取得)
  • 予約登録フォーム(ユーザーID・イベントIDを指定 → POST送信)
  • 予約一覧表示画面(GET取得)

今後も、バックエンドとフロントエンドをつなぎながら、少しずつ全体像を作っていく予定です!

Discussion