モダンJavaバッチ開発:Spring Boot & Spring BatchでH2データベースCRUD操作をマスターする完全サンプル

現代のビジネスアプリケーションにおいて、大量のデータを効率的かつ確実に処理するバッチ処理は不可欠です。

特にJavaエコシステムでは、Spring BootとSpring Batchの組み合わせが、堅牢なバッチアプリケーション開発のデファクトスタンダードとなっています。

本記事では、この強力な組み合わせを活用し、H2データベース上で、CSVファイルからの指示に基づくCRUD操作(登録、更新、削除)を行うバッチ処理の実装を、初心者向けにステップバイステップで解説します。

本記事で学べること

Spring Batchの基本的な概念から、H2データベースのセットアップ、そして実際のCRUD処理の実装、さらにはAPI経由での手動起動やJavaの最新機能の活用まで、実践的な知識を網羅します。

  • Spring Bootプロジェクトのセットアップ
  • H2データベースの組み込みと設定
  • CSVファイルからのデータ読み込み(ItemReader)
  • CRUD操作を決定するデータ処理ロジック(ItemProcessor)
  • H2データベースへのCRUD操作(ItemWriter)
  • Spring Batchジョブの定義と実行
  • (応用編 1)バッチ処理をAPIで手動起動する
  • (応用編 2)Javaの最新機能(Virtual Threads, Pattern Matching for switch)の活用

このガイドを通じて、Spring BootとSpring Batchを用いたモダンなバッチ開発の基礎を習得し、ご自身のプロジェクトに活かすための第一歩を踏み出しましょう。


対象読者

  • Spring Batchの基本的な概念を学びたい方
  • Spring Bootでバッチアプリケーションを開発したい方
  • H2データベースを使った簡単なデータ操作をバッチで実現したい方
  • CSVファイルからの入力に基づいてデータベースを操作するバッチ処理に興味がある方

目次


1. Spring BatchとH2データベースの基本

1-1. Spring Batchとは

Spring Batchは、堅牢でスケーラブルなバッチアプリケーションを開発するためのSpring Frameworkのサブプロジェクトです。大量のデータを処理する際に必要となる、トランザクション管理、チャンク処理、再起動、スキップ、リトライなどの機能を提供します。


1-2. H2データベースとは

H2データベースは、Javaで書かれたリレーショナルデータベース管理システムです。非常に軽量で高速であり、インメモリモードで動作させることができるため、開発やテスト環境での利用に最適です。本記事では、このインメモリモードを利用して、手軽にデータベース操作を試します。


1-3. 関連記事

Spring Bootバッチ環境構築やバッチ処理の基本構造に関しては、以下の記事で詳細に解説していますので、是非ご覧ください。


2. プロジェクトセットアップ

2-1. Spring Bootプロジェクトの作成

Spring Initializrを使用して、新しいSpring Bootプロジェクトを作成します。

プロジェクトのメタデータは以下のように設定します。

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 最新の安定版(例: 3.5.7)
  • Group: com.example
  • Artifact: spring-batch-h2-crud
  • Name: spring-batch-h2-crud
  • Description: Demo H2 Batch project for Spring Boot(任意)
  • Package name: com.example.springbatchh2crud
  • Packaging: Jar
  • Configuration: Properties
  • Java: 最新のLTS版(例: 25)または選択可能なバージョン(例: 17, 21)

以下の依存関係(Dependencies)を追加してください。

  • Spring Batch: バッチ処理のフレームワーク
  • Spring Web: H2コンソールを有効にするため(任意ですが、開発時に便利です)
  • H2 Database: インメモリデータベース
  • Spring Data JDBC: データベースアクセスを簡素化するため

プロジェクトを生成し、お好みのIDE(IntelliJ IDEA, VS Codeなど)で開いてください。

生成されたプロジェクトは、以下のようなSpring Bootアプリケーションの構造を持ちます。
このフォルダ構造は、本記事の完成形を示しています。

