Skip to content
Published on

Spring Boot Batch完全ガイド:Job・Step・Chunk処理と本番パターン

Authors

1. Spring Batchアーキテクチャ

Spring Batchは大量データを処理するための軽量バッチフレームワークです。ETL(Extract, Transform, Load)、データマイグレーション、レポート生成、精算処理など、様々なバッチ処理に活用されています。

コアアーキテクチャコンポーネント

Job
└── Step 1
│   └── Chunk (ChunkSize: 100)
│       ├── ItemReader   (読み取り)
│       ├── ItemProcessor (変換/フィルター)
│       └── ItemWriter   (書き込み)
└── Step 2
    └── Tasklet (単一処理)

主要コンポーネント:

  • Job: バッチ処理の最上位単位。1つ以上のStepで構成
  • Step: Job内の実行単位。ChunkベースまたはTaskletベース
  • Chunk: 指定されたサイズのデータを読み取り、処理し、書き込む単位
  • ItemReader: データソースからアイテムを1件ずつ読み取るインターフェース
  • ItemProcessor: 読み取ったアイテムを変換またはフィルタリング
  • ItemWriter: 処理済みアイテムをストレージに書き込み

JobRepository、JobLauncher、JobExplorer

JobLauncher ──→ Job (実行リクエスト)
JobRepository (実行履歴の保存・取得)
JobExplorer (読み取り専用の履歴参照)

Spring Batchメタテーブル

-- 主要メタデータテーブル
BATCH_JOB_INSTANCE    -- Jobインスタンス情報
BATCH_JOB_EXECUTION   -- Job実行情報(ステータス、開始/終了時刻)
BATCH_JOB_PARAMS      -- Jobパラメータ
BATCH_STEP_EXECUTION  -- Step実行情報
BATCH_STEP_EXECUTION_CONTEXT -- Stepコンテキストデータ
BATCH_JOB_EXECUTION_CONTEXT  -- Jobコンテキストデータ

2. 依存関係の設定

Maven依存関係

<!-- Spring Boot Batch -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

<!-- バッチメタテーブル用DB(例:PostgreSQL) -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

<!-- テスト用 -->
<dependency>
    <groupId>org.springframework.batch</groupId>
    <artifactId>spring-batch-test</artifactId>
    <scope>test</scope>
</dependency>

application.yml設定

spring:
  batch:
    job:
      enabled: false # アプリケーション起動時の自動実行を防止
    jdbc:
      initialize-schema: always # メタテーブルの自動作成
  datasource:
    url: jdbc:postgresql://localhost:5432/batchdb
    username: batchuser
    password: batchpass

logging:
  level:
    org.springframework.batch: DEBUG

3. 基本的なJob設定

UserMigrationJobConfig - 全体設定例

@Configuration
@EnableBatchProcessing
public class UserMigrationJobConfig {

    // Job定義
    @Bean
    public Job userMigrationJob(JobRepository jobRepository,
                                 Step migrationStep) {
        return new JobBuilder("userMigrationJob", jobRepository)
                .start(migrationStep)
                .listener(jobExecutionListener())
                .build();
    }

    // Step定義(Chunkベース)
    @Bean
    public Step migrationStep(JobRepository jobRepository,
                               PlatformTransactionManager txManager,
                               ItemReader<User> userItemReader,
                               ItemProcessor<User, UserDto> userItemProcessor,
                               ItemWriter<UserDto> userItemWriter) {
        return new StepBuilder("migrationStep", jobRepository)
                .<User, UserDto>chunk(100, txManager)
                .reader(userItemReader)
                .processor(userItemProcessor)
                .writer(userItemWriter)
                .faultTolerant()
                .skipLimit(10)
                .skip(DataIntegrityViolationException.class)
                .retryLimit(3)
                .retry(TransientDataAccessException.class)
                .listener(stepExecutionListener())
                .build();
    }

    @Bean
    public JobExecutionListener jobExecutionListener() {
        return new JobExecutionListener() {
            @Override
            public void beforeJob(JobExecution jobExecution) {
                System.out.println("Job開始: "
                    + jobExecution.getJobInstance().getJobName());
            }

            @Override
            public void afterJob(JobExecution jobExecution) {
                System.out.printf("Job完了: %s, ステータス: %s%n",
                    jobExecution.getJobInstance().getJobName(),
                    jobExecution.getStatus());
            }
        };
    }

