🔖

Spring BootにおけるPRG(Post-Redirect-Get)パターンの実装と活用

に公開

はじめに

この記事は、「@RequestParamからの脱却と型安全性の向上」記事で軽く触れたPRG(Post-Redirect-Get)パターンについて、より詳しく解説する補足記事です。
https://zenn.dev/delisit/articles/f124a2bade22d4

Spring BootでWebアプリケーションを開発している際に、フォーム送信後の重複実行問題に直面したことはありませんか?PRGパターンは、このような問題を解決するWeb開発における重要な設計パターンです。

対象読者

  • Spring Boot / Spring MVCを使用している開発者
  • Webフォーム処理で重複送信問題に直面している方
  • Webアプリケーションのユーザビリティ向上を目指している開発チーム
  • RESTfulなWebアプリケーション設計を学びたい方

PRG(Post-Redirect-Get)パターンとは

概要

PRGパターンは、Post → Redirect → Getの頭文字を取った名前で、Webアプリケーションにおけるフォーム処理の標準的な設計パターンです。

フォーム送信(POST)の処理完了後に、ブラウザに対してリダイレクトレスポンス(302/303ステータス)を返し、最終的にGETリクエストで結果画面を表示する流れを指します。

処理フロー

なぜPRGパターンが必要なのか

重複送信問題の発生メカニズム

従来のPOST → レスポンス方式では、以下のような問題が発生します:

// 問題のあるパターン(PRGなし)
@PostMapping("/order")
public String processOrder(@Valid OrderForm form) {
    orderService.createOrder(form); // 注文データをDBに保存
    return "order-complete"; // 直接テンプレートを返す
}

この実装では、ユーザーが「更新」ボタンを押すと:

  1. ブラウザが同じPOSTリクエストを再送信
  2. orderService.createOrder(form)が再実行
  3. 同じ注文が重複して作成される

実際の被害例

  • ECサイト: 商品注文の重複により在庫や売上データに不整合
  • 決済システム: 同じ決済処理が複数回実行される
  • 会員登録: 同一ユーザーのアカウントが複数作成される
  • アンケート: 同じ回答が複数回送信される

Spring BootでのPRGパターン実装

基本的な実装パターン

@Controller
public class OrderController {

    @PostMapping("/order")
    public String processOrder(
            @Valid @ModelAttribute OrderForm form,
            BindingResult bindingResult,
            RedirectAttributes redirectAttributes) {
        
        // バリデーションエラーの場合
        if (bindingResult.hasErrors()) {
            redirectAttributes.addFlashAttribute("orderForm", form);
            redirectAttributes.addFlashAttribute(
                "org.springframework.validation.BindingResult.orderForm",
                bindingResult);
            return "redirect:/order-form";
        }
        
        try {
            // ビジネスロジック実行
            String orderId = orderService.createOrder(form);
            
            // 成功時のメッセージ設定
            redirectAttributes.addFlashAttribute("successMessage", 
                "ご注文を承りました。注文番号: " + orderId);
            
            // PRGパターン:リダイレクト実行
            return "redirect:/order-complete";
            
        } catch (BusinessException e) {
            // エラー時もリダイレクトでPRGパターンを維持
            redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
            redirectAttributes.addFlashAttribute("orderForm", form);
            return "redirect:/order-form";
        }
    }
    
    @GetMapping("/order-complete")
    public String showOrderComplete(Model model) {
        // フラッシュスコープからメッセージを取得
        // (Spring MVCが自動的にModelに追加)
        return "order-complete";
    }
    
    @GetMapping("/order-form")
    public String showOrderForm(Model model) {
        // 初回表示時のフォーム初期化
        if (!model.containsAttribute("orderForm")) {
            model.addAttribute("orderForm", new OrderForm());
        }
        return "order-form";
    }
}

RedirectAttributesとフラッシュスコープ

PRGパターンでは、リダイレクト間でのデータ保持が重要です:

// フラッシュスコープの活用
@PostMapping("/submit")
public String submit(@Valid OrderForm form, 
                    BindingResult result,
                    RedirectAttributes redirectAttributes) {
    
    if (result.hasErrors()) {
        // エラー時:フォームデータとエラー情報を保持
        redirectAttributes.addFlashAttribute("orderForm", form);
        redirectAttributes.addFlashAttribute(
            "org.springframework.validation.BindingResult.orderForm", result);
        return "redirect:/form";
    }
    
    // 成功時:成功メッセージを保持
    redirectAttributes.addFlashAttribute("message", "処理が完了しました");
    return "redirect:/success";
}

Thymeleafテンプレートでの実装

<!-- order-form.html -->
<form th:object="${orderForm}" th:action="@{/order}" method="POST">
    <!-- エラーメッセージ表示 -->
    <div th:if="${errorMessage}" class="alert alert-danger">
        <span th:text="${errorMessage}"></span>
    </div>
    
    <!-- フィールドエラー表示 -->
    <div class="form-group">
        <label for="customerName">顧客名</label>
        <input th:field="*{customerName}" type="text" class="form-control">
        <div th:if="${#fields.hasErrors('customerName')}" 
             th:errors="*{customerName}" class="text-danger"></div>
    </div>
    
    <button type="submit">注文する</button>
</form>
<!-- order-complete.html -->
<div class="container">
    <!-- 成功メッセージ表示 -->
    <div th:if="${successMessage}" class="alert alert-success">
        <span th:text="${successMessage}"></span>
    </div>
    
    <h1>ご注文完了</h1>
    <p>ご注文ありがとうございました。</p>
    
    <!-- このページで「更新」を押してもGETリクエストのみ -->
    <a th:href="@{/orders}" class="btn btn-primary">注文履歴を見る</a>
