快速版本:我们正在寻找一种方法来强制事务回退,当在后备Bean上执行方法期间发生特定情况时,回退事务,但是我们希望进行回退而不必向用户显示通用的500错误页面。相反,我们希望用户看到她刚提交的表单以及一个FacesMessage来指示问题所在。
较长的版本:我们有一些支持bean,这些bean使用组件来执行数据库中的一些相关操作(使用JPA / Hibernate)。在此过程中,某些数据库操作发生后可能会发生错误。这可能是出于几种不同的原因,但是出于这个问题,我们假设在发生某些DB写入之后检测到验证错误,而在写入之前无法检测到该错误。发生这种情况时,我们希望确保到目前为止所有的数据库更改都将被回滚。Seam可以处理此问题,因为如果您从当前FacesRequest中抛出RuntimeException,Seam将回滚当前事务。
问题是向用户显示了一个通用错误页面。在我们的案例中,我们实际上希望向用户显示她所在的页面,其中包含关于出了什么问题的描述性消息,并有机会纠正导致问题的错误输入。我们想出的解决方案是从组件中抛出异常,该异常会发现带有注释的验证问题:
@ApplicationException( rollback = true )
然后,我们的后备bean可以捕获此异常,并假设抛出该异常的组件已经发布了适当的FacesMessage,并且只需返回null即可将用户带回显示错误的输入页面。ApplicationException注释告诉Seam回滚事务,我们没有向用户显示通用错误页面。
这在我们第一次使用它时恰好只是在做插入操作的地方效果很好。我们尝试使用它的第二个地方,我们必须在此过程中删除某些内容。在第二种情况下,如果没有验证错误,则一切正常。如果确实发生验证错误,则会引发rollback Exception并将该事务标记为回滚。即使没有任何数据库修改被回滚,当用户修复错误的数据并重新提交页面时,我们也会得到:
java.lang.IllegalArgumentException: Removing a detached instance
分离的实例是从另一个对象延迟加载的(存在多对一关系)。实例化后备bean时将加载该父对象。由于验证错误后事务已回滚,因此该对象现在已分离。
我们的下一步是将该页面从对话范围更改为页面范围。当我们这样做时,Seam甚至无法在验证错误后呈现页面,因为我们的页面必须命中数据库才能呈现,并且该事务已被标记为可回滚。
所以我的问题是:其他人如何干净地处理错误并同时适当地管理事务?更好的是,如果有人可以发现我做错的事情相对容易修复,那么我希望能够使用我们现在拥有的一切。
我已经阅读了统一错误页面和异常处理上的Seam Framework文章,但这更适合您的应用程序可能遇到的更常见的错误。
更新 :这是一些伪代码和页面流的详细信息。
在这种情况下,假设我们正在编辑某些用户的信息(在这种情况下,我们实际上并未与用户打交道,但我不会发布实际的代码)。
编辑功能的edit.page.xml文件包含一个用于RESTful URL的简单重写模式和两个导航规则:
edit.xhtml非常基本,其中包含可以编辑的用户所有部分的字段。
支持bean具有以下注释:
@Name( "editUser" ) @Scope( ScopeType.PAGE )
有一些注入的组件,例如User:
@In @Out( scope = ScopeType.CONVERSATION ) // outjected so the view page knows what to display protected User user;
我们在支持bean上有一个save方法,该方法将工作委派给用户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类似。
现在(您将不得不接受这一飞跃,因为这不一定听起来像是此User场景的问题,但这是针对我们正在做的事情)假定更改组织会导致ModifyUser失败,验证,但无法提前验证此失败。我们已经在当前的txn中将组织更新写入了db,但是由于用户修改未能通过验证,因此GuaranteedRollbackException将标记事务回滚。通过此实现,当我们再次呈现编辑页面以显示验证器添加的错误消息时,我们将无法在当前范围内使用数据库。渲染时,我们点击db以使某些内容显示在页面上,这是不可能的,因为Session无效:
由org.hibernate.LazyInitializationException引起,并带有消息:“无法初始化代理-没有会话”
我必须同意@duffymo关于在启动事务之前进行验证的信息。处理数据库异常并将其呈现给用户非常困难。
出现分离异常的原因很可能是因为您认为已向数据库中写入了某些内容,然后调用了对象的remove或refresh,然后尝试再次编写一些内容。
你需要做的,而不是什么是创建一个long-running conversation与flushMode设置为MANUAL。然后,您开始保存内容,然后可以执行验证,如果可以,则再次保存。完成后,一切顺利,您致电entityManager.flush()。它将所有内容保存到数据库。
long-running conversation
flushMode
MANUAL
entityManager.flush()
如果发生故障,则不要冲洗。您只是return null或"error"带有一些信息。让我向您展示一些伪代码。
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并且只有在没有发生异常的情况下才返回成功,这样您就不会被抛出错误页面。
try catch
更新资料
您可以尝试以下方法:
@PersistenceContext(type=EXTENDED) EntityManager em;
这将使有状态Bean具有EJB3扩展的持久性上下文。只要bean存在,在查询中检索到的消息就保持受管状态,因此对有状态bean的任何后续方法调用都可以更新它们,而无需对EntityManager进行任何显式调用。这可以避免您的LazyInitializationException。您现在可以使用 em.refresh(user);
em.refresh(user);