    @Bean
    public StepExecutionListener stepExecutionListener() {
        return new StepExecutionListener() {
            @Override
            public void beforeStep(StepExecution stepExecution) {
                System.out.println("Step開始: " + stepExecution.getStepName());
            }

            @Override
            public ExitStatus afterStep(StepExecution stepExecution) {
                System.out.printf("Step完了: 読み取り=%d, スキップ=%d, 書き込み=%d%n",
                    stepExecution.getReadCount(),
                    stepExecution.getProcessSkipCount(),
                    stepExecution.getWriteCount());
                return stepExecution.getExitStatus();
            }
        };
    }
}

TaskletベースのStep

@Bean
public Step cleanupStep(JobRepository jobRepository,
                         PlatformTransactionManager txManager) {
    return new StepBuilder("cleanupStep", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                // シンプルな一回限りの処理に適している
                System.out.println("一時ファイルを削除中...");
                // ファイル削除ロジック
                return RepeatStatus.FINISHED;
            }, txManager)
            .build();
}

4. ItemReaderの実装

JdbcCursorItemReader - 大量DBデータの読み取り

@Bean
@StepScope
public JdbcCursorItemReader<User> userCursorReader(DataSource dataSource) {
    return new JdbcCursorItemReaderBuilder<User>()
            .name("userCursorReader")
            .dataSource(dataSource)
            .sql("SELECT id, username, email, status FROM users WHERE status = 'ACTIVE' ORDER BY id")
            .rowMapper(new BeanPropertyRowMapper<>(User.class))
            .fetchSize(1000)
            .build();
}

JdbcPagingItemReader - ページ単位の読み取り

@Bean
@StepScope
public JdbcPagingItemReader<User> userPagingReader(DataSource dataSource) {
    Map<String, Order> sortKeys = new HashMap<>();
    sortKeys.put("id", Order.ASCENDING);

    PostgresPagingQueryProvider queryProvider = new PostgresPagingQueryProvider();
    queryProvider.setSelectClause("SELECT id, username, email, status");
    queryProvider.setFromClause("FROM users");
    queryProvider.setWhereClause("WHERE status = 'ACTIVE'");
    queryProvider.setSortKeys(sortKeys);

    return new JdbcPagingItemReaderBuilder<User>()
            .name("userPagingReader")
            .dataSource(dataSource)
            .queryProvider(queryProvider)
            .pageSize(100)
            .rowMapper(new BeanPropertyRowMapper<>(User.class))
            .build();
}

FlatFileItemReader - CSVファイルの読み取り

@Bean
@StepScope
public FlatFileItemReader<UserCsvDto> csvUserReader(
        @Value("#{jobParameters['inputFile']}") String inputFile) {

    return new FlatFileItemReaderBuilder<UserCsvDto>()
            .name("csvUserReader")
            .resource(new FileSystemResource(inputFile))
            .linesToSkip(1)  // ヘッダー行をスキップ
            .delimited()
            .delimiter(",")
            .names("id", "username", "email", "createdAt")
            .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{
                setTargetType(UserCsvDto.class);
            }})
            .build();
}

カスタムItemReaderの実装

@Component
@StepScope
public class ApiCallItemReader implements ItemReader<UserData> {

    private final UserApiClient apiClient;
    private int page = 0;
    private List<UserData> currentPageData = new ArrayList<>();
    private int currentIndex = 0;
    private boolean exhausted = false;

    public ApiCallItemReader(UserApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public UserData read() throws Exception {
        if (exhausted) return null;

        if (currentIndex >= currentPageData.size()) {
            currentPageData = apiClient.fetchUsers(page++, 100);
            currentIndex = 0;

            if (currentPageData.isEmpty()) {
                exhausted = true;
                return null;
            }
        }

        return currentPageData.get(currentIndex++);
    }
}

5. ItemProcessorの実装

データ変換とフィルタリング

@Component
@StepScope
public class UserMigrationProcessor implements ItemProcessor<User, UserDto> {

    private static final Logger log = LoggerFactory.getLogger(UserMigrationProcessor.class);