</div>

メリット・デメリット

メリット

1. 重複送信の完全防止

// ユーザーが「更新」ボタンを押しても
// GET /order-complete が実行されるだけ
// → 注文処理は再実行されない

2. ブラウザの自然な動作

  • 「戻る」ボタンが期待通りに動作
  • ブックマーク可能なURL
  • 適切なブラウザ履歴

3. SEO対応

  • 検索エンジンが適切にページをインデックス
  • クローラーが重複アクションを実行しない

4. ユーザビリティの向上

  • 直感的なページ遷移
  • エラーメッセージの適切な表示
  • フォーム再入力時の利便性

デメリット

1. レスポンス時間の増加

従来: POST → Response (1回の通信)
PRG:  POST → 302 → GET → Response (2回の通信)

2. 実装の複雑さ

  • フラッシュスコープの管理
  • エラーハンドリングの考慮
  • リダイレクト先の適切な設計

3. セッション使用量の増加

  • フラッシュスコープによる一時的なメモリ使用
  • セッションタイムアウトの考慮が必要

実装時の注意点とベストプラクティス

1. 適切なHTTPステータスコードの選択

// Spring BootのデフォルトはHttp 302
return "redirect:/success";

// 明示的に303 (See Other) を使用したい場合
@PostMapping("/submit")
public ResponseEntity<Void> submit() {
    // 処理実行
    return ResponseEntity.status(HttpStatus.SEE_OTHER)
                        .location(URI.create("/success"))
                        .build();
}

2. フラッシュスコープの適切な使用

// ✅ 良い例:一時的なメッセージのみ
redirectAttributes.addFlashAttribute("message", "処理完了");

// ❌ 悪い例:大きなオブジェクトの保持
redirectAttributes.addFlashAttribute("largeDataList", hugeList);

3. エラーハンドリングの統一

@PostMapping("/submit")
public String submit(@Valid OrderForm form, 
                    BindingResult result,
                    RedirectAttributes redirectAttributes) {
    
    // バリデーションエラー
    if (result.hasErrors()) {
        addErrorAttributesToRedirect(form, result, redirectAttributes);
        return "redirect:/form";
    }
    
    try {
        service.process(form);
        redirectAttributes.addFlashAttribute("successMessage", "完了しました");
        return "redirect:/success";
    } catch (BusinessException e) {
        redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
        addErrorAttributesToRedirect(form, result, redirectAttributes);
        return "redirect:/form";
    }
}

private void addErrorAttributesToRedirect(OrderForm form, 
                                         BindingResult result,
                                         RedirectAttributes redirectAttributes) {
    redirectAttributes.addFlashAttribute("orderForm", form);
    redirectAttributes.addFlashAttribute(
        "org.springframework.validation.BindingResult.orderForm", result);
}

4. URLパラメータでの情報引き継ぎ

// 必要最小限の情報はURLパラメータで引き継ぎ
@PostMapping("/order")
public String processOrder(@Valid OrderForm form, RedirectAttributes redirectAttributes) {
    String orderId = orderService.createOrder(form);
    return "redirect:/order-complete?orderId=" + orderId;
}

@GetMapping("/order-complete")
public String showComplete(@RequestParam String orderId, Model model) {
    // 注文IDから詳細情報を取得
    Order order = orderService.findById(orderId);
    model.addAttribute("order", order);
    return "order-complete";
}

まとめ

PRG(Post-Redirect-Get)パターンは、Webアプリケーションにおける重複送信問題を根本的に解決する重要な設計パターンです。

適用を推奨するケース

  • データ更新を伴うフォーム処理(注文、登録、決済など)
  • 外部システム連携が含まれる処理
  • 取り消し不可能な操作
  • ユーザビリティを重視するアプリケーション

Spring Bootでの実装ポイント

  1. redirect:プレフィックスでリダイレクト実行
  2. RedirectAttributesでフラッシュスコープ活用
  3. Model Attributeによるフォームデータバインディングとの組み合わせ
  4. 適切なエラーハンドリングの統一的実装

PRGパターンは、初期の実装コストはかかりますが、長期的なユーザビリティとデータ整合性の向上により、確実にその投資を回収できる重要なパターンです。特にエンタープライズレベルのWebアプリケーションでは、必須の実装パターンと言えるでしょう。

現在のプロジェクトでフォーム処理に課題を感じている場合は、ぜひPRGパターンの導入を検討してみてください。


補足:RedirectAttributesとフラッシュスコープ

PRGパターンでは、リダイレクト間でのデータ保持が重要です。ここで活用されるのがフラッシュスコープという仕組みです。

フラッシュスコープとは

フラッシュスコープは、リダイレクト後の1回のリクエストでのみ有効な一時的なデータ保存領域です:

  • 通常のセッション: ユーザーがログアウトするまで永続化
  • フラッシュスコープ: リダイレクト先での1回のアクセスで自動消去
  • リクエストスコープ: 同一リクエスト内でのみ有効(リダイレクトで消失)
// POST → リダイレクト → GET の流れでデータを橋渡し
redirectAttributes.addFlashAttribute("message", "処理完了"); // フラッシュスコープに保存
// ↓ リダイレクト実行
// ↓ GET リクエストでフラッシュスコープからデータ取得(自動)
// ↓ GET レスポンス後、フラッシュスコープのデータは自動削除

この仕組みにより、リダイレクト後に一度だけメッセージやフォームデータを表示でき、その後のページ更新では表示されません。
関連記事:

Discussion