Конфликты: Spring MVC с валидатором Hibernate — действие отправки/привязки

У меня есть две сущности, это: Человек и Адрес, отношение один к одному. Исходный код JPA/Hibernate был удален для простоты.

Моя проблема связана с Hibernate Validator.

У меня есть следующее для класса Person:

@Valid
@NotNull(message="{person.address.null}", groups={AddressCheck.class})
public Address getAddress() {
    return address;
}

и для Адрес:

@NotNull(message="{field.null}", groups=AddressCheck.class)
public Person getPerson() {
    return person;
}

Примечание. Что касается JUnit, все работает нормально, если какая-то зависимость имеет значение null, создается соответствующая ошибка.

Проблема заключается в том, что когда Hibernate Validator интегрирован с Spring MVC, он использует подход binding

О коде представления ниже фрагмента JSP для класса Address

<form:form modelAttribute="person"  method="post" >
...
<fieldset>
    <legend><spring:message code="address.form.legend"/></legend>
    <table>
        <form:hidden path="address.id"/> 
        <tr>
            <td><spring:message code="address.form.street"/></td>
            <td><form:input path="address.street" size="40"/></td>
            <td><form:errors path="address.street" cssClass="error"/></td>
        </tr>
        <tr>
            <td><spring:message code="address.form.number"/></td>
            <td><form:input path="address.number" size="40"/></td>
            <td><form:errors path="address.number" cssClass="error"/></td>
        </tr>
    </table>
</fieldset>

Чтобы создать представление формы, чтобы JSP отображал оба объекта, у меня есть следующее:

@RequestMapping(value="/register.htm", method=RequestMethod.GET)
public String createRegisterForm(Model model){
    logger.info("createRegisterForm GET");
    Person person = new Person();
    Address address = new Address();
    address.setId("5");//just to play/test

    person.setAddress(address);
    address.setPerson(person);

    model.addAttribute("person", person);
    return "jsp/person/registerForm";
}

Обратите внимание, как вызываются два сеттера.

Когда я делаю отправку, должен быть выполнен следующий код

@RequestMapping(value="/register.htm", method=RequestMethod.POST)
public String registerPerson( @Validated(PersonRegistrationOrdered.class) Person person, 
                              BindingResult result,
                              Model model){
    logger.info("registerPerson POST");
    logger.info("{}", person.toString());
    logger.info("{}", person.getAddress().toString());

    if(result.hasErrors()){
        logger.error("There are errors!!!!");
        model.addAttribute("person", person);

        for(ObjectError objectError : result.getAllErrors()){
            logger.error("Error {}", objectError);
        }

        return "jsp/person/registerForm";
    }
    logger.info("All fine!!!!");
    return "jsp/manolo";
}

К сожалению, консоль показывает ошибку о том, что объект Person из класса Address имеет значение null, даже если в createRegisterForm он был связан address.setPerson(person);

Мой уникальный способ обойти это - отключить проверку

//@NotNull(message="{field.null}", groups=AddressCheck.class)
public Person getPerson() {
    return person;
}

Первый вопрос: как этого избежать, не отключая проверку @NotNull?

Более того, если я хочу защитить id для класса Address:

@InitBinder
public void initBinder(WebDataBinder binder) {
    ...
    binder.setDisallowedFields("address.id");
}

Код HTML, сгенерированный из файла JSP,

<fieldset>
    <legend>Address:</legend>
    <table>
        <input id="address.id" name="address.id" type="hidden" value="5"/> 
        <tr>
            <td>Street:</td>
            <td><input id="address.street" name="address.street" type="text" value="" size="40"/></td>
            <td></td>
        </tr>
        <tr>
            <td>Number:</td>
            <td><input id="address.number" name="address.number" type="text" value="" size="40"/></td>
            <td></td>
        </tr>
    </table>
</fieldset>

Но опять же, когда я делаю отправку, я получаю

- ValidationMessages found.
- org.hibernate.validator.ValidationMessages found.
- registerPerson POST
- Person [id=5, firstName=Manuel, lastName=Jordan, alive=true, dateBirth=Wed Dec 12 00:00:00 PET 2012, dateRetirement=null, dateDeath=null, age=15, weight=34.0, salary=1500]
- Address [id=null, street=La blanquita, number=155]
- There are errors!!!!
- Error Field error in object 'person' on field 'address.id': rejected value [null]; codes [NotNull.person.address.id,NotNull.address.id,NotNull.id,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.address.id,address.id]; arguments []; default message [address.id]]; default message [The field must be not empty]

Если я создаю нового человека и использую, возможно, специальный класс, дающий потенциальный идентификатор (5, например, address.setId("5"); в данном случае) для идентификатора адреса, это будет проблемой.

Второй вопрос: как решить эту проблему?


person Manuel Jordan    schedule 09.09.2014    source источник


Ответы (1)


Проблема в том, что вы создаете свою модель в методе с GET. Это означает, что как только страница отображается, объект модели исчезает. Когда POST входит, создается новый пустой Person, а из-за автоматического расширения пути также Address, но это не будет иметь правильного отношения.

