Spring WebMVCをやってみる (14) - @Valid+BindingResult+RedirectAttributes -
前回で出した問題点として、「エラー表示はビューをレンダリングしているのでリロードするとPOSTなリクエストになる」って件。まぁ良くあるパターンなのかは微妙な所なんですが、対策として
- ビューをレンダリングしないでフォームを出す所にリダイレクトする
- リダイレクトする際にFlashスコープでエラーを突っ込んでおく
- リダイレクト先で上記で突っ込んだエラーがあるのならば、それをビューで表示できるように構成する (BindingResult.MODEL_KEY_PREFIX + ModelAttributeな文字列をキーとする)
っていう感じじゃねーかと。んまぁ今回修正案として出すのはコントローラーだけ(それとテストも)
SampleController.java
package sample;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/sample")
public class SampleController {
    private static final String FORM_NAME = "sample_form";
    @ModelAttribute(FORM_NAME)
    public SampleForm setupForm() {
        SampleForm sample = new SampleForm();
        return sample;
    }
    @RequestMapping("index")
    public String index(ModelMap model) {
        String key = BindingResult.MODEL_KEY_PREFIX + FORM_NAME;
        if (model.containsKey("errors")) {
            model.addAttribute(key, model.get("errors"));
        }
        return "index";
    }
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public String save(
        @Valid @ModelAttribute(FORM_NAME) final SampleForm sample,
        final BindingResult bindingResult,
        final RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            // ここでBindingResult.MODEL_KEY_PREFIX + FORM_NAMEしても作用しない
            attributes.addFlashAttribute("errors", bindingResult);
            return "redirect:index.action";
        }
        attributes.addAttribute("name", sample.getName());
        return "redirect:show.action";
    }
    @RequestMapping(value = "/show")
    @ResponseBody
    public String show(@RequestParam("name") String name) {
        return "NAME: " + name;
    }
}
っていうような感じで入力検証を行なってエラーが出た場合には前回とは違い今回のはリダイレクトを行う。でリダイレクトする前に持っているBindingResultをFlashスコープに入れておいてリダイレクト後のアクションでそれを取り出して、「BindingResult.MODEL_KEY_PREFIX + ModelAttribute(ここではFORM_NAME)」で表示するエラー情報を構成する。もちろんこれはFlashスコープなのでエラーが起きた直後のみ表示されリロードすると消える
SampleControllerTest.java
package swmvc;
import org.junit.Test;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.servlet.FlashMap;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
public class SampleControllerTest extends AbstractTestCase {
    @Test
    public void test_save() throws Exception {
        MvcResult result = mock.perform(post("/sample/save").param("name", "hoge"))
            .andExpect(status().isFound())
            .andExpect(redirectedUrl("show.action?name=hoge"))
            .andReturn();
        mock.perform(get("/sample/" + result.getResponse().getRedirectedUrl()))
            .andExpect(status().isOk());
    }
    @Test
    public void test_save_fail() throws Exception {
        MvcResult result = mock.perform(post("/sample/save"))
            .andExpect(status().isFound())
            .andExpect(redirectedUrl("index.action"))
            .andExpect(flash().attributeExists("errors"))
            .andReturn();
        FlashMap map = result.getFlashMap();
        assertThat(map, hasKey("errors"));
    }
}
終わり。てな感じでんまぁRailsとかにありがちなパターン?的な事をやろうと思えば出来るのかも。このやり方が正しいかは不明ですが