Spring MVC, генерирующий объект поддержки формы из запроса?

Я использую Spring MVC 2.5 и пытаюсь загрузить объект формы JSTL из запроса GET. В качестве вспомогательных объектов у меня есть Hibernate POJO.

Одна страница ведет на другую страницу с идентификатором класса (первичный ключ строки) в запросе. Запрос выглядит как "newpage.htm?name=RowId". Это переходит на страницу с объектом поддержки формы,

Новая страница выше загружает поля объекта в редактируемые поля, заполненные существующими значениями строки. Идея состоит в том, что вы должны иметь возможность редактировать эти поля, а затем сохранять их обратно в базу данных.

Вид этой страницы выглядит примерно так

<form:form commandName="thingie">
    <span>Name:</span>
    <span><form:input path="name" /></span>
    <br/>
    <span>Scheme:</span>
    <span><form:input path="scheme" /></span>
    <br/>
    <span>Url:</span>
    <span><form:input path="url" /></span>
    <br/>
    <span>Enabled:</span>
    <span><form:checkbox path="enabled"/></span>
    <br/>

    <input type="submit" value="Save Changes" />
</form:form>

В контроллере есть это,

public class thingieDetailController extends SimpleFormController {

    public thingieDetailController() {    
        setCommandClass(Thingie.class);
        setCommandName("thingie");
    }

    @Override
    protected Object formBackingObject(HttpServletRequest request) throws Exception {
        Thingie thingieForm = (Thingie) super.formBackingObject(request);

        //This output is always null, as the ID is not being set properly
        logger.debug("thingieForm.getName(): [" + thingieForm.getName() + "]");
        //thingieForm.setName(request.getParameter("name"));
        SimpleDAO.loadThingie(thingieForm);

        return thingieForm;
    }

    @Override
    protected void doSubmitAction(Object command) throws Exception {            
        Thingie thingie = (Thingie) command;
        SimpleDAO.saveThingie(thingie);
    }
}

Как видно из закомментированного кода, я попытался вручную установить идентификатор объекта (имя в данном случае) из запроса. Однако Hibernate жалуется на десинхронизацию объекта, когда я пытаюсь сохранить данные в форме.

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

Эта ошибка, кажется, что-то делает со всем сеансом, который перестает работать для всего моего веб-приложения, постоянно вызывая исключение состояния устаревшего объекта, показанное выше.

Если кто-нибудь, знакомый с Spring MVC, может помочь мне с этим или предложить обходной путь, я был бы очень признателен.

EDIT:
Фабричный код сеанса.

private static final SessionFactory sessionFactory;
private static final Configuration configuration = new Configuration().configure();

static {
    try {
        // Create the SessionFactory from standard (hibernate.cfg.xml) 
        // config file.
        sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
    } catch (Throwable ex) {
        // Log the exception. 
        System.err.println("Initial SessionFactory creation failed." + ex);
        throw new ExceptionInInitializerError(ex);
    }
}

public static SessionFactory getSessionFactory() {
    return sessionFactory;
}

person James McMahon    schedule 30.03.2009    source источник


Ответы (4)


Один из основных недостатков использования Spring MVC + hibernate заключается в том, что естественным подходом является использование объекта домена гибернации в качестве объекта поддержки для формы. Spring свяжет что-либо в запросе на основе имени по УМОЛЧАНИЮ. Это непреднамеренно включает такие вещи, как идентификатор или имя (обычно первичный ключ) или другие устанавливаемые управляемые свойства гибернации. Это также делает вас уязвимыми для инъекций формы.

Чтобы быть в безопасности в этом сценарии, вы должны использовать что-то вроде:

protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) 
throws Exception {
 String[] allowedFields = {"name", "birthday"}
 binder.setAllowedFields(allowedFields);
}

и ЯВНО установите в РАЗРЕШЕННЫХ полях только те, которые указаны в вашей форме, и исключите первичный ключ, иначе вы получите беспорядок !!!

person Justin    schedule 17.08.2009
comment
Спасибо за объяснение того, что на самом деле происходит здесь. Я должен буду попробовать, когда вернусь к этому проекту. А пока я собираюсь дать вам ответ, так как, похоже, вы знаете, о чем говорите :) - person James McMahon; 17.08.2009
comment
С тех пор я вернулся и подтвердил, что это действительно работает. Спасибо за понимание. - person James McMahon; 16.12.2009