Есть 2 способа исправить это.

  1. Создайте метод с аннотацией @ModelAttribute, который создает модель, необходимую для привязки.
  2. Добавлен @SessionAttributes для хранения Person в сеансе между запросами.

Еще одна вещь, чтобы любой вариант работал, находится рядом с @Validated, вам нужно будет добавить @ModelAttribute к аргументу метода Person вашего метода.

Метод с @ModelAttribute

Сначала добавьте метод, который создает объект модели. Для этого создайте метод и аннотируйте его @ModelAttribute и, при желании, дайте ему имя объекта модели person. (Что также будет по умолчанию, так как это означает использование имени класса с первой буквой нижнего регистра).

@ModelAttribute("person")
public Person prepareModel() {
    Person person = new Person();
    Address address = new Address();
    address.setId("5");//just to play/test

    person.setAddress(address);
    address.setPerson(person);
    return person;
}

Затем измените методы GET и POST. Прежде всего, для обоих методов вы можете удалить Model в качестве аргумента метода, поскольку он больше не нужен. Person уже добавлен в модель.

@RequestMapping(value="/register.htm", method=RequestMethod.GET)
public String createRegisterForm(){
    logger.info("createRegisterForm GET");
    return "jsp/person/registerForm";
}

А для POST добавить @ModelAttribute и не надо

@RequestMapping(value="/register.htm", method=RequestMethod.POST)
public String registerPerson( @Validated(PersonRegistrationOrdered.class) @ModelAttribute Person person, 
                              BindingResult result){
    logger.info("registerPerson POST");
    logger.info("{}", person.toString());
    logger.info("{}", person.getAddress().toString());

    if(result.hasErrors()){
        logger.error("There are errors!!!!");

        for(ObjectError objectError : result.getAllErrors()){
            logger.error("Error {}", objectError);
        }

        return "jsp/person/registerForm";
    }
    logger.info("All fine!!!!");
    return "jsp/manolo";
}

Использование @SessionAttributes

Используя @SessionAttributes, вы можете определить, какие объекты временно хранить в папке HttpSession. Добавьте эту аннотацию в свои классы контроллеров, а в методе POST добавьте SessionStatus в качестве аргумента и удалите Model, поскольку он уже будет заполнен.

@Controller
@SessionAttributes(Person.class)
public class RegisterController {

    @RequestMapping(value="/register.htm", method=RequestMethod.POST)
    public String registerPerson( @Validated(PersonRegistrationOrdered.class) @ModelAttribute Person person, 
                                  BindingResult result, SessionStatus status){
        logger.info("registerPerson POST");
        logger.info("{}", person.toString());
        logger.info("{}", person.getAddress().toString());

        if(result.hasErrors()){
            logger.error("There are errors!!!!");

            for(ObjectError objectError : result.getAllErrors()){
                logger.error("Error {}", objectError);
            }

            return "jsp/person/registerForm";
        }
        logger.info("All fine!!!!");
        status.setComplete();
        return "jsp/manolo";
    }

}

Вам нужно вызвать метод setComplete() для SessionStatus, чтобы сеанс был очищен от устаревших объектов модели.

person M. Deinum    schedule 10.09.2014
comment
Я думаю, что @SessionAttributes лучше, если у нас есть несколько пользователей, создающих Person (конечно, разные значения для каждого случая) в одно и то же время. - person Manuel Jordan; 10.09.2014
comment
Это не имеет значения, @ModelAttribute вызывается перед каждым методом обработки запроса. Таким образом, каждый запрос получает новый/свежий экземпляр класса Person. - person M. Deinum; 10.09.2014
comment
Истинный. Кстати, я не создаю новый метод и не использую @ModelAttribute в соответствии с вашим первым предложением, потому что, когда я делаю отправку (POST), он снова вызывает метод с @ModelAttribute. Конечно, ни одно значение не переопределяется при создании нового пустого объекта Person, я использую второй вариант, @SessionAttributes и создаю объект в методе (GET) и использую Model.addAttribute, как я делал с самого начала. Все работает нормально. Спасибо еще раз - person Manuel Jordan; 10.09.2014
comment
Метод с @ModelAttribute не вызывается, когда происходит действие POST, поскольку объект person уже существует в модели. Практически это работает как Карта. Идеальный - person Manuel Jordan; 11.09.2014
comment
Ну, это называется, если вы не используете @SessionAttributes, если вы используете @SessionAttributes, он сначала проверит сеанс, чтобы увидеть, существует ли уже объект модели с заданным именем (или типом). - person M. Deinum; 11.09.2014
comment
Да, вы правы, @ModelAttribute вызывается при вызове POST и, когда @SessionAttributes не используется, и для модели не происходит переопределения. Спасибо еще раз - person Manuel Jordan; 11.09.2014
comment
Кажется мудрой и хорошей практикой использовать @ModelAttribute для GET и сохранять его в @SessionAttributes для использования в POST. - person Manuel Jordan; 11.09.2014
comment
Для аудитории избегайте @SessionAttributes в случае, если у контроллера есть возможность создавать и обновлять объект формы, это огромная проблема, если пользователь открывает много вкладок в одном и том же браузере. - person Manuel Jordan; 17.09.2014