在Seam中强制事务回滚验证错误 [英] Forcing a transaction to rollback on validation errors in Seam

查看:139
本文介绍了在Seam中强制事务回滚验证错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

快速版本:
我们正在寻找一种方法来强制事务在回滚bean上的方法执行期间发生特定情况时回滚,但我们希望回滚的发生无需显示用户是一个通用的500错误页面。相反,我们希望用户看到她刚刚提交的表单以及表明问题出在哪里的FacesMessage。



长版本:
我们已经有几个支持bean使用组件在数据库中执行一些相关的操作(使用JPA / Hibernate)。在此过程中,某些数据库操作发生后可能会发生错误。这可能是由于几个不同的原因,但是对于这个问题,我们假设有一些验证错误在一些数据库写入发生之后发生,在写入发生之前不可检测到。发生这种情况时,我们希望确保所有到此为止的数据库变更都会回滚。 Seam可以处理这个问题,因为如果你从当前的FacesRequest中抛出一个RuntimeException,Seam将会回滚当前的事务。

这个问题是用户被显示通用错误页面。在我们的案例中,我们实际上希望用户能够看到她所在的页面,并提供关于错误的描述性信息,并有机会纠正导致问题的错误输入。我们提出的解决方案是从发现验证问题的组件中引发一个异常:

  @ ApplicationException(rollback = true)

然后我们的backing bean可以捕获这个异常,假设抛出它的组件已经发布了相应的FacesMessage,并且简单地返回null以使用户回到输入页面并显示错误。 ApplicationException注释告诉Seam回滚事务,并且不会向用户显示一个通用的错误页面。



这对我们使用它的第一个地方很有效只做插入。我们尝试使用它的第二个地方是,我们必须在这个过程中删除一些东西。在这第二种情况下,如果没有验证错误,一切都有效。如果发生验证错误,则会抛出回滚异常并将事务标记为回滚。即使没有数据库修改发生回滚,当用户修复不良数据并重新提交页面时,我们也会收到:

  java.lang.IllegalArgumentException:删除分离的实例

分离的实例被延迟加载从另一个对象(有多对一的关系)。该父对象在实例化辅助bean时加载。因为事务在验证错误后回滚,所以对象现在被分离。



下一步是将此页面从对话范围更改为页面范围。当我们这样做时,Seam甚至无法在验证错误后呈现页面,因为我们的页面必须击中数据库才能呈现,并且事务已标记为回滚。



所以我的问题是:其他人如何干净利落地处理错误并同时妥善管理交易?更好的是,我希望能够使用我们现在拥有的所有东西,如果有人能够发现我做错的事情,那将会相对容易解决。



我已阅读关于统一错误页面和异常处理的Seam框架文章,但这更适合更通用的



更新:以下是一些psudo代码和页面流程的详细信息。



在这种情况下,假设我们正在编辑某些用户的信息(在这种情况下,我们实际上并未处理用户,但我不会发布实际的代码)。



编辑功能的edit.page.xml文件包含RESTful URL和两个导航规则的简单重写模式:


  1. 如果结果是成功编辑,请将用户重定向到相应的视图页面以查看更新的信息。
  2. 如果用户点击取消按钮,将用户重定向到相应的视图页面。

edit.xhtml是非常基本的,可以编辑用户的所有部分的字段。



支持bean具有以下注释:

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

有一些注入的组件像User:

  @In 
@Out(scope = ScopeType.CONVERSATION)//注销以便视图页知道要显示的内容
受保护的用户用户;