spring-batch-h2-crud
├───pom.xml
└───src
    ├───main
       ├───java
          └───com
              └───example
                  └───springbatchh2crud
                      ├───SpringBatchH2CrudApplication.java
                      ├───config
                         └───BatchConfig.java
                      ├───controller
                         └───JobLaunchController.java
                      ├───model
                         ├───User.java
                         └───UserOperation.java
                      ├───processor
                         └───UserOperationProcessor.java
                      ├───repository
                         └───UserRepository.java
                      └───writer
                          └───UserOperationWriter.java
       └───resources
           ├───application.properties
           ├───input
              └───input.csv
           ├───sql
              └───data.sql
           ├───static
           └───templates
    └───test
        └───java
            └───com
                └───example
                    └───springbatchh2crud
                        └───SpringBatchH2CrudApplicationTests.java

2-2. 必要な依存関係の追加(pom.xml

もし既存のプロジェクトにSpring BatchとH2データベースを追加する場合は、pom.xmlに依存関係を追加します。以下の設定を参考にしてください。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-batch-h2-crud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-batch-h2-crud</name>
    <description>Demo H2 Batch project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3. H2データベースの準備と設定

3-1. application.propertiesの設定

src/main/resources/application.propertiesファイルに、H2データベースとH2コンソールの設定を追加します。

spring.application.name=spring-batch-h2-crud

# H2 データベース設定
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# H2コンソール設定 (開発時のみ有効にすることを推奨)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA 設定
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true

# Spring Batch 設定
spring.batch.jdbc.initialize-schema=always
spring.batch.job.enabled=true
spring.sql.init.data-locations=classpath:sql/data.sql
  • spring.application.name=spring-batch-h2-crud
    • この設定は、Spring Bootアプリケーションの名前を定義します。アプリケーションのログ出力や監視ツールなどで識別しやすくなります。
  • H2 データベース設定
    • spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
      • H2データベースへの接続URLを指定します。
      • jdbc:h2:mem:testdb: インメモリデータベースとしてtestdbという名前のデータベースを作成します。アプリケーションが停止するとデータは失われます。
      • DB_CLOSE_DELAY=-1: 最後の接続が閉じられてもデータベースを閉じないようにします。これにより、複数の接続やアプリケーションのライフサイクルを通じてデータベースが利用可能になります。
      • DB_CLOSE_ON_ON_EXIT=FALSE: JVMが終了してもデータベースを閉じないようにします。インメモリデータベースの場合、これは通常、アプリケーションの再起動時にデータがリセットされることを意味します。
    • spring.datasource.driverClassName=org.h2.Driver
      • H2データベースに接続するためのJDBCドライバーのクラス名を指定します。
    • spring.datasource.username=sa
      • データベース接続に使用するユーザー名を指定します。H2データベースのデフォルトユーザー名です。
    • spring.datasource.password=
      • データベース接続に使用するパスワードを指定します。H2データベースのデフォルトではパスワードは空です。
    • spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
      • JPAプロバイダ(ここではHibernate)がH2データベース固有のSQL方言を使用するように設定します。これにより、H2データベースに最適化されたSQLが生成されます。
  • JPA 設定
    • spring.jpa.hibernate.ddl-auto=update
      • アプリケーション起動時にデータベースのスキーマ(テーブル構造など)を自動的に操作する方法を定義します。
      • update: エンティティクラスの定義に基づいて、既存のスキーマを更新します。新しいテーブルやカラムが追加されますが、既存のデータは保持されます。開発環境でスキーマ変更を頻繁に行う場合に便利です。
    • spring.jpa.show-sql=true
      • Hibernateが実行するSQL文をログに出力するように設定します。デバッグやSQLの確認に役立ちます。
    • spring.jpa.properties.hibernate.format_sql=true
      • show-sqltrueの場合に、出力されるSQLを整形して読みやすくします。
    • spring.jpa.defer-datasource-initialization=true
      • データソースの初期化(schema.sqldata.sqlの実行)を、JPAのスキーマ生成が完了するまで遅延させます。これにより、JPAがテーブルを作成した後に初期データが投入されるため、ddl-autodata.sqlを併用する際の競合を防ぎ、ベストプラクティスとされています。
  • H2コンソール設定 (開発時のみ有効にすることを推奨)
    • spring.h2.console.enabled=true
      • H2データベースのWebベースの管理コンソールを有効にします。開発中にデータベースの内容を視覚的に確認するのに非常に便利です。
    • spring.h2.console.path=/h2-console
      • H2コンソールにアクセスするためのURLパスを設定します。アプリケーション起動後、http://localhost:8080/h2-consoleにアクセスしてデータベースの内容を確認できます。
  • Spring Batch 設定
    • spring.batch.jdbc.initialize-schema=always
      • Spring Batchが自身のメタデータ(ジョブの実行履歴などを管理するテーブル)をデータベースに自動的に作成・初期化する方法を定義します。
      • always: アプリケーション起動時に常にメタデータスキーマを作成します。
    • spring.batch.job.enabled=true
      • Spring Bootアプリケーション起動時に、定義されているSpring Batchジョブの自動実行を有効にします。
      • この値をfalseに設定することにより、APIエンドポイント経由など、プログラムから明示的にジョブを実行する制御が可能になります。
    • spring.sql.init.data-locations=classpath:sql/data.sql
      • データベースの初期化に使用するデータスクリプトの場所を指定します。
      • classpath:sql/data.sql: src/main/resources/sql/data.sqlファイルが初期データとして使用されることを示します。

これらの設定により、H2データベースをバックエンドとして使用し、Spring Batchジョブを柔軟に制御できるSpring Bootアプリケーションの基盤が構築されます。


3-2. スキーマ定義と初期データ(data.sql

src/main/resources/sqlディレクトリにdata.sqlを作成し、データベースのスキーマ定義と初期データを設定します。

data.sql

INSERT INTO users (name, email, status) VALUES ('Alice', 'alice@example.com', 'ACTIVE');
INSERT INTO users (name, email, status) VALUES ('Bob', 'bob@example.com', 'INACTIVE');

4. CSVファイルからのCRUD指示の読み込み(ItemReader)

4-1. CSVファイルフォーマットの定義

CSVファイルは、各行が1つの操作(登録、更新、削除)を表し、その操作に必要なデータを含みます。
フォーマット例:operation,id,name,email

  • operation: INSERT, UPDATE, DELETE のいずれか
  • id: ユーザーID(UPDATE, DELETE操作で必須)
  • name: ユーザー名(INSERT, UPDATE操作で任意)
  • email: メールアドレス(INSERT, UPDATE操作で任意)

input.csvの例

src/main/resources/inputディレクトリにinput.csvを作成します。

CREATE,,Charlie,charlie@example.com,ACTIVE
UPDATE,1,Alice_Updated,alice_updated@example.com,ACTIVE
DELETE,2,,,

4-2. データモデルの作成

CSVの各行をマッピングするためのJavaクラスを作成します。ここでは、Lombokのアノテーションを使用して、ボイラープレートコード(getter, setter, toStringなど)を削減します。

src/main/java/com/example/springbatchh2crud/model/UserOperation.java

package com.example.springbatchh2crud.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserOperation {
    private String operation;
    private Long id;
    private String name;
    private String email;
    private String status;
}

CSVの各行をデータベース操作するためのJavaクラスを作成します。ここでは、Lombokのアノテーションを使用して、ボイラープレートコード(getter, setter, toStringなど)を削減します。

package com.example.springbatchh2crud.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private String status;
}

補足:JPAエンティティの命名とテーブル名の衝突回避について

データベースの予約語とJPAエンティティ名が衝突する場合があります。
例えば、Userというエンティティ名がデータベースの予約語と重なるケースです。
このような場合、Javaのクラス名自体を変更するのではなく、@Table(name = "...") アノテーションを使用してデータベース上のテーブル名のみを変更することがベストプラクティスとされています。

これは、ビジネスロジックを表現するドメインモデル(Javaクラス)と、データを永続化するための永続化モデル(データベーステーブル)の関心を分離し、ドメイン駆動設計の原則に従うためです。
クラス名をビジネスドメインに忠実なものに保ちつつ、データベース固有の制約はアノテーションで吸収することで、コードの可読性と保守性を高めることができます。


4-3. ItemReaderの実装

CSVファイルを読み込み、UserOperationオブジェクトにマッピングするFlatFileItemReaderを設定します。

src/main/java/com/example/springbatchh2crud/config/BatchConfig.javaに以下のBean定義を追加します。

package com.example.springbatchh2crud.config;

import com.example.springbatchh2crud.model.UserOperation;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    public BatchConfig(
        JobRepository jobRepository,
        PlatformTransactionManager transactionManager,
    ) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
    }

    @Bean
    public FlatFileItemReader<UserOperation> userOperationReader() {
        return new FlatFileItemReaderBuilder<UserOperation>()
            .name("userOperationReader")
            .resource(new ClassPathResource("input/input.csv")) // input.csvファイルを指定
            .delimited()
            .names("operation", "id", "name", "email", "status") // CSVヘッダーに対応するフィールド名
            .fieldSetMapper(
                new BeanWrapperFieldSetMapper<>() {
                    {
                        setTargetType(UserOperation.class);
                    }
                }
            )
            .build();
    }

    // ItemProcessor, ItemWriter, Step, Jobの定義は後述
}