    @Override
    public UserDto process(User user) throws Exception {
        // nullを返すとそのアイテムはスキップされ、Writerに渡されない
        if (!isEligibleForMigration(user)) {
            log.debug("スキップするユーザー: {}", user.getUsername());
            return null;
        }

        UserDto dto = new UserDto();
        dto.setId(user.getId());
        dto.setUsername(user.getUsername().toLowerCase().trim());
        dto.setEmail(user.getEmail().toLowerCase());
        dto.setDisplayName(formatDisplayName(user.getFirstName(), user.getLastName()));
        dto.setMigratedAt(LocalDateTime.now());

        return dto;
    }

    private boolean isEligibleForMigration(User user) {
        return user.getStatus() != null
            && "ACTIVE".equals(user.getStatus())
            && user.getEmail() != null
            && user.getEmail().contains("@");
    }

    private String formatDisplayName(String firstName, String lastName) {
        return Stream.of(firstName, lastName)
                .filter(s -> s != null && !s.isBlank())
                .collect(Collectors.joining(" "));
    }
}

CompositeItemProcessor - 複数Processorのチェーン

@Bean
public CompositeItemProcessor<User, UserDto> compositeProcessor() {
    List<ItemProcessor<?, ?>> processors = new ArrayList<>();
    processors.add(new ValidationProcessor());
    processors.add(new EnrichmentProcessor(externalService));
    processors.add(new TransformationProcessor());

    CompositeItemProcessor<User, UserDto> composite = new CompositeItemProcessor<>();
    composite.setDelegates(processors);
    return composite;
}

6. ItemWriterの実装

JdbcBatchItemWriter - 一括INSERT/UPDATE

@Bean
public JdbcBatchItemWriter<UserDto> userDtoWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<UserDto>()
            .dataSource(dataSource)
            .sql("""
                INSERT INTO users_new (id, username, email, display_name, migrated_at)
                VALUES (:id, :username, :email, :displayName, :migratedAt)
                ON CONFLICT (id) DO UPDATE
                SET username = EXCLUDED.username,
                    email = EXCLUDED.email,
                    migrated_at = EXCLUDED.migrated_at
                """)
            .beanMapped()
            .build();
}

JpaItemWriter

@Bean
public JpaItemWriter<UserDto> jpaUserWriter(EntityManagerFactory entityManagerFactory) {
    JpaItemWriter<UserDto> writer = new JpaItemWriter<>();
    writer.setEntityManagerFactory(entityManagerFactory);
    return writer;
}

FlatFileItemWriter - 結果ファイルへの出力

@Bean
@StepScope
public FlatFileItemWriter<UserDto> csvResultWriter(
        @Value("#{jobParameters['outputFile']}") String outputFile) {

    return new FlatFileItemWriterBuilder<UserDto>()
            .name("csvResultWriter")
            .resource(new FileSystemResource(outputFile))
            .headerCallback(writer -> writer.write("id,username,email,migrated_at"))
            .delimited()
            .delimiter(",")
            .names("id", "username", "email", "migratedAt")
            .build();
}

CompositeItemWriter - 複数ターゲットへの同時書き込み

@Bean
public CompositeItemWriter<UserDto> compositeWriter(
        JdbcBatchItemWriter<UserDto> dbWriter,
        FlatFileItemWriter<UserDto> fileWriter) {

    CompositeItemWriter<UserDto> writer = new CompositeItemWriter<>();
    writer.setDelegates(Arrays.asList(dbWriter, fileWriter));
    return writer;
}

7. 高度な機能

パーティショニング - 大量データの並列処理

@Configuration
public class PartitionedJobConfig {

    @Bean
    public Step masterStep(JobRepository jobRepository,
                            Partitioner partitioner,
                            Step workerStep) {
        return new StepBuilder("masterStep", jobRepository)
                .partitioner("workerStep", partitioner)
                .step(workerStep)
                .gridSize(4)  // パーティション数(スレッド数)
                .taskExecutor(taskExecutor())
                .build();
    }

    @Bean
    public Partitioner columnRangePartitioner(DataSource dataSource) {
        return gridSize -> {
            Map<String, ExecutionContext> result = new HashMap<>();
            int totalCount = getTotalCount(dataSource);
            int rangeSize = totalCount / gridSize;

            for (int i = 0; i < gridSize; i++) {
                ExecutionContext context = new ExecutionContext();
                context.putLong("minId", (long) i * rangeSize + 1);
                context.putLong("maxId",
                    i == gridSize - 1 ? totalCount : (long)(i + 1) * rangeSize);
                result.put("partition" + i, context);
            }
            return result;
        };
    }

