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] 繰り返しフォームを扱うDTO設計

Last updated at Posted at 2025-05-17

はじめに

Spring Bootでの長期インターンで得た経験をもとに、繰り返しフォームを扱うDTO設計に関するTipsを紹介します。スタッフを管理するようなシステムを例に、「可読性・再利用性・保守性」を意識したコーディングと、実務で用いられていた典型的な工夫を紹介します(あくまでインターン生として感じた工夫です)。

自己紹介

  • 商学部出身
  • 兵庫の情報系大学院生(27卒予定)
  • サーバーサイド大好き人間
  • 趣味はハッカソン
  • バックエンドエンジニア志望
  • 長期インターンにてSpring Bootを使用(半年以上)

背景と目的

フォーム構造が複雑になると、Controllerでのパラメータ受け取りやバリデーションの責務が分散しやすく、再表示処理も不安定になるなどの設計上の課題が増えていきます。本記事では、長期インターンで学んだ設計観点として、以下を紹介します。

  • WrapperDtoによるフォーム構造の集約
  • バインディング命名規則と動的フォーム連携設計
  • 階層的バリデーション戦略と責務の再帰適用
  • Null回避と初期表示戦略によるフォーム安定性

WrapperDtoによるフォーム構造の集約

繰り返しフォームの入力を扱う際、DTOをそのままリストで受け取るのではなく、ラッパーオブジェクト(WrapperDto)でまとめて管理することで、バリデーション・再表示・Service連携がシンプルになります。

// スタッフ1名分の入力項目を保持するDTO
public class StaffDto {

    // 氏名(必須・空文字不可)
    @NotBlank
    private String name;

    // メールアドレス(形式チェックあり)
    @Email
    private String email;
}

// StaffDTO のリストをまとめて受け取るためのラッパー
public class StaffWrapperDto {
    private List<StaffDto> staffs = new ArrayList<>();
}

StaffDto を複数扱うため、StaffWrapperDto 内でリストとして集約している。この構造により、フォームデータを1つのオブジェクトとして受け取れる。

// スタッフ情報を一括登録
@PostMapping("/staffs/bulk")
public String submitStaffs(@ModelAttribute StaffWrapperDto wrapper) {
    // 入力リストをService層に渡して保存処理
    staffService.saveAll(wrapper.getStaffs());

    // 一覧画面へリダイレクト
    return "redirect:/staffs";
}

staffService.saveAll() にはList<StaffDto> をそのまま渡せるため、Service層の引数設計とも統一される。

バインディング命名規則と動的フォーム連携設計

バインディング命名規則(list[0].field形式)を使用することで、テンプレートとDTOリストを自動的にマッピングできます。これにより、動的に追加・削除されるフォームとも自然に連携できます。

<tr th:each="staff, stat : *{staffs}">
  <td><input type="text" th:field="*{staffs[__${stat.index}__].name}" /></td>
  <td><input type="email" th:field="*{staffs[__${stat.index}__].email}" /></td>
</tr>

th:eachth:field を組み合わせることで、HTMLテンプレート側でもインデックス付きの構造を簡単に記述できる。

階層的バリデーション戦略と責務の再帰適用

フォームの正当性を確保するため、ネストしたDTO構造全体にバリデーションを適用できるようにします。WrapperDtoに @Valid を付ければ、内包する各DTOのバリデーションが再帰的に適用されます。

public class StaffWrapperDto {
    @Valid
    @Size(min = 1, message = "1名以上のスタッフ情報を入力してください")
    private List<StaffDto> staffs = new ArrayList<>();
}

@Valid によって StaffDto に書かれたバリデーションルール(e.g. @NotBlank)が自動適用される。また、@Size により空のリスト送信もブロックできる。

@PostMapping("/staffs/bulk")
public String submitStaffs(@ModelAttribute @Valid StaffWrapperDto wrapper, BindingResult result, Model model) {
    // 入力チェックでエラーがあれば、エラーメッセージ付きで再表示
    if (result.hasErrors()) {
        model.addAttribute("errorMessage", "入力に誤りがあります");
        return "staff/form";
    }

    // 正常時はスタッフ情報を保存し、一覧画面へリダイレクト
    staffService.saveAll(wrapper.getStaffs());
    return "redirect:/staffs";
}

BindingResult を使用することで、バリデーション失敗時にもフォームを再表示できる。エラーメッセージは Model に渡し、View 側で制御している。

<div th:if="${#fields.hasErrors('staffs')}" th:errors="*{staffs}" class="alert alert-danger"></div>

staffsフィールドに関するバリデーションエラーを一括で表示。

Null回避と初期表示戦略によるフォーム安定性

テンプレート上で th:each を使ってリストを展開するには、あらかじめ中身が存在している必要があります。そのため、初期表示時に空のDTOを用意しておきます。

@GetMapping("/staffs/bulk")
public String showStaffForm(Model model) {
    StaffWrapperDto wrapper = new StaffWrapperDto();
    
    // 空のスタッフDTOを3件分追加して、フォームの初期行を構成
    List<StaffDto> staffs =wrapper.getStaffs();
    for (int i = 0; i < 3; i++) {
      staffs.add(new StaffDto());
    }

    // フォームに渡すオブジェクトをModelに追加
    model.addAttribute("staffForm", wrapper);
    return "staff/form";
}

初期値として空の StaffDto をあらかじめ複数入れておくことで、テンプレート上の th:each を安定して表示できる。

NGパターン:フィールド単位でのリスト受け取り

@PostMapping("/staffs/bulk")
public String submit(@RequestParam List<String> name, @RequestParam List<String> email) {
    return "redirect:/staffs";
}

この場合、name[i] と email[i] の整合性はフレームワーク側では保証されない。また、バリデーションや再表示が困難になる。

まとめ

  • フォーム構造はWrapperDtoで集約し、Controllerを簡潔に保つ
  • テンプレートとDTOの命名規則を統一し、動的フォームにも自然に対応
  • バリデーションはDTOに集約し、再帰的検証とエラー制御を簡潔化
  • 初期表示時の安定性を設計段階で確保

おわりに

今後も、実務で得た細かい知見をTipsとして紹介していきます。質問や改善のフィードバックがあれば、ぜひコメントで教えてください!

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?