Spring 使用 JPA 事务处理批处理错误

Spring batch error handling with JPA transaction

我有一个 spring 批处理作业以我想要的方式在快乐路径场景中工作,但现在我专注于错误处理。

我的目标是跳过一组已知错误,并在任何其他异常(如数据库错误或外部 API 错误)上使作业失败。我稍后会重新开始工作。为此,我将步骤配置创建为

.faultTolerant()
    .skip(SkippableException.class)
    .skip(FlatFileParseException.class)
    .skipLimit(Integer.MAX_VALUE)
    .retryLimit(0)

在集成测试中,我已经证明如果我的 reader/processor/writer.

中抛出可跳过的异常,该作业将适当地跳过错误记录

虽然在另一个测试中,我想证明一个不可预见的数据库错误会导致作业失败。为此,我创建了一个触发器,它会导致对我要插入的 table 的插入失败。

这似乎有效,在我的编写器执行后事务提交期间抛出异常,并且我收到以下日志消息:

    2019-11-14 16:12:15.183 ERROR 88508 --- [           main] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [org.hibernate.exception.GenericJDBCException: could not execute statement]
    2019-11-14 16:12:15.184  INFO 88508 --- [           main] o.s.batch.core.step.tasklet.TaskletStep  : Commit failed while step execution data was already updated. Reverting to old version.

这似乎也是预期的行为。问题是,这不会停止工作。该步骤退出到 SimplyRetryExceptionHandler,它似乎认为异常不是致命的。它 "retries" 块并将其标记为成功并继续作为成功完成。

如果我通过在我自己的逻辑中引起数据库错误来强制在 reader/processor/writer 中抛出此异常,则作业成功..失败。

我需要做些什么来处理作为事务提交一部分发生的异常的 retry/skip 逻辑吗?

更新:我可以确认我的 skip/retry 设置是正确的,因为如果我注入我的 EntityManager 并在我的 writer 中调用 flush(),作业就会正确地失败。但我绝对不想那样做。

更新 2:再看一遍,框架提供的 JpaItemWriter 实现似乎在 write() 方法的末尾调用 entityManager.flush...所以我也可以这样做。

我自己的 reader/processor/writer 代码中没有抛出异常,这似乎是一个问题,因为 sql 语句在事务尝试提交之前不会执行。

也可以在 spring 批处理 JpaItemWriter 中看到的解决方法是将我的 EntityManager 注入我的编写器并手动调用 flush()。这会强制从我的代码中抛出任何 sql 异常,并且我的容错步骤会按预期处理它们。

我测试了@Tyler Helmuth 提出的方法,它有效。 我创建了一个通用编写器(我正在使用 spring 数据 JPA 存储库和 spring 批处理),它在父级的 write 方法之后调用 flush。 作者源码如下:

public class GenericRepositoryItemWriter<T> extends RepositoryItemWriter<T> {

    @PersistenceContext
    EntityManager entityManager;

    public void write(List<? extends T> items) throws Exception{
        super.write(items);
        entityManager.flush();
    }
}

我的writer是这样定义的:

@Bean(name = "joueurWriter")
    @StepScope
    public RepositoryItemWriter<Joueur> joueurWriter() throws Exception {
        RepositoryItemWriter<Joueur> writer = new GenericRepositoryItemWriter<>();
        writer.setRepository(joueurRepository);
        writer.setMethodName("save");
        return writer;
    }

我在我的 spring 批处理配置中与许多编写器有很多步骤,每次在编写器中我都会用正确的实体实例化 GenericRepositoryItemWriter。