Skip to content

Repository files navigation

Spring Data JPA Batch DML

Comparison of 6 batch insert strategies using Spring Data JPA and PostgreSQL.

Strategies

Strategy Description
JDBC Batch JdbcTemplate.batchUpdate() with native SQL
Hibernate Batch saveAllAndFlush() with hibernate.jdbc.batch_size and reWriteBatchedInserts
UNNEST INSERT ... SELECT FROM UNNEST(...): single SQL statement using PostgreSQL arrays
COPY COPY ... FROM STDIN: PostgreSQL binary protocol via CopyManager
Multi-row VALUES INSERT INTO ... VALUES (...), (...), ...: single statement with all rows as parameter placeholders
JSON INSERT ... SELECT FROM jsonb_to_recordset(...): single JSON parameter, no bind-param limit

Project Structure

spring-data-jpa-batch-dml/
├── batch-core/          # Models, repositories, services, utilities (JAR)
├── web/                 # Spring Boot app, controllers, records (bootJar)
├── benchmark/           # JMH benchmarks
└── docker-compose.yml   # PostgreSQL 17

Modules

  • batch-core: shared business logic (JPA entities, repositories, 6 @Service classes for batch strategies)
  • web: Spring Boot application with REST API (/customers, /stores), Springdoc
  • benchmark: JMH benchmarks comparing the 6 batch insert strategies at 100, 1,000, and 10,000 rows

Prerequisites

  • Java 25+
  • Docker (for PostgreSQL)

Getting Started

# Start PostgreSQL
docker compose up -d

# Build
./gradlew clean build -x test

# Run the application
./gradlew :web:bootRun

The application starts on port 8081.

API

# Batch insert (modes: jdbc, hibernate, unnest, copy, multirow, json)
curl -X POST "http://localhost:8081/customers/save-all?number=1000&mode=json"

# Get all customers
curl http://localhost:8081/customers/get-all

# Get all with store and orders (eager fetch)
curl http://localhost:8081/customers/get-all-with-store-orders

# Delete all
curl -X DELETE http://localhost:8081/customers/delete-all

Tests

# Unit tests (H2)
./gradlew test

# Integration tests (requires PostgreSQL)
./gradlew :batch-core:integrationTest

JMH Benchmarks

# Run benchmarks (requires PostgreSQL)
./gradlew :benchmark:jmh

Results are written to benchmark/build/reports/jmh/results.json.

Results (100, 1,000, and 10,000 customers)

Environment: GraalVM 25.0.3 (JDK 25), PostgreSQL 17 (Docker), batch_size=100, reWriteBatchedInserts=true

JMH config: fork=1, warmup=1×3s, measurement=3×5s, mode=AverageTime

Strategy 100 rows (ms) 1,000 rows (ms) 10,000 rows (ms)
JDBC Batch 1.609 ± 0.525 5.874 ± 0.321 45.163 ± 10.520
COPY 2.568 ± 1.044 6.989 ± 1.286 39.170 ± 14.839
UNNEST 1.538 ± 1.129 5.277 ± 0.702 41.715 ± 3.730
Multi-row VALUES 1.568 ± 1.161 5.725 ± 0.661 46.895 ± 9.378
JSON 1.702 ± 1.251 6.005 ± 3.417 48.295 ± 4.480
Hibernate Batch 3.061 ± 2.220 24.160 ± 81.483 211.531 ± 225.190

Note: High error margins at 10,000 rows are typical for JMH with Spring application context startup included (each fork starts a fresh context). Focus on the score column for relative comparisons. The multi-row VALUES and JSON batch sizes are capped at 4,000 rows per statement: multi-row to stay below PostgreSQL's 32,767 bind parameter limit, JSON for practical JSON array size management.

JDBC Batch, UNNEST, and multi-row VALUES are the fastest for small batches (100 rows). JSON is competitive across all scales: performance is within 5-15% of the leading strategies.

At 10,000 rows, COPY and UNNEST lead. JSON and multi-row VALUES are mid-pack with nearly identical throughput.

Hibernate Batch is slower due to persistence context overhead (dirty checking, cascading).

Tech Stack

  • Spring Boot 4.1.0
  • Spring Data JPA / Hibernate 7.4
  • PostgreSQL 17
  • Lombok
  • JMH 1.37
  • Gradle 9.4.1 (multi-module)

About

Different ways to batch SQL statements with Spring Boot

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages