【问题标题】:Forcing a transaction to rollback on validation errors in Seam强制事务回滚 Seam 中的验证错误
【发布时间】:2010-05-12 21:01:59
【问题描述】:

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

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

这样做的问题是向用户显示了一个通用错误页面。在我们的案例中,我们实际上希望向用户显示她所在的页面,其中包含有关问题的描述性消息,并有机会更正导致问题的错误输入。我们提出的解决方案是从发现注释验证问题的组件中抛出一个异常:

@ApplicationException( rollback = true )

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

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

java.lang.IllegalArgumentException: Removing a detached instance 

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

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

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

我已经阅读了 Unified error page and exception handling 上的 Seam 框架文章,但这更适合您的应用程序可能遇到的更一般的错误。

更新:这里是一些伪代码和页面流程的详细信息。

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

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

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

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

backing 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 方法,它为用户 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验证失败,但无法提前验证此失败。我们已经将组织更新写入当前 txn 中的数据库,但由于用户修改无法验证,因此 GuaranteedRollbackException 会将事务标记为回滚。使用此实现,当我们再次渲染编辑页面以显示验证器添加的错误消息时,我们无法在当前范围内使用数据库。在渲染时,我们点击 db 以在页面上显示一些内容,但这是不可能的,因为 Session 无效:

由 org.hibernate.LazyInitializationException 引起,消息为:“无法初始化代理 - 无会话”

【问题讨论】:

  • 我可以帮你,但是你能用纯代码显示吗 第一个场景和第二个场景:组件、你的页面、导航规则和异常处理程序
  • @Arthur Ronald F D Garcia,我添加了更多信息以及一些代码片段。这需要太多代码来发布整个内容,但希望这能让您了解您需要了解的内容。

标签: java hibernate jpa transactions seam


【解决方案1】:

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

你得到分离异常的原因很可能是因为你认为你已经写了一些东西到数据库中,然后你在对象上调用 remove 或 refresh,然后你又尝试写一些东西。

您需要做的是创建一个long-running conversation,并将flushMode 设置为MANUAL。 然后你开始持久化东西,然后你可以执行你的验证,如果没问题,你再坚持一次。完成后一切顺利,您致电entityManager.flush()。这会将所有内容保存到数据库中。

如果发生故障,您不会冲洗。您只需 return null"error" 提供一些信息。让我给你看一些伪代码。

假设您有一个 Person 和 Organization 实体。 现在您需要先存储 Person,然后才能将 person 放入 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";
}
}

请注意,我们现在保存人员,但我们还没有刷新事务。但是,它将检查您在 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 中,只有在没有发生异常的情况下才返回成功,这样你就不会被抛出到错误页面。

更新

你可以试试这个:

@PersistenceContext(type=EXTENDED)
EntityManager em;

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

【讨论】:

  • 感谢您的回复。我们正在考虑朝这个方向发展,但使用 @ApplicationException 注释似乎更简洁——您只需要抛出异常并在支持 bean 中捕获它。我添加了一些代码和更多关于我们在上面尝试做的事情的解释。我们基本上是想弄清楚其他人是如何处理这个问题的。能够首先验证的简单案例很容易。相信我,我们很乐意在接触数据库之前进行验证,但这在我们的案例中并不实用。
  • 我们最终采用了与此解决方案类似的解决方案(更新前的内容)。感谢您的建议。
  • Manual Flush 的问题在于,它只有在您不必进行任何数据库查询时才有效。如果您将 ajax 与一些需要数据库查询的建议一起使用,或者您进行涉及数据库查询的验证,则它不起作用,因为当您发出数据库查询时,Hibernate 会自动调用刷新。在这种情况下,您需要回滚事务。我和 Chris 有同样的问题,当我抛出 ApplicationException(rollback=true) 并重定向到另一个页面时,我得到一个带有“无法初始化代理”的 LIE。有趣的是,这不会发生在我重定向到的所有页面上。
  • 这实际上是 seam 最令人讨厌的事情之一 :) 如果您仅仅因为 ajax 请求而遇到问题,您可以尝试在处理这些 ajax 请求的组件中使用专用的实体管理器。这可以解决这些问题,因为编辑实体的实体管理器不会被触及。
【解决方案2】:

我认为验证应该在交易开始之前完成。

【讨论】:

  • 这是一种易于实现的乐观模式。当您遇到更复杂的情况时会发生什么:您可能会写入数据库,这可能会导致步骤 2 中出现验证错误,而您在步骤 1 完成之前无法验证?此外,当您尝试将与任务 1 相关的所有代码合并到一个地方并在另一个地方合并与任务 2 相关的所有代码时,当与 1 和 2 相关的验证被拉出并放在另一个地方时,事情变得更难阅读和维护。当您必须编写特殊逻辑使其像任务 1 完成一样时,验证任务 2 也会变得复杂。
  • @Chris 我添加了一些更新。您可以尝试使用扩展的持久性上下文。它可能会帮助你
【解决方案3】:

我最近以各种形式面临这种情况。

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

更新类似:获取新副本并进行更新。

这有点麻烦,但它避免了长事务以及与之相关的所有邪恶锁定问题。

【讨论】:

    猜你喜欢
    • 2012-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 2015-11-09
    • 1970-01-01
    • 1970-01-01
    • 2010-12-17
    相关资源
    最近更新 更多