Чтобы ответить на ваш непосредственный вопрос, проблема, с которой вы столкнулись в Hibernate, связана со следующей последовательностью событий:

  1. Один сеанс Hibernate открыт (назовем его сеансом A) в formBackingObject
  2. Используя сеанс A, вы загружаете объект Thingie в formBackingObject
  3. Вы возвращаете объект Thingie в результате formBackingObject
  4. Когда вы возвращаете объект Thingie, сеанс A закрывается, но Thingie по-прежнему связан с ним.
  5. При вызове doSubmitAction в качестве команды передается тот же экземпляр объекта поддержки Thingie
  6. Открывается новый сеанс Hibernate (назовем его сеансом B).
  7. Вы пытаетесь сохранить объект Thingie (первоначально открытый в сеансе A) с помощью сеанса B.

На данный момент Hibernate ничего не знает о сеансе A, потому что он закрыт, поэтому вы получаете сообщение об ошибке. Хорошая новость заключается в том, что вы не должны делать это таким образом, и правильный способ полностью обойдет эту ошибку.

Метод formBackingObject используется для заполнения объекта команды формы данными перед отображением формы. Основываясь на вашем обновленном вопросе, похоже, вы просто пытаетесь отобразить форму, заполненную информацией из данной строки базы данных, и обновить эту строку базы данных при отправке формы.

Похоже, у вас уже есть класс модели для вашей записи; В этом ответе я назову это классом Record). У вас также есть DAO для класса Record, который я назову RecordDao. Наконец, вам нужен класс UpdateRecordCommand, который будет вашим вспомогательным объектом. UpdateRecordCommand должен быть определен со следующими полями и сеттерами/геттерами:

public class UpdateRecordCommand {
  // Row ID of the record we want to update
  private int rowId;
  // New name
  private int String name;
  // New scheme
  private int String scheme;
  // New URL
  private int String url;
  // New enabled flag
  private int boolean enabled;

  // Getters and setters left out for brevity
}

Затем определите свою форму, используя следующий код:

<form:form commandName="update">
  <span>Name:</span>
  <span><form:input path="name" /></span><br/>
  <span>Scheme:</span>
  <span><form:input path="scheme" /></span><br/>
  <span>Url:</span>
  <span><form:input path="url" /></span><br/>
  <span>Enabled:</span>
  <span><form:checkbox path="enabled"/></span><br/>
  <form:hidden path="rowId"/>
  <input type="submit" value="Save Changes" />
</form:form>

Теперь вы определяете свой контроллер формы, который будет заполнять форму в formBackingObject и обрабатывать запрос на обновление в doSubmitAction.

public class UpdateRecordController extends SimpleFormController {

  private RecordDao recordDao;

  // Setter and getter for recordDao left out for brevity

  public UpdateRecordController() {    
      setCommandClass(UpdateRecordCommand.class);
      setCommandName("update");
  }

  @Override
  protected Object formBackingObject(HttpServletRequest request)
      throws Exception {
    // Use one of Spring's utility classes to cleanly fetch the rowId
    int rowId = ServletRequestUtils.getIntParameter(request, "rowId");

    // Load the record based on the rowId paramrter, using your DAO
    Record record = recordDao.load(rowId);

    // Populate the update command with information from the record
    UpdateRecordCommand command = new UpdateRecordCommand();

    command.setRowId(rowId);
    command.setName(record.getName());
    command.setScheme(record.getScheme());
    command.setUrl(record.getUrl());
    command.setEnabled(record.getEnabled());

    // Returning this will pre-populate the form fields
    return command;
  }

  @Override
  protected void doSubmitAction(Object command) throws Exception {
    // Load the record based on the rowId in the update command
    UpdateRecordCommand update = (UpdateRecordCommand) command;
    Record record = recordDao.load(update.getRowId());

    // Update the object we loaded from the data store
    record.setName(update.getName());
    record.setScheme(update.getScheme());
    record.setUrl(update.getUrl());
    record.setEnabled(update.setEnaled());

    // Finally, persist the data using the DAO
    recordDao.save(record);
  }
}
person William Brendel    schedule 30.03.2009
comment
Извините, я попытался уточнить вопрос с дополнительной информацией о том, чего я пытаюсь достичь. Persist на самом деле очень плохое имя для этого метода. На самом деле он загружает данные из базы данных. - person James McMahon; 30.03.2009
comment
Спасибо за обширный ответ. Однако я использую фабрику сеансов, чтобы гарантировать, что у меня есть только один объект сеанса за раз. Я опубликую подробности выше в вопросе. - person James McMahon; 30.03.2009
comment
Избавится ли фабрика сеансов от необходимости дискретного разделения, которое вы продемонстрировали выше? - person James McMahon; 30.03.2009
comment
Нет, вы путаете объект команды с объектом вашей модели. Должен быть отдельный объект команды, который в основном хранит содержимое отправляемой формы. Затем на основе этого командного объекта вы загружаете объект модели (запись), вносите изменения и сохраняете объект модели. - person William Brendel; 30.03.2009
comment
Поэтому должно быть четкое разделение. Фабрика сеансов в этом случае не поможет, потому что вы действительно обрабатываете два отдельных запроса: начальную загрузку формы и отправку формы. - person William Brendel; 30.03.2009
comment
Спасибо, я относительно новичок в Hibernate и Spring, поэтому некоторые вещи мне все еще непонятны. Позвольте мне попробовать реализовать ваш метод и посмотреть, как это происходит. - person James McMahon; 30.03.2009
comment
Отлично, дайте мне знать, как дела. А пока я рекомендую делать это руководство шаг за шагом. Это действительно лучшее введение в Spring MVC. static.springframework.org/docs/Spring-MVC-step-by -шаг - person William Brendel; 30.03.2009
comment
Глядя на это подробнее, это немного странно, потому что вы создаете два класса, которые по сути одинаковы. Будет ли целесообразно создать два экземпляра Hibernate POJO? - person James McMahon; 31.03.2009
comment
Проблема в том, что Hibernate творит чудеса за кулисами, позволяя вам рассматривать объект базы данных как POJO. В этой ситуации классы могут выглядеть одинаково, но они служат разным целям (один представляет запись базы данных, другой — операцию отправки формы). - person William Brendel; 31.03.2009