我们在辅助bean上有一个保存方法,用于保存用户的工作:

  public String save()
{
try
{
userManager.modifyUser(user ,newFName,newLName,newType,newOrgName);

catch(GuaranteedRollbackException grbe)
{
log.debug(修改用户时得到GuaranteedRollbackException。);
返回null;
}

return USER_EDITED;
}

我们的GuaranteedRollbackException看起来像:

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


UserManager.modifyUser看起来像这样:

  public void modifyUser(User user,String newFName,String newLName,String type,String newOrgName)
{
/ /更改用户 - 组织关系
modifyUser.modifyOrg(user,newOrgName);

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

ModifyUser.modifyOrg类似于

  public void modifyOrg(User user,String newOrgName)
{
if(!userValidator.validateUserOrg(user,newOrgName))
{
//也许组织不存在某些东西。我们不在意,验证器
//会为我们添加适当的错误消息
抛出新的GauaranteedRollbackException(无法验证org);
}

//在这里做些东西来改变用户的东西
...
}

ModifyUser.modifyUser类似于modifyOrg。



现在(你将不得不采取这个飞跃因为它不一定听起来像是这个用户场景中的问题,但它是针对我们正在做的事情)假设更改组织会导致modifyUser无法验证,但是无法提前验证此失败。我们已经在当前的txn中将db的组织更新写入了db,但由于用户修改无法验证,GuaranteedRollbackException将标记要回退的事务。在这个实现中,当我们再次渲染编辑页面来显示验证器添加的错误消息时,我们无法在当前范围内使用数据库。在渲染过程中,我们点击数据库以在页面上显示某些内容,并且由于会话无效而无法执行:



由org.hibernate.LazyInitializationException引发消息:无法初始化代理 - 没有会话

解决方案

我必须同意@duffymo在事务启动之前进行验证。处理数据库异常并将其呈现给用户是非常困难的。

您得到分离异常的原因很可能是因为您认为您已将某些内容写入数据库,然后调用remove或refresh对象,然后您可以尝试重新写一些东西。



您需要做的是创建一个长时间运行的会话 flushMode 设置为 MANUAL
然后你开始坚持东西,然后你可以执行你的验证,如果那是好的,你再坚持。完成之后,一切都很顺利,您可以调用 entityManager.flush()。这会将所有内容保存到数据库中。



如果出现问题,请不要刷新。您只需返回null error并附带一些消息。让我告诉你一些伪代码。



假设你有一个Person和Organization实体。
现在您需要先存储人员,然后才能将人员转到组织。

 私人人物; 
private Organization org;

@Begin(join = true,FlushMode = MANUAL)//是语法错误,但你得到了点
public String savePerson(){
// Inside some save方法和人员包含用户通过表格填写的一些数据

//现在,如果想要保存人员姓名(如果他们有姓名填写)(是的,我知道该示例应该从视图中完成,但是这只是一个例子
尝试{
if(。equals(person.getName()){
StatusMessages.instance()。add(User needs name);
returnerror; // or null
}
entityManager.save(person);
returnsuccess;
} catch(Exception ex){
//处理错误
返回失败;
}
}

请注意,我们现在保存人员,但是我们没有刷新事务,但它会检查您在entitybean上设置的约束(@NotNull,@NotEmpty等),因此它只会模拟保存。



现在您可以节省组织人员的费用。

  @End(FlushMode = MANUAL)//是语法错误,但是您得到了点
public String saveOrganization(){
//里面有一些保存方法,并且组织包含一些用户通过表单填写的数据,或者从组合框

org.setPerson(person)中选择的数据; //是的,这只是演示,应该是集合(OneToMany)
//做一些约束或验证检查
entityManager.save(org);
//模拟保存org
//如果一切正常
entityManager.flush()//现在人员和组织最终存储在数据库中
返回success;

$ / code $ / pre

在这里你甚至可以把东西放在 try catch ,并且只有在没有发生异常情况下才会返回成功,这样您就不会被抛到错误页面。



更新

您可以试试这个:

  @PersistenceContext(type = EXTENDED )
EntityManager em;

这将使有状态Bean具有EJB3扩​​展持久性上下文。只要bean存在,查询中检索到的消息就保持在受管理状态,因此任何对有状态Bean的方法调用都可以更新它们,而无需对EntityManager进行任何显式调用。这可能会避免您的LazyInitializationException。
您现在可以使用
em.refresh(user);


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"

解决方案

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

这篇关于在Seam中强制事务回滚验证错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