    @Bean
    @StepScope
    public JdbcPagingItemReader<User> partitionedReader(
            DataSource dataSource,
            @Value("#{stepExecutionContext['minId']}") Long minId,
            @Value("#{stepExecutionContext['maxId']}") Long maxId) {

        Map<String, Object> parameterValues = new HashMap<>();
        parameterValues.put("minId", minId);
        parameterValues.put("maxId", maxId);

        return new JdbcPagingItemReaderBuilder<User>()
                .name("partitionedReader")
                .dataSource(dataSource)
                .parameterValues(parameterValues)
                // ... クエリ設定
                .build();
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("batch-partition-");
        executor.initialize();
        return executor;
    }
}

マルチスレッドStep

@Bean
public Step multiThreadedStep(JobRepository jobRepository,
                               PlatformTransactionManager txManager,
                               SynchronizedItemStreamReader<User> reader,
                               ItemWriter<UserDto> writer) {
    return new StepBuilder("multiThreadedStep", jobRepository)
            .<User, UserDto>chunk(100, txManager)
            .reader(reader)   // スレッドセーフなReaderが必要
            .writer(writer)
            .taskExecutor(new SimpleAsyncTaskExecutor())
            .throttleLimit(4)
            .build();
}

// スレッドセーフなReaderのラッピング
@Bean
public SynchronizedItemStreamReader<User> synchronizedReader(
        JdbcCursorItemReader<User> reader) {
    SynchronizedItemStreamReader<User> synchronizedReader = new SynchronizedItemStreamReader<>();
    synchronizedReader.setDelegate(reader);
    return synchronizedReader;
}

AsyncItemProcessor / AsyncItemWriter

@Bean
public AsyncItemProcessor<User, UserDto> asyncProcessor(
        UserMigrationProcessor delegateProcessor) {

    AsyncItemProcessor<User, UserDto> asyncProcessor = new AsyncItemProcessor<>();
    asyncProcessor.setDelegate(delegateProcessor);
    asyncProcessor.setTaskExecutor(new SimpleAsyncTaskExecutor());
    return asyncProcessor;
}

@Bean
public AsyncItemWriter<UserDto> asyncWriter(
        JdbcBatchItemWriter<UserDto> delegateWriter) {

    AsyncItemWriter<UserDto> asyncWriter = new AsyncItemWriter<>();
    asyncWriter.setDelegate(delegateWriter);
    return asyncWriter;
}

JobParametersによる動的設定

@Bean
@StepScope
public JdbcCursorItemReader<User> dynamicReader(
        DataSource dataSource,
        @Value("#{jobParameters['startDate']}") String startDate,
        @Value("#{jobParameters['endDate']}") String endDate) {

    return new JdbcCursorItemReaderBuilder<User>()
            .name("dynamicReader")
            .dataSource(dataSource)
            .sql("SELECT * FROM users WHERE created_at BETWEEN ? AND ?")
            .preparedStatementSetter(ps -> {
                ps.setString(1, startDate);
                ps.setString(2, endDate);
            })
            .rowMapper(new BeanPropertyRowMapper<>(User.class))
            .build();
}

8. 再起動・再処理戦略

スキップ・リトライポリシー

@Bean
public Step robustStep(JobRepository jobRepository,
                        PlatformTransactionManager txManager) {
    return new StepBuilder("robustStep", jobRepository)
            .<User, UserDto>chunk(100, txManager)
            .reader(reader())
            .processor(processor())
            .writer(writer())
            .faultTolerant()
            .skipLimit(10)
            .skip(ValidationException.class)
            .skip(DataIntegrityViolationException.class)
            .retryLimit(3)
            .retry(TransientDataAccessException.class)
            .retry(DeadlockLoserDataAccessException.class)
            .noSkip(FatalBatchException.class)
            .build();
}

SkipListenerの実装

@Component
public class UserSkipListener implements SkipListener<User, UserDto> {

    private static final Logger log = LoggerFactory.getLogger(UserSkipListener.class);

    @Override
    public void onSkipInRead(Throwable t) {
        log.error("読み取り中にスキップ: {}", t.getMessage());
    }

    @Override
    public void onSkipInProcess(User user, Throwable t) {
        log.warn("処理中にスキップ - ユーザーID: {}, エラー: {}",
            user.getId(), t.getMessage());
    }

