强制事务回滚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的简单重写模式和两个导航规则:
- 如果结果成功编辑,则将用户重定向到相应的视图页面以查看更新的信息。
- 如果用户点击取消按钮,则将用户重定向到相应的视图页面。
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 conversation
, flushMode
设置为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(它不会进入数据库,因为它几乎肯定被缓存)找到它的“附加”双胞胎,然后删除返回的对象。
类似于更新:获取新副本并更新它。
这有点麻烦,但它避免了长期交易和所有与之相关的邪恶锁定问题。
- Hibernate中FlushMode.AUTO和FlushMode.ALWAYS之间的区别?
- 关联对象的Hibernate标准
- org.hibernate.MappingException:未知实体:
- 无法检索PlatformTransactionManager以进行测试上下文的@Transactional测试
- PSQLException:错误:关系“TABLE_NAME”不存在
- Hibernate无法提取ResultSetexception
- Hibernatevalidation – 仅validation对象是否为空
- Hibernate使用PostgreSQL序列不会影响序列表
- 没有活动事务,CreateQuery无效