Spring BootにおけるPRG(Post-Redirect-Get)パターンの実装と活用
はじめに
この記事は、「@RequestParamからの脱却と型安全性の向上」記事で軽く触れたPRG(Post-Redirect-Get)パターンについて、より詳しく解説する補足記事です。
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"; // 直接テンプレートを返す
}
この実装では、ユーザーが「更新」ボタンを押すと:
- ブラウザが同じPOSTリクエストを再送信
-
orderService.createOrder(form)
が再実行 - 同じ注文が重複して作成される
実際の被害例
- 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での実装ポイント
-
redirect:
プレフィックスでリダイレクト実行 - RedirectAttributesでフラッシュスコープ活用
- Model Attributeによるフォームデータバインディングとの組み合わせ
- 適切なエラーハンドリングの統一的実装
PRGパターンは、初期の実装コストはかかりますが、長期的なユーザビリティとデータ整合性の向上により、確実にその投資を回収できる重要なパターンです。特にエンタープライズレベルのWebアプリケーションでは、必須の実装パターンと言えるでしょう。
現在のプロジェクトでフォーム処理に課題を感じている場合は、ぜひPRGパターンの導入を検討してみてください。
補足:RedirectAttributesとフラッシュスコープ
PRGパターンでは、リダイレクト間でのデータ保持が重要です。ここで活用されるのがフラッシュスコープという仕組みです。
フラッシュスコープとは
フラッシュスコープは、リダイレクト後の1回のリクエストでのみ有効な一時的なデータ保存領域です:
- 通常のセッション: ユーザーがログアウトするまで永続化
- フラッシュスコープ: リダイレクト先での1回のアクセスで自動消去
- リクエストスコープ: 同一リクエスト内でのみ有効(リダイレクトで消失)
// POST → リダイレクト → GET の流れでデータを橋渡し
redirectAttributes.addFlashAttribute("message", "処理完了"); // フラッシュスコープに保存
// ↓ リダイレクト実行
// ↓ GET リクエストでフラッシュスコープからデータ取得(自動)
// ↓ GET レスポンス後、フラッシュスコープのデータは自動削除
この仕組みにより、リダイレクト後に一度だけメッセージやフォームデータを表示でき、その後のページ更新では表示されません。
関連記事:
Discussion