    @Override
    public void onSkipInWrite(UserDto dto, Throwable t) {
        log.error("書き込み中にスキップ - ユーザーID: {}, エラー: {}",
            dto.getId(), t.getMessage());
    }
}

Job再起動制御

@Bean
public Job nonRestartableJob(JobRepository jobRepository, Step step1) {
    return new JobBuilder("nonRestartableJob", jobRepository)
            .start(step1)
            .preventRestart()  // 失敗後の再起動を禁止
            .build();
}

9. スケジューラー連携

@Scheduled + JobLauncher

@Configuration
@EnableScheduling
public class BatchSchedulerConfig {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job userMigrationJob;

    @Scheduled(cron = "0 0 2 * * *")  // 毎日午前2時
    public void runDailyBatch() {
        try {
            JobParameters params = new JobParametersBuilder()
                    .addString("date", LocalDate.now().toString())
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters();

            JobExecution execution = jobLauncher.run(userMigrationJob, params);
            System.out.println("バッチ実行ステータス: " + execution.getStatus());
        } catch (Exception e) {
            System.err.println("バッチ実行失敗: " + e.getMessage());
        }
    }
}

Quartz Scheduler連携

@Configuration
public class QuartzBatchConfig {

    @Bean
    public JobDetail batchJobDetail() {
        return JobBuilder.newJob(BatchQuartzJob.class)
                .withIdentity("batchJob")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger batchJobTrigger() {
        return TriggerBuilder.newTrigger()
                .forJob(batchJobDetail())
                .withIdentity("batchTrigger")
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
                .build();
    }
}

@Component
public class BatchQuartzJob implements org.quartz.Job {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job userMigrationJob;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            JobParameters params = new JobParametersBuilder()
                    .addLong("time", System.currentTimeMillis())
                    .toJobParameters();
            jobLauncher.run(userMigrationJob, params);
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }
}

REST APIによるJob手動実行

@RestController
@RequestMapping("/api/batch")
public class BatchController {

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job userMigrationJob;

    @PostMapping("/run")
    public ResponseEntity<String> runJob(
            @RequestParam String startDate,
            @RequestParam String endDate) {
        try {
            JobParameters params = new JobParametersBuilder()
                    .addString("startDate", startDate)
                    .addString("endDate", endDate)
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters();

            JobExecution execution = jobLauncher.run(userMigrationJob, params);
            return ResponseEntity.ok("Job実行ID: " + execution.getId()
                + ", ステータス: " + execution.getStatus());
        } catch (Exception e) {
            return ResponseEntity.internalServerError()
                .body("Job実行失敗: " + e.getMessage());
        }
    }
}

10. テスト

@SpringBatchTestの設定

@SpringBatchTest
@SpringBootTest(classes = {UserMigrationJobConfig.class, TestBatchConfig.class})
@ActiveProfiles("test")
class UserMigrationJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;

    @BeforeEach
    void clearMetadata() {
        jobRepositoryTestUtils.removeJobExecutions();
    }

    @Test
    void testCompleteJob() throws Exception {
        JobExecution jobExecution = jobLauncherTestUtils.launchJob(
            new JobParametersBuilder()
                .addString("date", "2026-03-17")
                .toJobParameters()
        );

        assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
        assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);
    }

    @Test
    void testSingleStep() throws Exception {
        JobExecution jobExecution = jobLauncherTestUtils.launchStep("migrationStep");

        StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
        assertThat(stepExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
        assertThat(stepExecution.getWriteCount()).isGreaterThan(0);
    }
}

StepScopeTestExecutionListenerの活用

@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    StepScopeTestExecutionListener.class
})
class ItemReaderTest {

    @Autowired
    private JdbcCursorItemReader<User> userReader;

    public StepExecution getStepExecution() {
        StepExecution execution = MetaDataInstanceFactory.createStepExecution();
        execution.getExecutionContext().putString("inputFile", "classpath:test-users.csv");
        return execution;
    }

    @Test
    void testReader() throws Exception {
        List<User> users = new ArrayList<>();
        User user;
        while ((user = userReader.read()) != null) {
            users.add(user);
        }
        assertThat(users).isNotEmpty();
    }
}

統合テストの例