5. データ処理(ItemProcessor)

ItemProcessorでは、UserOperationオブジェクトを受け取り、そのoperationフィールドに基づいて、後続のItemWriterで実行すべきデータベース操作を決定します。

ここでは、UserOperationオブジェクトをそのまま次のステップに渡しますが、必要に応じて別のオブジェクトに変換することも可能です。

src/main/java/com/example/springbatchh2crud/processor/UserOperationProcessor.java

package com.example.springbatchh2crud.processor;

import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;

import com.example.springbatchh2crud.model.UserOperation;

@Component
public class UserOperationProcessor implements ItemProcessor<UserOperation, UserOperation> {

    @Override
    public UserOperation process(UserOperation item) throws Exception {
        // ここで必要に応じてデータを加工したり、バリデーションを行ったりできます。
        // 今回は、UserOperationオブジェクトをそのまま次のステップに渡します。
        System.out.println("Processing user operation: " + item);
        return item;
    }
}

BatchConfig.javaUserOperationProcessorのBean定義を追加します。

// BatchConfig.java (抜粋)
// ...
import com.example.springbatchh2crud.processor.UserOperationProcessor;
// ...

@Configuration
public class BatchConfig {
    // ...

    @Bean
    public UserOperationProcessor userOperationProcessor() {
        return new UserOperationProcessor();
    }

