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。
我有一个 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。