【问题标题】:How does Spring Batch CompositeItemWriter manage transaction for delegate writers?Spring Batch CompositeItemWriter 如何为委托编写者管理事务?
【发布时间】:2019-01-25 00:49:08
【问题描述】:

在批处理作业步骤配置中,我计划在writer中执行2个查询,第一个查询是更新表A中的记录,然后第二个查询是再次插入表A中的新记录。

目前我认为CompositeItemWriter可以实现我上面的目标,即我需要创建2个JdbcBatchItemWriters,一个用于更新,一个用于插入。

我的第一个问题是 CompositeItemWriter 是否适合上述要求?

如果是,则引出关于交易的第二个问题。例如,如果第一次更新成功,第二次插入失败。第一次更新事务会自动回滚吗?否则,如何在同一个事务中手动拉取两个更新?

提前致谢!

【问题讨论】:

  • 据我了解,Spring Batch 框架为您管理事务:读取器/处理器/写入器步骤始终在事务中,配置参数控制框架将多少项作为同一事务的一部分事务,在它提交并开始一个新事务以继续工作之前。

标签: spring-batch compositeitemwriter


【解决方案1】:

我的第一个问题是 CompositeItemWriter 是否适合上述要求?

是的,CompositeItemWriter 是要走的路。

如果是,则引出关于交易的第二个问题。例如,如果第一次更新成功,第二次插入失败。第一次更新事务会自动回滚吗?否则,如何在同一个事务中手动拉取两个更新?

很好的问题!是的,如果在第一个写入器中更新成功,然后在第二个写入器中插入失败,则所有语句将自动回滚。您需要知道的是事务围绕面向块的 tasklet 步骤的执行(以及围绕复合项目编写器的 write 方法)。因此,此方法中所有 sql 语句的执行(在委托编写器中执行)将是原子的。

为了说明这个用例,我编写了以下测试:

  • 给定一个表people,其中包含两列idname,其中只有一条记录:1,'foo'
  • 让我们想象一个作业读取两条记录(1,'foo'2,'bar')并尝试将foo 更新为foo!!,然后在表中插入2,'bar'。这是通过 CompositeItemWriter 和两个项目编写者完成的:UpdateItemWriterInsertItemWriter
  • 用例是UpdateItemWriter成功但InsertItemWriter失败(通过抛出异常)
  • 预期的结果是foo没有更新为foo!!并且bar没有插入表中(由于InsertItemWriter中的异常,两条sql语句都回滚了)

这是代码(它是自包含的,因此您可以尝试一下,看看它是如何工作的,它使用应该在您的类路径中的嵌入式 hsqldb 数据库):