    // ...
}

6. H2データベースへのデータ操作(ItemWriter)

ItemWriterでは、UserOperationオブジェクトのoperationフィールドに基づいて、H2データベースに対して適切なSQL操作(INSERT, UPDATE, DELETE)を実行します。


まずはUserRepositoryを実装し、JPAを使って必要なデータアクセスを実装します。

package com.example.springbatchh2crud.repository;

import com.example.springbatchh2crud.model.User;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByEmail(String email);
}
補足:Spring Data JPAによる自動実装
UserRepositoryインターフェースはJpaRepositoryを継承しているため、Spring Data JPAが自動的に基本的なCRUD(Create, Read, Update, Delete)操作の実装を提供します。
これにより、開発者はデータベースアクセスのためのボイラープレートコードを記述することなく、save(), findById(), findAll(), delete()などのメソッドをすぐに利用できます。
また、findByEmail()のように、命名規則に従うことでカスタムクエリメソッドも自動生成されます。

次にH2データベースに対して適切なSQL操作(INSERT, UPDATE, DELETE)を実装します。
ここでは先ほど実装したUserRepositoryを使用してデータアクセスを実装します。

src/main/java/com/example/springbatchh2crud/writer/UserOperationWriter.java

package com.example.springbatchh2crud.writer;

