views:

9899

answers:

4

I am using Spring MVC 2.5, and I am trying to get a JSTL form object to load from a GET request. I have Hibernate POJOs as my backing objects.

There is one page directing to another page with a class id (row primary key) in the request. The request looks like "newpage.htm?name=RowId". This is going into a page with a form backing object,

The newpage above, loads the fields of the object into editable fields, populated with the existing values of the row. The idea is, that you should be able to edit these fields and then persist them back into the database.

The view of this page looks something like this

<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>

The controller has this in it,

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);
    }
}

As you can see from the commented code, I've tried manually setting the object id (name is this case) from the request. However Hibernate complains about the object being desynched when I try and persist the data in the form.

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

This error seems to do something to the entire session, which stops working for my entire web application, continually throwing the Stale Object State Exception seen above.

If anyone familiar with Spring MVC can help me with this or suggest a workaround, I would really appreciate it.

EDIT:
Session factory code.

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;
}
+1  A: 

Your issue may be related to Detached Objects. Because your DAO has been modified outside a Hibernate session, you need to reattach the object to the Hibernate session before saving. You can do this either by explicitly bringing the object into the session before saving using Merge() or update(). Experiment with both, and read the documentation for those actions, as they have different effects depending on the structure of your data objects.

Elie
The SimpleDAO.saveThingie(thingie) call is doing a session.saveOrUpdate(thingie) behind the scene.
James McMahon
Merge() leads to "org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session"
James McMahon
But despite that exception, a new row went into the database. However, for some reason the row id is identifier, identifier. Like it is taking the identifier from the GET and applying it twice.
James McMahon
when using merge, I think you need to first put the object into session (using a get with the identifier) and then calling merge on the detached object, which will push your version into the database.
Elie
Could you expand on that last comment, I am not sure if I follow you? Wouldn't merge put the object into the session?
James McMahon
I found that while merge is "supposed" to put the object into session, it doesn't always do it right. So I explicitly put the object into session using get() and then call merge(), which essentially tells hibernate that I'm sure my version of the object is more correct.
Elie
Using the get method you need a serializable id, where would I get this from?
James McMahon
Your id field from the database should be serializable, as should all of your DAOs. Just make your classes implement serializable (you are using a numeric field as a primary id, right?)
Elie
No actually, I originally had numeric fields everywhere for primary keys, but my boss talked me into using natural keys.
James McMahon
What do you mean by "natural keys"?
Elie
An attribute that uniquely identifies a row. Even with an "artificial" auto generated numeric id, this string (nvchar) column would exist and uniquely identify the row. So with the natural key philosophy you use these attributes as keys rather then the a generated artificial key.
James McMahon
You should still be able to serialize your class, though. A String is serializable, so you should not have any issues.
Elie
+2  A: 

To answer your immediate question, the problem you are having with Hibernate has to do with the following sequence of events:

  1. One Hibernate session is opened (let's call it session A) in formBackingObject
  2. Using session A, you load a Thingie object in formBackingObject
  3. You return the Thingie object as the result of formBackingObject
  4. When you return the Thingie object, session A is closed, but Thingie is still linked to it
  5. When doSubmitAction is called, the same instance of the Thingie backing object is passed as the command
  6. A new Hibernate session (call it session B) is opened
  7. You attempt to save the Thingie object (originally opened with session A) using session B

Hibernate doesn't know anything about session A at this point, because it's closed, so you get an error. The good news is that you shouldn't be doing it that way, and the correct way will bypass that error completely.

The formBackingObject method is used to populate a form's command object with data, prior to showing the form. Based on your updated question, it sounds like you are simply trying to display a form populated with information from a given database row, and update that database row when the form is submitted.

It looks like you already have a model class for your record; I'll call that the Record class in this answer). You also have DAO for the Record class, which I will call RecordDao. Finally, you need a UpdateRecordCommand class that will be your backing object. The UpdateRecordCommand should be defined with the following fields and setters/getters:

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
}

Then define your form using the following code:

<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>

Now you define your form controller, which will populate the form in formBackingObject and process the update request in 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);
  }
}
William Brendel
Sorry, I've tried to clarify the question with more information about what I am trying to accomplish. Persist is actually a really bad name for that method. What it actually does is load the data from the database.
James McMahon
Thank you for the extensive answer. However I am using a session factory to ensure I only have one session object at a time. I will post details above in the question.
James McMahon
Would the session factory avoid the need for a discrete separation that you demonstrate above?
James McMahon
Nope, you are confusing a command object with your model object. There should be a separate command object that basically stores the contents of the form being submitted. Then, based on that command object, you load the model object (Record), make the changes, and save the model object.
William Brendel
So, there should be a clear separation. A session factory will not help in this case, because you are really handling two separate requests: the initial form load, and the form submission.
William Brendel
Thanks, I am relatively new to Hibernate and Spring, so I am still fuzzy on some things. Let me trying implementing your method and see how it goes.
James McMahon
Great, let me know how it goes. In the meantime, I recommend doing this tutorial, step-by-step. It really is the best introduction to Spring MVC out there. http://static.springframework.org/docs/Spring-MVC-step-by-step/
William Brendel
Looking at it more, it is kinda of strange, because you are creating two classes that are essentially the same. Would it be viable to create two instances of the Hibernate POJO?
James McMahon
The problem is that Hibernate does "magic" behind the scenes to allow you to treat a database object as a POJO. The classes may look identical in this situation, but they serve different purposes (one represents a database record, one represents a form submission operation).
William Brendel
+1  A: 

What was going on is that the ?name=rowId was somehow messing up the form post. Once I changed that to a name that didn't reflect a parameter in the object, everything worked fine. No change to the DAO or controller code necessary.

Thanks to everyone for your answers. It helped me narrow down what was going on.

James McMahon
+3  A: 

One of the major flaws with using Spring MVC + hibernate is that the natural approach is to use the hibernate domain object as the backing object for the form. Spring will bind anything in the request based on name by DEFAULT. This inadvertently includes things like ID or name (usually the primary key) or other hibernate managed properties being set. This also makes you vulnerable to form injection.

In order to be secure under this scenario you must use something like:

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

and EXPLICITLY set the ALLOWED fields to only those in your form, and exclude the primary key or you will end up with a mess!!!

Justin
Thanks for explaining what was actually going on here. I'll have to give this a try when I return to this project. In the meantime, I'm going to give you the answer, since it certainly sounds like you know what you are talking about :)
James McMahon
I've since gone back and confirmed that this does work. Thank you for the insight.
James McMahon