Tech Blog

Spring MVC の @SessionAttributes で検索条件を画面遷移後も保持する

Spring Boot Thymeleaf Spring MVC

はじめに

管理画面を作っているとき、こんな場面があります。

  1. 作品一覧で「アクション」カテゴリでフィルタ
  2. 特定の作品の編集ページへ遷移
  3. 保存して一覧に戻ると……フィルタがリセットされている

この「戻ったらフィルタが消えた問題」を @SessionAttributes で解決した話です。


問題の構造

Spring MVCのコントローラはリクエストごとに新しいインスタンスが作られます。
フォームの入力値を Model に入れても、次のリクエストで消えてしまいます。

@Controller
@RequestMapping("/admin/films")
public class FilmController {
    
    @GetMapping
    public String list(@ModelAttribute FilmSearchForm form, Model model) {
        // form は毎回新しいインスタンス → 検索条件が消える
        List<Film> films = filmService.search(form);
        model.addAttribute("films", films);
        return "admin/films/list";
    }
}

@SessionAttributes の使い方

@SessionAttributes をコントローラクラスに付けると、指定したオブジェクトをセッションに保存します。

@Controller
@RequestMapping("/admin/films")
@SessionAttributes("filmSearchForm")  // ← これを追加
public class FilmController {
    
    @ModelAttribute("filmSearchForm")
    public FilmSearchForm initSearchForm() {
        return new FilmSearchForm();  // セッションに存在しない場合の初期値
    }
    
    @GetMapping
    public String list(
            @ModelAttribute("filmSearchForm") FilmSearchForm form,
            Model model) {
        
        List<Film> films = filmService.search(form);
        model.addAttribute("films", films);
        return "admin/films/list";
    }
    
    @PostMapping("/search")
    public String search(
            @ModelAttribute("filmSearchForm") FilmSearchForm form,
            BindingResult result) {
        
        if (result.hasErrors()) {
            return "admin/films/list";
        }
        // POST後にGETへリダイレクト(PRGパターン)
        return "redirect:/admin/films";
    }
}

フローの説明

  1. 初回アクセス時:@ModelAttribute メソッドが FilmSearchForm を生成してセッションに保存
  2. 検索フォーム送信:form の値がセッションに更新される
  3. 別ページへ遷移して戻る:セッションから form を復元 → 検索条件が維持される

セッションをクリアする

「検索条件をリセット」ボタンを実装する場合、セッションの値を削除します。

@PostMapping("/search/reset")
public String resetSearch(SessionStatus sessionStatus) {
    sessionStatus.setComplete();  // セッション属性を削除
    return "redirect:/admin/films";
}

SessionStatus#setComplete() を呼ぶと、このコントローラで管理しているセッション属性がすべてクリアされます。


Thymeleafでの記述

<!-- list.html -->
<form method="post" action="/admin/films/search">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    
    <input type="text" name="keyword" 
           th:value="${filmSearchForm.keyword}" 
           placeholder="タイトル検索">
    
    <select name="categoryId">
        <option value="">すべてのカテゴリ</option>
        <option th:each="cat : ${categories}"
                th:value="${cat.categoryId}"
                th:text="${cat.name}"
                th:selected="${cat.categoryId == filmSearchForm.categoryId}">
        </option>
    </select>
    
    <button type="submit">検索</button>
    <button type="submit" formaction="/admin/films/search/reset">リセット</button>
</form>

th:value="${filmSearchForm.keyword}" でセッションから復元した値がフォームに表示されます。


注意点

① コントローラごとにセッションが分かれる

@SessionAttributes はコントローラ単位です。
FilmController で保存した filmSearchFormActorController からは見えません。

② セッションが残り続ける問題

タブを複数開いたり、ブラウザを閉じずに長時間操作すると、セッションに大量のオブジェクトが溜まることがあります。
SessionStatus#setComplete() の呼び出しタイミングを設計に含めてください。

③ マルチタブでの競合

同じブラウザで2つのタブを開いてそれぞれ異なる条件で検索すると、セッションが共有されるため予期しない動作になることがあります。
管理画面のような用途では問題になりにくいですが、注意が必要です。


localStorage との使い分け(Vue 3の場合)

顧客向けアプリ(Vue 3)では、localStorage でフィルタ状態を保持しています。

保持手段向いている場面
@SessionAttributesSpring MVC + Thymeleaf、サーバーサイドでの状態管理
localStorageSPA(Vue/React)、クライアントサイドでの状態管理
Pinia(Vue 3)SPAでのグローバル状態管理(ページリロードで消える)
Pinia + localStorageページリロード後も状態を保持したいSPA

まとめ

@SessionAttributes は、Spring MVC + Thymeleafで検索条件や入力値を画面遷移後も保持するための標準的な方法です。

  • @SessionAttributes("formName") — コントローラに付けてセッション保存対象を宣言
  • @ModelAttribute("formName") — セッションに存在しない場合の初期値を生成
  • SessionStatus#setComplete() — セッションをクリアするリセット処理

単純に実装すると「戻ったら消えた」問題が起きやすいので、最初から設計に含めておくと後で楽になります。


このシリーズの記事マップ

dvdrental 管理アプリと対になるエンドユーザー向けDVDレンタルアプリを作っている話 — Vue 3 + Spring Boot の全体構成と記事マップ
PostgreSQL のサンプル DB dvdrental をベースに Spring Boot + Thymeleaf で DVD レンタル管理アプリを作った話

気軽にメッセージください

技術相談・ご感想・ご質問があればメッセージをお願いします。