import com.example.springbatchh2crud.model.User;
import com.example.springbatchh2crud.model.UserOperation;
import com.example.springbatchh2crud.repository.UserRepository;
import java.util.Optional;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.stereotype.Component;

@Component
public class UserOperationWriter implements ItemWriter<UserOperation> {

    private final UserRepository userRepository;

    public UserOperationWriter(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void write(Chunk<? extends UserOperation> chunk) throws Exception {
        for (UserOperation item : chunk) {
            switch (item.getOperation()) {
                case "CREATE":
                    // emailが重複しないようにチェック
                    if (userRepository.findByEmail(item.getEmail()).isEmpty()) {
                        User newUser = new User(
                            null,
                            item.getName(),
                            item.getEmail(),
                            item.getStatus()
                        );
                        userRepository.save(newUser);
                        System.out.println("Created User: " + newUser);
                    } else {
                        System.out.println(
                            "Skipped CREATE for existing email: " +
                                item.getEmail()
                        );
                    }
                    break;
                case "UPDATE":
                    if (item.getId() != null) {
                        Optional<User> existingUser = userRepository.findById(
                            item.getId()
                        );
                        if (existingUser.isPresent()) {
                            User userToUpdate = existingUser.get();
                            userToUpdate.setName(item.getName());
                            userToUpdate.setEmail(item.getEmail());
                            userToUpdate.setStatus(item.getStatus());
                            userRepository.save(userToUpdate);
                            System.out.println("Updated User: " + userToUpdate);
                        } else {
                            System.out.println(
                                "Skipped UPDATE for non-existent ID: " +
                                    item.getId()
                            );
                        }
                    } else {
                        System.out.println(
                            "Skipped UPDATE: ID is null for operation " +
                                item.getOperation()
                        );
                    }
                    break;
                case "DELETE":
                    if (item.getId() != null) {
                        userRepository.deleteById(item.getId());
                        System.out.println(
                            "Deleted User with ID: " + item.getId()
                        );
                    } else {
                        System.out.println(
                            "Skipped DELETE: ID is null for operation " +
                                item.getOperation()
                        );
                    }
                    break;
                default:
                    System.out.println(
                        "Unknown operation: " + item.getOperation()
                    );
                    break;
            }
        }
    }
}

BatchConfig.javaUserOperationWriterのBean定義を追加します。

// BatchConfig.java (抜粋)
// ...
import com.example.springbatchh2crud.writer.UserOperationWriter;
import javax.sql.DataSource;
// ...

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final UserRepository userRepository;

    public BatchConfig(
        JobRepository jobRepository,
        PlatformTransactionManager transactionManager,
        UserRepository userRepository
    ) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.userRepository = userRepository;
    }

    // ...

    @Bean
    public UserOperationWriter userOperationWriter() {
        return new UserOperationWriter(userRepository);
    }

    // ...
}

7. バッチジョブの定義と実行

7-1. BatchConfigクラスでのJob/Step定義

BatchConfig.javaに、ItemReaderItemProcessorItemWriterを組み合わせてStepとJobを定義します。

完成したBatchConfig.javaのコードを以下に示します。

package com.example.springbatchh2crud.config;