Ваша проблема может быть связана с отсоединенными объектами. Поскольку ваш DAO был изменен вне сеанса Hibernate, вам необходимо повторно прикрепить объект к сеансу Hibernate перед сохранением. Вы можете сделать это либо явно перенеся объект в сеанс перед сохранением с помощью Merge() или update(). Поэкспериментируйте с обоими и прочитайте документацию по этим действиям, так как они имеют разные эффекты в зависимости от структуры ваших объектов данных.

person Elie    schedule 30.03.2009
comment
Вызов SimpleDAO.saveThingie(thingie) выполняет session.saveOrUpdate(thingie) за сценой. - person James McMahon; 30.03.2009
comment
Merge() приводит к org.hibernate.NonUniqueObjectException: другой объект с тем же значением идентификатора уже был связан с сеансом - person James McMahon; 30.03.2009
comment
Но, несмотря на это исключение, в базу данных вошла новая строка. Однако по какой-то причине идентификатор строки является идентификатором, идентификатором. Как будто он берет идентификатор из GET и дважды применяет его. - person James McMahon; 30.03.2009
comment
при использовании слияния, я думаю, вам нужно сначала поместить объект в сеанс (используя получение с идентификатором), а затем вызвать слияние для отсоединенного объекта, что втолкнет вашу версию в базу данных. - person Elie; 30.03.2009
comment
Не могли бы вы расширить этот последний комментарий, я не уверен, что подписан на вас? Разве слияние не поместит объект в сеанс? - person James McMahon; 30.03.2009
comment
Я обнаружил, что, хотя предполагается, что слияние помещает объект в сеанс, оно не всегда делает это правильно. Поэтому я явно помещаю объект в сеанс, используя get(), а затем вызываю merge(), который, по сути, сообщает hibernate, что я уверен, что моя версия объекта более правильная. - person Elie; 30.03.2009
comment
Используя метод get, вам нужен сериализуемый идентификатор, откуда я могу его получить? - person James McMahon; 31.03.2009
comment
Ваше поле id из базы данных должно быть сериализуемым, как и все ваши DAO. Просто сделайте свои классы реализуемыми сериализуемыми (вы используете числовое поле в качестве основного идентификатора, верно?) - person Elie; 31.03.2009
comment
На самом деле нет, изначально у меня везде были числовые поля для первичных ключей, но мой босс уговорил меня использовать естественные ключи. - person James McMahon; 31.03.2009
comment
Что вы подразумеваете под естественными ключами? - person Elie; 31.03.2009
comment
Атрибут, который однозначно идентифицирует строку. Даже с искусственным автоматически сгенерированным числовым идентификатором этот строковый столбец (nvchar) будет существовать и однозначно идентифицирует строку. Таким образом, с философией естественного ключа вы используете эти атрибуты в качестве ключей, а не сгенерированный искусственный ключ. - person James McMahon; 31.03.2009
comment
Однако вы все равно должны иметь возможность сериализовать свой класс. Строка сериализуема, поэтому у вас не должно возникнуть проблем. - person Elie; 01.04.2009

Происходило то, что ?name=rowId каким-то образом искажал сообщение формы. Как только я изменил это имя на имя, которое не отражало параметр в объекте, все заработало нормально. Никаких изменений в DAO или коде контроллера не требуется.

Спасибо всем за ваши ответы. Это помогло мне сузить круг происходящего.

person James McMahon    schedule 30.03.2009