Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forcing a transaction to rollback on validation errors in Seam

Quick version: We're looking for a way to force a transaction to rollback when specific situations occur during the execution of a method on a backing bean but we'd like the rollback to happen without having to show the user a generic 500 error page. Instead, we'd like the user to see the form she just submitted and a FacesMessage that indicates what the problem was.

Long version: We've got a few backing beans that use components to perform a few related operations in the database (using JPA/Hibernate). During the process, an error can occur after some of the database operations have happened. This could be for a few different reasons but for this question, let's assume there's been a validation error that is detected after some DB writes have happened that weren't detectible before the writes occurred. When this happens, we'd like to make sure all of the db changes up to this point will be rolled back. Seam can deal with this because if you throw a RuntimeException out of the current FacesRequest, Seam will rollback the current transaction.

The problem with this is that the user is shown a generic error page. In our case, we'd actually like the user to be shown the page she was on with a descriptive message about what went wrong, and have the opportunity to correct the bad input that caused the problem. The solution we've come up with is to throw an Exception from the component that discovers the validation problem with the annotation:

@ApplicationException( rollback = true )

Then our backing bean can catch this exception, assume the component that threw it has published the appropriate FacesMessage, and simply return null to take the user back to the input page with the error displayed. The ApplicationException annotation tells Seam to rollback the transaction and we're not showing the user a generic error page.

This worked well for the first place we used it that happened to only be doing inserts. The second place we tried to use it, we have to delete something during the process. In this second case, everything works if there's no validation error. If a validation error does happen, the rollback Exception is thrown and the transaction is marked for rollback. Even if no database modifications have happened to be rolled back, when the user fixes the bad data and re-submits the page, we're getting:

java.lang.IllegalArgumentException: Removing a detached instance 

The detached instance is lazily loaded from another object (there's a many to one relationship). That parent object is loaded when the backing bean is instantiated. Because the transaction was rolled back after the validation error, the object is now detached.

Our next step was to change this page from conversation scope to page scope. When we did this, Seam can't even render the page after the validation error because our page has to hit the DB to render and the transaction has been marked for rollback.

So my question is: how are other people dealing with handling errors cleanly and properly managing transactions at the same time? Better yet, I'd love to be able to use everything we have now if someone can spot something I'm doing wrong that would be relatively easy to fix.

I've read the Seam Framework article on Unified error page and exception handling but this is geared more towards more generic errors your application might encounter.

Update: here's some psudo-code and details of the page flow.

In this case, assume we're editing some user's information (we're not actually dealing with a user in this case but I'm not going to post the actual code).

The edit functionality's edit.page.xml file contains a simple re-write pattern for a RESTful URL and two navigation rules:

  1. If result was successful edit, redirect the user to the corresponding view page to see the updated info.
  2. If the user hit the cancel button, redirect the user to the corresponding view page.

The edit.xhtml is pretty basic with fields for all of the parts of a user that can be edited.

The backing bean has the following annotations:

@Name( "editUser" )
@Scope( ScopeType.PAGE )

There are some injected components like the User:

@In
@Out( scope = ScopeType.CONVERSATION ) // outjected so the view page knows what to display
protected User user;

We have a save method on the backing bean that delegates the work for the user save:

public String save()
{
    try
    {
        userManager.modifyUser( user, newFName, newLName, newType, newOrgName );
    }
    catch ( GuaranteedRollbackException grbe )
    {
        log.debug( "Got GuaranteedRollbackException while modifying a user." );
        return null;
    }

    return USER_EDITED;
}

Our GuaranteedRollbackException looks like:

@ApplicationException( rollback = true )
public class GuaranteedRollbackException extends RuntimeException
{
    public GuaranteedRollbackException(String message) {
        super(message);
    }
}

UserManager.modifyUser looks something like this:

public void modifyUser( User user, String newFName, String newLName, String type, String newOrgName )
{
    // change the user - org relationship
    modifyUser.modifyOrg( user, newOrgName );

    modifyUser.modifyUser( user, newFName, newLName, type );
}

ModifyUser.modifyOrg does something like

public void modifyOrg( User user, String newOrgName )
{
    if (!userValidator.validateUserOrg( user, newOrgName ))
    {
        // maybe the org doesn't exist something. we don't care, the validator
        // will add the appropriate error message for us
        throw new GauaranteedRollbackException( "couldn't validate org" );
    }

    // do stuff here to change the user stuff
    ...
}

ModifyUser.modifyUser is similar to modifyOrg.

Now (you're going to have to take this leap with me because it doesn't necessarily sound like it's a problem with this User scenario but it is for the stuff we're doing) assume changing the org causes the modifyUser to fail to validate but that it's impossible to validate this failure ahead of time. We've already written the org update to the db in our current txn but since the user modify fails to validate, the GuaranteedRollbackException will mark the transaction to be rolled back. With this implementation, we're not able to use the DB in the current scope when we're rendering the edit page again to display the error message added by the validator. While rendering, we hit the db to get something to display on the page and that isn't possible because the Session is invalid:

Caused by org.hibernate.LazyInitializationException with message: "could not initialize proxy - no Session"

like image 250
Chris Williams Avatar asked May 12 '10 21:05

Chris Williams


1 Answers

I must agree with @duffymo about validating before the transaction is initiated. It is quite difficult to handle database exceptions and presenting those to the user.

The reason you get the detached exception is most likely because you think you have written something to the database, and then you call remove on or refresh on the object, and then you try to write something again.

What you need to do instead is create a long-running conversation with flushMode set to MANUAL. Then you start persisting stuff, and then you can perform your validation, and if that is ok you persist again. After you are done and everything is good to go, you call entityManager.flush(). Which will save everything to the database.

And if something failed, you dont flush. You just return null or "error" with some message. Let me show you with some pseudo code.

Lets say you have a Person and Organization entity. Now you need to store Person before you can put person to Organization.

private Person person;
private Organization org;

@Begin(join=true,FlushMode=MANUAL) //yes syntax is wrong, but you get the point
public String savePerson() {
//Inside some save method, and person contains some data that user has filled through a form

//Now you want to save person if they have name filled in (yes I know this example should be done from the view, but this is only an example
try {
  if("".equals(person.getName()) {
    StatusMessages.instance().add("User needs name");
    return "error"; //or null
  }
  entityManager.save(person);
  return "success";
} catch(Exception ex) {
  //handle error
  return "failure";
}
}

Note that we now save person, but we have not flushed the transaction. However, it will check constraints that you have set on your entitybean. (@NotNull, @NotEmpty and so on). So it will only simulate a save.

Now you save organization for person.

@End(FlushMode=MANUAL) //yes syntax is wrong, but you get the point
public String saveOrganization() {
//Inside some save method, and organization contains some data that user has filled through a form, or chosen from combobox

org.setPerson(person); //Yes this is only demonstration and should have been collection (OneToMany)
//Do some constraint or validation check
entityManager.save(org);
//Simulate saving org
//if everything went ok
entityManager.flush() //Now person and organization is finally stored in the database
return "success";
}

Here you can even put stuff in try catch and only return success if no exception occurred, so that you don't get thrown to error page.

Update

You can try this:

@PersistenceContext(type=EXTENDED)
EntityManager em;

This will make a stateful bean has an EJB3 extended persistence context. The messages retrieved in the query remain in the managed state as long as the bean exists, so any subsequent method calls to the stateful bean can update them without needing to make any explicit call to the EntityManager. This might avoid your LazyInitializationException. You can now use em.refresh(user);

like image 89
Shervin Asgari Avatar answered Nov 09 '22 23:11

Shervin Asgari