import com.example.springbatchh2crud.model.UserOperation;
import com.example.springbatchh2crud.processor.UserOperationProcessor;
import com.example.springbatchh2crud.repository.UserRepository;
import com.example.springbatchh2crud.writer.UserOperationWriter;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final UserRepository userRepository;

    public BatchConfig(
        JobRepository jobRepository,
        PlatformTransactionManager transactionManager,
        UserRepository userRepository
    ) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.userRepository = userRepository;
    }

    @Bean
    public FlatFileItemReader<UserOperation> userOperationReader() {
        return new FlatFileItemReaderBuilder<UserOperation>()
            .name("userOperationReader")
            .resource(new ClassPathResource("input/input.csv")) // input.csvファイルを指定
            .delimited()
            .names("operation", "id", "name", "email", "status") // CSVヘッダーに対応するフィールド名
            .fieldSetMapper(
                new BeanWrapperFieldSetMapper<>() {
                    {
                        setTargetType(UserOperation.class);
                    }
                }
            )
            .build();
    }

    // ItemProcessor, ItemWriter, Step, Jobの定義は後述
    @Bean
    public UserOperationProcessor userOperationProcessor() {
        return new UserOperationProcessor();
    }

    @Bean
    public UserOperationWriter userOperationWriter() {
        return new UserOperationWriter(userRepository);
    }

    @Bean
    public Step processUserOperationStep() {
        return new StepBuilder("processUserOperationStep", jobRepository)
            .<UserOperation, UserOperation>chunk(10, transactionManager)
            .reader(userOperationReader())
            .processor(userOperationProcessor())
            .writer(userOperationWriter())
            .build();
    }

    @Bean
    public Job userOperationJob() {
        return new JobBuilder("userOperationJob", jobRepository)
            .start(processUserOperationStep())
            .build();
    }
}

7-2. バッチの起動方法

コマンドラインからの実行

このプロジェクトはMaven Wrapper (mvnw) を使用しているため、Mavenを別途インストールする必要はありません。

  • macOS / Linux の場合: コマンドの先頭に ./mvnw を付けて実行します。
  • Windows の場合: コマンドの先頭に mvnw.cmd を付けて実行します。
1. パッケージング(実行可能JARの作成)

アプリケーションを実行可能な単一のJARファイルとしてパッケージングします。

./mvnw clean package

成功すると target/ ディレクトリ内に spring-batch-h2-crud-0.0.1-SNAPSHOT.jar というファイルが生成されます。

2. アプリケーションの実行

パッケージ化されたJARファイルを使ってアプリケーションを起動します。

java -jar target/spring-batch-h2-crud-0.0.1-SNAPSHOT.jar

バッチ処理の実行結果確認

アプリケーションを起動すると、Spring Batchが自動的にuserOperationJobを実行します。

実行後、H2コンソール(http://localhost:8080/h2-console)にアクセスし、usersテーブルの内容を確認してください。CSVファイルで指示したCRUD操作が反映されているはずです。

SELECT * FROM USERS;
IDEMAILNAMESTATUS
1alice_updated@example.comAlice_UpdatedACTIVE
3charlie@example.comCharlieACTIV

H2コンソールの利用方法

アプリケーション起動後、H2コンソールを利用してデータベースの内容を確認できます。

  1. H2コンソールへのアクセス:
    ブラウザで http://localhost:8080/h2-console にアクセスします。
  2. 正しいJDBC URLでの接続:
    H2コンソールのログイン画面で、以下の情報を入力して「接続」ボタンをクリックします。
    • JDBC URL: jdbc:h2:mem:testdbapplication.propertiesで設定したURLと一致させる必要があります)
    • ユーザー名: saapplication.propertiesで設定したユーザー名と一致させます)
    • パスワード: (空欄、application.propertiesで設定したパスワードと一致させます)
    これにより、アプリケーションが使用している稼働中のインメモリデータベースに接続し、usersテーブルの内容を確認できます。CSVファイルで指示したCRUD操作が反映されているはずです。

8. 応用編1:バッチ処理をAPIで手動起動する

バッチジョブをAPI経由で手動で起動できるようにすることで、アプリケーションの起動とは独立してジョブの実行を制御できます。

