Spring Batch Java 25 · Spring Boot 4.1.1

Spring Batch: Bulk Update with JDBC

Perform a bulk update operation efficiently against a relational database with Spring Batch and Spring JDBC.

Background

Spring Batch allows us to perform large volumes of data processing and updates. When dealing with database records, updating them one by one can be inefficient due to multiple round-trips to the database.

In this article, we will explore how to implement a batch operation that reads records using JdbcPagingItemReader, processes them, and performs a bulk update using JdbcTemplate and its batchUpdate method. We will retrieve a list of users, update their statuses to the next one, and save the modifications in a single operation.

Job Configuration

Next is to implement the job that will be responsible for reading users, modifying their status, and saving the updates back to the database. Job consists of Step and Step consists of ItemReader, ItemProcessor, and ItemWriter. We will implement all of them in UpdateUserStatusJobConfiguration.

@Configuration
class UpdateUserStatusJobConfiguration {

    @Bean
    public Job job(JobRepository jobs, Step step) {
        return new JobBuilder("updateUserStatusJob", jobs).start(step).build();
    }

    @Bean
    public Step step(ItemReader<User> reader, ItemProcessor<User, UserUpdate> processor, ItemWriter<UserUpdate> writer, JobRepository jobs) {
        return new StepBuilder("updateUserStatusStep", jobs)
                .<User, UserUpdate>chunk(10)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    public JdbcPagingItemReader<User> reader(DataSource dataSource) throws Exception {
        return new JdbcPagingItemReaderBuilder<User>()
                .name("updateUserStatusReader")
                .dataSource(dataSource)
                .selectClause("SELECT *")
                .fromClause("FROM users")
                .sortKeys(Map.of("username", Order.ASCENDING))
                .rowMapper((rs, _) -> new User(rs.getLong("id"), rs.getString("username"), User.Status.valueOf(rs.getString("status"))))
                .build();
    }

    @Bean
    public ItemProcessor<User, UserUpdate> processor() {

        return new ItemProcessor<User, UserUpdate>() {
            @Override
            public @Nullable UserUpdate process(User item) throws Exception {
                var nextStatus = User.Status.values()[item.status().ordinal() + 1];
                return new UserUpdate(item.id(), nextStatus);
            }
        };

    }

    @Bean
    public JdbcBatchItemWriter<UserUpdate> writer(DataSource dataSource) throws Exception {
        return new JdbcBatchItemWriterBuilder<UserUpdate>()
                .dataSource(dataSource)
                .sql("UPDATE users SET status = ? WHERE id = ?")
                .itemPreparedStatementSetter((user, ps) -> {
                    ps.setString(1, user.status.name());
                    ps.setLong(2, user.id);
                })
                .build();
    }

}

From the code above, we can see that JdbcPagingItemReader is used to retrieve User objects from the database in a paginated manner. Then, ItemProcessor modifies the statuses, mapping them to a custom UserUpdate object. Finally, we implement an ItemWriter that depends on JdbcTemplate to execute a single batchUpdate query to update all the UserUpdate records within a specific chunk, increasing our application’s performance.

Verification

We will launch updateUserStatusJob which will retrieve our users from a database, change their statuses, and update them back. Once completed, we will verify that the database contains the expected updated statuses for our users.

@Import(TestcontainersConfiguration.class)
@SpringBatchTest
@SpringBootTest(properties = "spring.batch.jdbc.initialize-schema=always")
@Sql(
        scripts = "classpath:schema-postgres.sql",
        statements = {
                "INSERT INTO users (username, status) VALUES ('rashidi.zin', 'ACTIVE'), ('zaid.zin', 'INACTIVE'), ('john.doe', 'DORMANT')"
        },
        executionPhase = BEFORE_TEST_CLASS)
class BatchJdbcBulkUpdateApplicationTests {

    @Autowired
    private JdbcTemplate jdbc;

    @Autowired
    private JobOperatorTestUtils jobs;

    @Test
    @DisplayName("When job is executed Then users' status will be updated to the next Status")
    void contextLoads() throws Exception {

        await().atMost(10L, SECONDS).untilAsserted(() -> {
            var execution = jobs.startJob();

            assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
        });

        var updatedStatuses = jdbc.queryForList("SELECT status FROM users", String.class);

        assertThat(updatedStatuses).containsOnly("INACTIVE", "DORMANT", "DELETED");
    }

}

Full implementation can be found in BatchJdbcBulkUpdateApplicationTests.