Spring MVC: наличие нескольких @ModelAttribute в действии обработки формы

Контекст

У меня есть простая ассоциация между двумя сущностями - Category и Email (NtoM). Я пытаюсь создать веб-интерфейс для просмотра и управления ими. Чтобы просмотреть категорию и добавить электронные письма в эту категорию, я использую контроллер, обернутый @RequestMapping с идентификатором категории (UUID), поэтому все действия контроллера всегда происходят в контексте категории, указанной с помощью пути.

Я использую @ModelAttribute для предварительной загрузки категории контекста для всей области действия контроллера.

Проблема

Этот подход хорошо работал для листинга и отображения форм. Однако это не удается при отправке формы - после небольшой отладки я обнаружил, что данные формы переопределяют параметр моей категории @ModelAttribute.

В моем коде в методе save() category на самом деле не является атрибутом модели, загруженным с помощью метода addCategory(), но заполняется данными формы (модель email также заполняется, и это правильно).

Я ищу решение, которое позволит мне привязать данные формы только к определенному @ModelAttribute.

Я читал в документации Spring MVC, что порядок аргументов имеет значение, но я упорядочил их в соответствии с примерами, и все же это не работает, как ожидалось.

Код

Вот мой контроллер:

@Controller
@RequestMapping("/emails/{categoryId}")
public class EmailsController
{
    @ModelAttribute("category")
    public Category addCategory(@PathVariable UUID categoryId)
    {
        return this.categoryService.getCategory(categoryId);
    }

    @InitBinder
    public void initBinder(WebDataBinder binder)
    {
        binder.registerCustomEditor(Set.class, "categories", new CategoriesSetEditor(this.categoryService));
    }

    @RequestMapping(value = "/create", method = RequestMethod.GET)
    public String createForm(@ModelAttribute Category category, Model model)
    {
        // here everything works, as there is just a single @ModelAttribute

        return "emails/form";
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public String save(
        @ModelAttribute @Valid Email email,
        BindingResult result,
        Model model,
        @ModelAttribute("category") Category category
    ) {
        // saving entity, etc

        // HERE! problem is, that response is bound BOTH to `email' and `category' model attributes
        // and overrides category loaded in `addCategory()' method
        return String.format("redirect:/emails/%s/", category.getId().toString());
    }
}

На всякий случай вот еще код формы:

<form:form action="${pageContext.request.contextPath}/emails/${category.id}/save" method="post" modelAttribute="email">
    <form:hidden path="id"/>
    <fieldset>
        <label for="emailName"><spring:message code="email.form.label.Name" text="E-mail address"/>:</label>
        <form:input path="name" id="emailName" required="required"/>
        <form:errors path="name" cssClass="error"/>

        <label for="emailRealName"><spring:message code="email.form.label.RealName" text="Recipient display name"/>:</label>
        <form:input path="realName" id="emailRealName"/>
        <form:errors path="realName" cssClass="error"/>

        <label for="emailIsActive"><spring:message code="email.form.label.IsActive" text="Activation status"/>:</label>
        <form:checkbox path="active" id="emailIsActive"/>
        <form:errors path="active" cssClass="error"/>

        <form:checkboxes path="categories" element="div" items="${categories}" itemValue="id" itemLabel="name"/>
        <form:errors path="categories" cssClass="error"/>

        <button type="submit"><spring:message code="_common.form.Submit" text="Save"/></button>
    </fieldset>
</form:form>

Примечание. Я не хочу, чтобы несколько @ModelAttribute пришли из POST, просто хочу каким-то образом отличить модель формы от ранее сгенерированных атрибутов.


person Rafał Wrzeszcz    schedule 08.10.2013    source источник


Ответы (1)


Я не уверен, что полностью понимаю проблему, но мне кажется, что вы хотите, чтобы объект категории присутствовал в модели при отображении формы, но не хотите, чтобы он был изменен при публикации формы?

Когда вы указываете @ModelAttribute("categories") в списке аргументов, вы в основном говорите Spring MVC привязать данные формы к аннотированному объекту, используя имя параметра "categories".

Если вы не хотите, чтобы объект был привязан, просто исключите его из списка параметров. Если вам нужен исходный объект в методе обработчика, извлеките его вручную, вызвав addCategory и указав идентификатор, сопоставленный с @PathVariable:

@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(
    @ModelAttribute @Valid Email email,
    BindingResult result,
    Model model,
    @PathVaribale("categoryId") UUID categoryId
) {
    // saving entity, etc

    return String.format("redirect:/emails/%s/", categoryId.toString());
    //if category object is needed and not just id then fetch it with Category c = addCategory(categoryId).
}

(PS. Если вы зарегистрируете преобразователь, который преобразует Long в категорию с помощью categoryService, вы также можете поместить @PathVariable("categoryId") Category category для сопоставления объекта категории с переменной пути вместо UUID, если хотите, взгляните на 7.5.5 Настройка ConversionService)

(EDIT: удалено предложение назвать модель по-другому, так как это не поможет, как указано в комментариях, и добавлен пример)

Лично, если бы мне нужно было такое поведение (объект, который должен присутствовать в форме при отображении формы, но не привязан к ней при публикации формы), я бы не использовал аннотированный метод ModelAttribute для заполнения модели. Вместо этого я бы заполнил модель вручную при отображении формы. Это немного больше кода (ну, на самом деле, одна строка), но менее волшебно и легче для понимания.

person Krešimir Nesek    schedule 08.10.2013
comment
Но я не использую categories в качестве имени атрибута, я использую category, которое не пересекается ни с одним полем формы. Просто все поля формы привязаны к объекту категории (например, оба объекта имеют свойство name, а Spring привязывает значение поля формы как к category.name, так и к email.name). А для заполнения вручную при отображении формы - мне нужна категория в действии обработки отправки для перенаправления (обратите внимание на последнюю строку действия save()). - person Rafał Wrzeszcz; 08.10.2013
comment
Вы правы, переименование модели не сработает, поскольку Spring привязывает свойства. Я удалил это из ответа и отредактировал ответ с примером того, как получить исходную категорию в действии save (). - person Krešimir Nesek; 08.10.2013
comment
Была надежда решить это по-другому, но ваше решение на самом деле еще более компактно - на самом деле мне не нужен весь объект. Кажется немного странным, что такие варианты использования не поддерживаются (или, по крайней мере, я не могу узнать, как) Spring, но большое спасибо за помощь в этом. - person Rafał Wrzeszcz; 08.10.2013
comment
Если вам нужен идентификатор только для целей перенаправления, а не для какой-либо обработки при сохранении, вы можете просто вернуть redirect:/emails/{categoryId}/, и Spring заменит {categoryId} фактическим идентификатором из запроса. В этом случае вы можете вообще удалить категорию из списка аргументов метода. - person Krešimir Nesek; 08.10.2013