これは、特定のタイミングでジョブを実行したい場合や、外部システムからジョブをトリガーしたい場合に有用です。


8-1. ジョブ自動実行の無効化

まず、アプリケーション起動時にSpring Batchジョブが自動的に実行されないように設定を変更します。src/main/resources/application.propertiesファイルに以下の設定を追加または変更します。

spring.batch.job.enabled=false

この設定により、Spring Bootアプリケーションが起動しても、JobLauncherApplicationRunnerによるジョブの自動実行は行われなくなります。


8-2. 手動実行用APIエンドポイントの作成

次に、HTTPリクエストを受け付けてバッチジョブを起動するAPIエンドポイントを作成します。src/main/java/com/example/springbatchh2crud/controller/JobLaunchController.javaを新規作成し、以下のコードを記述します。

package com.example.springbatchh2crud.controller;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * [新規] このコントローラは、Spring Batchジョブを手動でトリガーするためのAPIエンドポイントを提供します。
 * ジョブを起動時に自動的に実行する代わりに、オンデマンドで実行できるように作成されています。
 */
@RestController
public class JobLaunchController {

    private final JobLauncher jobLauncher;
    private final Job userOperationJob;

    /**
     * 依存性注入のためのコンストラクタ。
     * @param jobLauncher Spring BatchのJobLauncher。
     * @param userOperationJob 起動する特定のジョブ。"userOperationJob"というBean名で識別されます。
     */
    @Autowired
    public JobLaunchController(
        JobLauncher jobLauncher,
        @Qualifier("userOperationJob") Job userOperationJob
    ) {
        this.jobLauncher = jobLauncher;
        this.userOperationJob = userOperationJob;
    }

    /**
     * /launch-jobにPOSTリクエストが行われたときに'userOperationJob'を起動します。
     * <p>
     * ジョブを複数回実行できるようにするため、各実行のJobParametersに
     * 一意の'time'パラメータが追加されます。Spring Batchは、まったく同じパラメータで
     * ジョブが複数回実行されるのを防ぎます。
     * </p>
     * @return ジョブの起動試行の結果を示すResponseEntity。
     */
    @PostMapping("/launch-job")
    public ResponseEntity<String> launchJob() {
        try {
            // ジョブインスタンスが常に一意であることを保証するために、JobParametersにタイムスタンプを追加します
            JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();

            jobLauncher.run(userOperationJob, jobParameters);

            return ResponseEntity.ok(
                "ジョブ '" +
                    userOperationJob.getName() +
                    "' は正常に起動されました。"
            );
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(
                "ジョブの起動に失敗しました: " + e.getMessage()
            );
        }
    }
}

JobLaunchControllerの解説

  • @RestController: このクラスがRESTful APIのエンドポイントであることを示します。
  • JobLauncher: Spring Batchが提供するインターフェースで、ジョブを起動するために使用します。
  • Job userOperationJob: BatchConfigで定義したuserOperationJobをインジェクションしています。@Qualifier("userOperationJob")アノテーションは、複数のJobBeanが存在する場合に、どのジョブをインジェクションするかを明示的に指定するために使用します。
  • @PostMapping("/launch-job"): /launch-jobへのPOSTリクエストがこのlaunchJob()メソッドにマッピングされます。
  • JobParameters: Spring Batchでは、同じJobParametersを持つジョブは一度しか実行されません。そのため、System.currentTimeMillis()を使って現在時刻をtimeパラメータとして追加することで、毎回異なるJobParametersを生成し、ジョブの複数回実行を可能にしています。
  • jobLauncher.run(userOperationJob, jobParameters): 実際にジョブを起動する処理です。

8-3. APIの実行方法

アプリケーションを起動した後、任意のHTTPクライアント(例: curl, Postman, VS CodeのREST Clientなど)を使用して、以下のエンドポイントにPOSTリクエストを送信することで、バッチジョブを手動で起動できます。

curl -X POST http://localhost:8080/launch-job