@SpringBatchTest
@SpringBootTest
@Testcontainers
class BatchIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @DynamicPropertySource
    static void setProps(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private UserRepository userRepository;

    @Test
    void testFullMigrationPipeline() throws Exception {
        // Given: テストデータの挿入
        insertTestUsers(100);

        // When: バッチJobの実行
        JobExecution execution = jobLauncherTestUtils.launchJob();

        // Then: 結果の検証
        assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
        assertThat(userRepository.countByMigratedTrue()).isEqualTo(100);
    }
}

11. モニタリング

Spring Batch Actuatorエンドポイント

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,batch
  endpoint:
    batch:
      enabled: true

Micrometer + Prometheusメトリクス

@Configuration
public class BatchMetricsConfig {

    @Bean
    public BatchMetrics batchMetrics(MeterRegistry meterRegistry) {
        return new BatchMetrics(meterRegistry);
    }
}

主要な監視メトリクス:

  • spring.batch.job.* — Job実行時間、成功/失敗件数
  • spring.batch.step.* — Step別の読み取り/処理/書き込み件数
  • spring.batch.item.* — アイテムごとの処理レイテンシ

クイズ:Spring Batch知識チェック

Q1. ChunkベースのStepでItemProcessorがnullを返すとどうなりますか?

答え: そのアイテムはフィルタリングされ、ItemWriterに渡されません。

解説: ItemProcessorが特定のアイテムに対してnullを返すと、Spring Batchはそのアイテムを自動的にフィルタリングし、Writerに転送しません。これは条件付きフィルタリングを実装する最もクリーンな方法で、SkipLimitカウンターにも含まれません。例外をスローしてスキップ処理する方法とは異なり、スキップカウントに影響しない点が特徴です。

Q2. JdbcCursorItemReaderとJdbcPagingItemReaderの主な違いと、それぞれの適切なユースケースは何ですか?

答え: JdbcCursorItemReaderはDBカーソルを使って単一接続でストリーミング読み取りを行い、JdbcPagingItemReaderはLIMIT/OFFSETクエリでページ単位に読み取ります。

解説: JdbcCursorItemReaderは単一接続でカーソルを保持しながらデータをストリーミング読み取りするためメモリ効率が高いですが、スレッドセーフではありません(マルチスレッドStepには不向き)。JdbcPagingItemReaderはページごとに新しいクエリを発行するため接続プールとの相性が良く、マルチスレッド環境でも安全です。大量データのシングルスレッド処理にはCursor、並列処理や再起動が重要な場合はPagingを推奨します。

Q3. Spring BatchでJobを再起動(Restart)する際、前回の失敗地点から処理を再開するにはどうすればよいですか?

答え: 同じJobParametersでJobを再実行すると、Spring Batchが自動的に最後に失敗したStepから再開します。

解説: Spring BatchはJobRepositoryに実行履歴を保存します。同じJobParametersでJobを再実行すると、BATCH_JOB_EXECUTIONテーブルから最後に失敗したJobExecutionを探し、そのStepから処理を再開します。preventRestart()を呼び出すと再起動が無効になります。Chunk処理では、すでに正常にコミットされたChunkは再処理されず、失敗したChunk以降から再開します。

Q4. パーティショニングStepを使う理由とgridSizeパラメータの意味を説明してください。

答え: 大量データを複数パーティションに分割して並列処理するために使用し、gridSizeは作成するパーティション(並列実行単位)の数を指定します。

解説: パーティショニングStepは大規模なデータセットを論理的に複数のセグメントに分割し、各セグメントを別スレッドまたはプロセスで並列処理します。gridSizeはパーティション数を決定し、通常は利用可能なCPUコア数やDB接続プールのサイズに合わせて設定します。Partitionerインターフェースを実装することで、ID範囲、日付範囲、ファイルリストなど様々な基準でパーティションを定義できます。

Q5. application.ymlでspring.batch.job.enabled: falseと設定する理由は何ですか?

答え: アプリケーション起動時に登録されているすべてのJobが自動実行されることを防ぐためです。

解説: Spring Batchはデフォルトでアプリケーションコンテキストのロード時に登録されているすべてのJobを自動実行します。WebアプリケーションにバッチJobを組み込む場合や、REST APIやスケジューラーを通じて明示的にJobを実行したい場合にはfalseに設定します。開発環境でもサーバーを起動するたびにバッチが実行されることを防げます。Jobを実行するにはJobLauncherを通じて明示的に実行する必要があります。