- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Spring Batchアーキテクチャ
- 2. 依存関係の設定
- 3. 基本的なJob設定
- 4. ItemReaderの実装
- 5. ItemProcessorの実装
- 6. ItemWriterの実装
- 7. 高度な機能
- 8. 再起動・再処理戦略
- 9. スケジューラー連携
- 10. テスト
- 11. モニタリング
- クイズ:Spring Batch知識チェック
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を通じて明示的に実行する必要があります。