リクエストが成功すると、ジョブ 'userOperationJob' は正常に起動されました。のようなレスポンスが返されます。ジョブの実行状況はアプリケーションのログで確認できます。


curlコマンドの具体的な利用方法については、以下の記事で詳細に解説していますので、是非ご覧ください。


9. 応用編2:Javaの最新機能の活用

9-1. Virtual Threadsの活用

Java 21で正式導入されたVirtual Threads(仮想スレッド)は、軽量なスレッドであり、I/Oバウンドな処理が多いバッチアプリケーションにおいて、スループットの向上に貢献する可能性があります。Spring Batch 5.1以降では、Virtual Threadsを簡単に統合できるようになっています。

BatchConfig.javaに以下の設定を追加することで、Spring Batchのタスク実行にVirtual Threadsを使用できます。

// BatchConfig.java (抜粋)
// ...
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.VirtualThreadTaskExecutor;
// ...

@Configuration
public class BatchConfig {
    // ...

    @Bean
    public TaskExecutor taskExecutor() {
        return new VirtualThreadTaskExecutor();
    }

    @Bean
    public Step processUserOperationStep() {
        return new StepBuilder("processUserOperationStep", jobRepository)
                .<UserOperation, UserOperation>chunk(10, transactionManager)
                .reader(userOperationReader())
                .processor(userOperationProcessor())
                .writer(userOperationWriter())
                .taskExecutor(taskExecutor()) // Virtual Threadsを使用するように設定
                .build();
    }

    // ...
}

これにより、processUserOperationStep内のチャンク処理がVirtual Threads上で実行されるようになります。特に、データベースアクセスなどのI/O処理が多い場合に、スレッドプールの枯渇を気にすることなく、より多くの並行処理を効率的に実行できるようになります。


9-2. Pattern Matching for switchの活用

Java 17で正式導入されたPattern Matching for switchは、switch文をより簡潔かつ安全に記述するための機能です。UserOperationWriterwriteメソッド内でoperationフィールドの文字列比較を行っていますが、これをよりモダンなJavaの書き方に変更できます。

UserOperationWriter.javawriteメソッド内のswitch文を以下のように変更できます。

// UserOperationWriter.java (抜粋)
// ...
    @Override
    public void write(Chunk<? extends UserOperation> chunk) throws Exception {
        List<UserOperation> inserts = new ArrayList<>();
        List<UserOperation> updates = new ArrayList<>();
        List<UserOperation> deletes = new ArrayList<>();

        for (UserOperation item : chunk.getItems()) {
            switch (item.getOperation()) {
                case "INSERT" -> inserts.add(item);
                case "UPDATE" -> updates.add(item);
                case "DELETE" -> deletes.add(item);
                default -> System.err.println("Unknown operation: " + item.getOperation());
            }
        }
        // ... 後続の処理は同じ
    }
// ...

この変更により、switch文の可読性が向上し、より簡潔に各ケースの処理を記述できるようになります。


まとめ

本記事では、Spring BatchとSpring Boot、H2データベースを使用して、CSVファイルからの指示に基づいてデータベースのCRUD操作を行うバッチ処理を実装する方法を解説しました。また、Javaの最新機能であるVirtual ThreadsとPattern Matching for switchを応用することで、よりモダンで効率的なバッチアプリケーションを構築できることを示しました。

このチュートリアルを参考に、ご自身のプロジェクトでSpring Batchを活用してみてください。


免責事項:

本記事は、執筆時点での情報に基づいており、将来的に情報が変更される可能性があります。本記事の内容によって生じたいかなる損害についても、筆者は一切の責任を負いません。読者の皆様ご自身の判断と責任において、本記事の情報をご活用ください。

本記事はチュートリアルを目的としており、本番環境での利用には追加の考慮事項(エラーハンドリング、ロギング、監視、セキュリティなど)が必要です


SNSでもご購読できます。

コメントを残す

*