强制事务回滚Seam中的validation错误

快速版本:我们正在寻找一种方法来强制事务在特定情况下在支持bean上执行方法时发生回滚,但是我们希望在不向用户显示通用500错误页面的情况下进行回滚。 相反,我们希望用户看到她刚刚提交的表单和一个表明问题所在的FacesMessage。

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

这样做的问题是用户显示了一般错误页面。 在我们的例子中,我们实际上希望用户能够显示她所在的页面,并提供有关出错的描述性消息,并有机会纠正导致问题的错误输入。 我们提出的解决方案是从发现带有注释的validation问题的组件中抛出exception:

@ApplicationException( rollback = true ) 

然后我们的支持bean可以捕获此exception,假设抛出它的组件已发布相应的FacesMessage,并且只需返回null以将用户带回输入页面并显示错误。 ApplicationException注释告诉Seam回滚事务,我们没有向用户显示一般错误页面。

这在我们使用它的第一个地方运作良好,恰好只是插入。 我们尝试使用它的第二个地方,我们必须在此过程中删除一些东西。 在第二种情况下,如果没有validation错误,一切正常。 如果确实发生了validation错误,则抛出回滚exception并将事务标记为回滚。 即使没有发生回滚数据库修改,当用户修复错误数据并重新提交页面时,我们得到:

 java.lang.IllegalArgumentException: Removing a detached instance 

分离的实例从另一个对象延迟加载(存在多对一关系)。 在实例化辅助bean时加载该父对象。 由于事务在validation错误后回滚,因此该对象现在已分离。

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

所以我的问题是:其他人如何干净利落地处理错误并同时正确管理交易? 更好的是,我希望能够使用我们现在拥有的所有东西,如果有人能够发现我做错的东西,而且相对容易修复。

我已经阅读了有关统一错误页面和exception处理的Seam Framework文章,但这更倾向于应用程序可能遇到的更一般的错误。

更新 :这里有一些psudo代码和页面流的详细信息。

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

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

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

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

辅助bean具有以下注释:

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

有一些注入的组件,如用户:

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

我们在backing bean上有一个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; } 

我们的GuaranteedRollbackException如下所示:

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

UserManager.modifyUser看起来像这样:

 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做了类似的事情

 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类似于modifyOrg。

现在(你将不得不与我一起跳跃,因为它不一定听起来像这个用户场景的问题,但它是我们正在做的事情)假设更改组织导致modifyUser失败validation,但提前validation此故障是不可能的。 我们已经在当前的txn中将数据库更新写入了db,但由于用户修改无法validation,因此GuaranteedRollbackException将标记要回滚的事务。 通过此实现,当我们再次呈现编辑页面以显示validation器添加的错误消息时,我们无法在当前作用域中使用DB。 在渲染时,我们点击数据库以获取在页面上显示的内容,但这是不可能的,因为会话无效:

由org.hibernate.LazyInitializationException引起的消息:“无法初始化代理 – 没有会话”

我必须同意@duffymo关于在启动交易之前进行validation。 处理数据库exception并将其呈现给用户是相当困难的。

你得到分离exception的原因很可能是因为你认为你已经写了一些东西到数据库,然后你调用remove on或刷新对象,然后你再尝试写一些东西。

您需要做的是创建一个long-running conversationflushMode设置为MANUAL 。 然后你开始持久化,然后你可以执行validation,如果可以,你再次坚持。 完成后,一切都很好,你可以调用entityManager.flush() 。 这将保存数据库中的所有内容。

如果出现问题,你就不要冲洗了。 您只需使用某些消息return null"error" 。 让我给你看一些伪代码。

假设您有一个人员和组织实体。 现在,您需要在将人员安排到组织之前存储人员。

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

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

现在你为人保存组织。

 @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"; } 

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

更新

你可以试试这个:

 @PersistenceContext(type=EXTENDED) EntityManager em; 

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

我认为应该在事务发起之前完成validation。

我最近以各种forms面对这种情况。

我找到的最好的方法是将你拥有的对象视为一个值对象(它基本上是在回滚之后)。 要从数据库中删除它,使用find的id(它不会进入数据库,因为它几乎肯定被缓存)找到它的“附加”双胞胎,然后删除返回的对象。

类似于更新:获取新副本并更新它。

这有点麻烦,但它避免了长期交易和所有与之相关的邪恶锁定问题。