import java.util.Arrays;
import java.util.List;
import javax.sql.DataSource;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.CompositeItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.jdbc.JdbcTestUtils;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TransactionWithCompositeWriterTest.JobConfiguration.class)
public class TransactionWithCompositeWriterTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Before
    public void setUp() {
        jdbcTemplate.update("CREATE TABLE people (id INT IDENTITY NOT NULL PRIMARY KEY, name VARCHAR(20));");
        jdbcTemplate.update("INSERT INTO people (id, name) VALUES (1, 'foo');");
    }

    @Test
    public void testTransactionRollbackWithCompositeWriter() throws Exception {
        // given
        int peopleCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "people");
        int fooCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 1 and name = 'foo'");
        int barCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 2 and name = 'bar'");
        Assert.assertEquals(1, peopleCount);
        Assert.assertEquals(1, fooCount);
        Assert.assertEquals(0, barCount);

        // when
        JobExecution jobExecution = jobLauncherTestUtils.launchJob();

        // then
        Assert.assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode());
        Assert.assertEquals("Something went wrong!", jobExecution.getAllFailureExceptions().get(0).getMessage());
        StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
        Assert.assertEquals(0, stepExecution.getCommitCount());
        Assert.assertEquals(1, stepExecution.getRollbackCount());
        Assert.assertEquals(0, stepExecution.getWriteCount());

        peopleCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "people");
        fooCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 1 and name = 'foo'");
        barCount = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "people", "id = 2 and name = 'bar'");
        Assert.assertEquals(1, peopleCount); // bar is not inserted
        Assert.assertEquals(0, barCount); // bar is not inserted
        Assert.assertEquals(1, fooCount); // foo is not updated to "foo!!"
    }

    @Configuration
    @EnableBatchProcessing
    public static class JobConfiguration {

        @Bean
        public DataSource dataSource() {
            return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.HSQL)
                    .addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
                    .addScript("/org/springframework/batch/core/schema-hsqldb.sql")
                    .build();
        }

        @Bean
        public JdbcTemplate jdbcTemplate(DataSource dataSource) {
            return new JdbcTemplate(dataSource);
        }

        @Bean
        public ItemReader<Person> itemReader() {
            Person foo = new Person(1, "foo");
            Person bar = new Person(2, "bar");
            return new ListItemReader<>(Arrays.asList(foo, bar));
        }

        @Bean
        public ItemWriter<Person> updateItemWriter() {
            return new UpdateItemWriter(dataSource());
        }

        @Bean
        public ItemWriter<Person> insertItemWriter() {
            return new InsertItemWriter(dataSource());
        }

        @Bean
        public ItemWriter<Person> itemWriter() {
            CompositeItemWriter<Person> compositeItemWriter = new CompositeItemWriter<>();
            compositeItemWriter.setDelegates(Arrays.asList(updateItemWriter(), insertItemWriter()));
            return compositeItemWriter;
        }

        @Bean
        public Job job(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
            return jobBuilderFactory.get("job")
                    .start(stepBuilderFactory
                            .get("step").<Person, Person>chunk(2)
                            .reader(itemReader())
                            .writer(itemWriter())
                            .build())
                    .build();
        }

        @Bean
        public JobLauncherTestUtils jobLauncherTestUtils() {
            return new JobLauncherTestUtils();
        }
    }

    public static class UpdateItemWriter implements ItemWriter<Person> {

        private JdbcTemplate jdbcTemplate;

        public UpdateItemWriter(DataSource dataSource) {
            this.jdbcTemplate = new JdbcTemplate(dataSource);
        }

        @Override
        public void write(List<? extends Person> items) {
            for (Person person : items) {
                if ("foo".equalsIgnoreCase(person.getName())) {
                    jdbcTemplate.update("UPDATE people SET name = 'foo!!' WHERE id = 1");
                }
            }
        }
    }

    public static class InsertItemWriter implements ItemWriter<Person> {

        private JdbcTemplate jdbcTemplate;

        public InsertItemWriter(DataSource dataSource) {
            this.jdbcTemplate = new JdbcTemplate(dataSource);
        }

        @Override
        public void write(List<? extends Person> items) {
            for (Person person : items) {
                if ("bar".equalsIgnoreCase(person.getName())) {
                    jdbcTemplate.update("INSERT INTO people (id, name) VALUES (?, ?)", person.getId(), person.getName());
                    throw new IllegalStateException("Something went wrong!");
                }
            }
        }
    }

    public static class Person {

        private long id;

        private String name;

        public Person() {
        }

        public Person(long id, String name) {
            this.id = id;
            this.name = name;
        }

        public long getId() {
            return id;
        }

        public void setId(long id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

我的示例使用自定义项目编写器,但这也适用于两个 JdbcBatchItemWriters。

我希望这会有所帮助!

【讨论】:

  • 我试过你的例子。 UpdateItemWriter 成功,而 InsertItemWriter 失败。但是,第一条语句中的值仍会更新。有什么不对吗?我使用 Spring Batch 4.2.1 和 Oracle 作为数据源。还有一个问题是,即使我的第一次更新失败,我的工作也可以插入吗?如何? @Mahmoud Ben Hassine
  • 刚刚用oracle DB测试过,同样的测试通过了。 1 more question is that could my job do Insert even when my 1st Update is fail? 不,是这样的想法,事务围绕复合编写器:所以无论哪个委托失败,事务都将为所有委托回滚。要么全有,要么全无。
  • @MahmoudBenHassine - 如果我们能以某种方式让第一个交易成功,即使第二个作家更新失败,有什么办法吗?如果第二个写入器更新失败,那么我们应该保存正确的数据并将垃圾数据保存到错误表中?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-01
  • 2015-05-31
  • 1970-01-01
  • 2022-12-04
  • 2021-02-07
  • 2016-06-02
相关资源
最近更新 更多