我应该将托管实体传递给需要新事务的方法吗?

时间:2016-04-06 13:33:37

标签: java hibernate jpa spring-data spring-data-jpa

我的应用程序加载了应该处理的实体列表。这发生在使用调度程序

的类中
@Component
class TaskScheduler {

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private HandlingService handlingService;

    @Scheduled(fixedRate = 15000)
    @Transactional
    public void triggerTransactionStatusChangeHandling() {
        taskRepository.findByStatus(Status.OPEN).stream()
                               .forEach(handlingService::handle);
    }
}

在我的HandlingService处理使用REQUIRES_NEW进行传播级别的日照中的每项任务。

@Component
class HandlingService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(Task task) {
        try {
            processTask(task); // here the actual processing would take place
            task.setStatus(Status.PROCCESED);
        } catch (RuntimeException e) {
            task.setStatus(Status.ERROR);
        }
    }
}

代码的工作原理只是因为我在TaskScheduler类上启动了父事务。如果我删除了@Transactional注释,则不再管理实体,并且对任务实体的更新不会传播到db。我发现将调度方法设置为事务性并不自然。

从我看到我有两个选择:

1。保持现在的代码。

  • 也许只是我,这是一个正确的方法。
  • 这种变体对数据库的访问次数最少。

2。从调度程序中删除@Transactional注释,传递任务的ID并在HandlingService中重新加载任务实体。

@Component
class HandlingService {

    @Autowired
    private TaskRepository taskRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(Long taskId) {
        Task task = taskRepository.findOne(taskId);
        try {
            processTask(task); // here the actual processing would take place
            task.setStatus(Status.PROCCESED);
        } catch (RuntimeException e) {
            task.setStatus(Status.ERROR);
        }
    }
}
  • 有更多的数据库访问(一个额外的查询/元素)
  • 可以使用@Async
  • 执行

您能否提出您对解决此类问题的正确方法的看法,或许还有其他我不了解的方法?

3 个答案:

答案 0 :(得分:10)

如果您打算在单独的事务中处理每个任务,那么您的第一个方法实际上不起作用,因为一切都是在调度程序事务结束时提交的。

原因在于嵌套事务Task实例基本上是分离的实体(嵌套事务中启动的Session不知道那些实例)。在调度程序事务结束时,Hibernate会对托管实例执行脏检查,并将更改与数据库同步。

这种方法也非常危险,因为如果您尝试在嵌套事务中的Task实例上访问未初始化的代理,则可能会出现问题。如果通过在嵌套事务中添加一些其他实体实例来更改嵌套事务中的Task对象图,则可能会出现问题(因为当控件返回到调度程序事务时,该实例现在将被分离)

另一方面,你的第二种方法是正确和直接的,有助于避免上述所有陷阱。只是,我会读取ID并提交事务(在处理任务时不需要暂停它)。实现它的最简单方法是从调度程序中删除Transactional注释,并使存储库方法具有事务性(如果它不是事务性的)。

如果(并且仅当)第二种方法的性能是一个问题,正如您已经提到的那样,您可以使用异步处理,甚至可以在某种程度上并行化处理。此外,您可能需要查看extended sessions(对话),也许您会发现它适合您的用例。

答案 1 :(得分:4)

当前代码处理嵌套事务中的任务,但更新外部事务中任务的状态(因为Task对象由外部事务管理)。因为这些是不同的事务,所以有可能一个成功而另一个失败,使数据库处于不一致状态。特别是,使用此代码,如果处理另一个任务抛出异常,或者在处理完所有任务之前重新启动服务器,则已完成的任务将保持打开状态。

正如您的示例所示,将托管实体传递给另一个事务会使哪些事务更新这些实体变得模糊,因此最好避免使用。相反,您应该传递id(或分离的实体),并避免不必要的事务嵌套。

答案 2 :(得分:1)

假设processTask(task);HandlingService类中的方法(与handle(task)方法相同),那么删除@Transactional中的HandlingService将无效,因为Spring的动态代理的自然行为。

引自spring.io forum

  

当Spring加载你的TestService时,它用代理包装它。如果在TestService之外调用TestService的方法,则将调用代理,并且将正确管理您的事务。但是,如果在同一对象的方法中调用事务方法,则不会调用代理,而是直接调用代理的目标,并且不会执行包含在服务中的代码来管理事务。

This is one of SO thread关于这个主题,以下是一些关于此的文章:

  1. http://tutorials.jenkov.com/java-reflection/dynamic-proxies.html
  2. http://tutorials.jenkov.com/java-persistence/advanced-connection-and-transaction-demarcation-and-propagation.html
  3. http://blog.jhades.org/how-does-spring-transactional-really-work/
  4. 如果您真的不喜欢在@Transaction方法中添加@Scheduled注释,则可以从EntityManager获取事务并以编程方式管理事务,例如:

    UserTransaction tx = entityManager.getTransaction();
    try {
      processTask(task);
      task.setStatus(Status.PROCCESED);
      tx.commit(); 
    } catch (Exception e) {
      tx.rollback();
    }
    

    但我怀疑你会采取这种方式(嗯,我不会)。最后,

      

    您能否就解决此类问题的正确方法提出您的意见

    在这种情况下,没有正确的方法。我个人认为,注释(例如,@Transactional)只是一个标记,你需要一个注释处理器(在本例中是spring)来使@Transactional工作。没有处理器,注释将完全没有影响。

    我会更担心,例如,为什么processTask(task)task.setStatus(Status.PROCESSED);生活在processTask(task)之外,如果它看起来像是同样的事情等等。

    HTH。