java 105 lines · 3 tabs

Efficient JDBC Batch Inserts With addBatch, executeBatch, and Generated Keys

Shared by codesnips Aug 2026
3 tabs
public record User(Long id, String email, String displayName, boolean active) {

    public static User of(String email, String displayName) {
        return new User(null, email, displayName, true);
    }

    public User withId(long assignedId) {
        return new User(assignedId, email, displayName, active);
    }
}
3 files · java Explain with highlit

Inserting thousands of rows one INSERT at a time is one of the most common performance traps in JDBC code: every statement pays a network round-trip and, without an explicit transaction, an implicit commit per row. This snippet shows the standard cure — grouping statements into batches with PreparedStatement.addBatch() and flushing them with executeBatch() inside a single transaction — wrapped behind a small repository so callers never touch raw JDBC.

The User record in the first tab is a plain immutable carrier for the data being persisted. It intentionally carries no persistence logic; keeping the domain object dumb means the batching mechanics live entirely in the repository layer where they can be tuned in one place.

BatchUserRepository is the core of the example. In saveAll, a connection is taken from a DataSource (typically pool-backed such as HikariCP) and setAutoCommit(false) is called so the whole load is one atomic unit — either every row lands or none does, and no per-row commit overhead is incurred. A single PreparedStatement is reused across all rows: for each User the parameters are bound and addBatch() queues the row without sending it. The code flushes with executeBatch() every BATCH_SIZE rows rather than accumulating the entire dataset, which bounds memory and driver-side buffering — an important pitfall when loading millions of rows. Statement.RETURN_GENERATED_KEYS plus getGeneratedKeys() after each flush recovers the database-assigned ids and rebuilds User instances with them. A catch block issues an explicit rollback() so a mid-load failure leaves the table untouched.

A subtle but real-world detail: many drivers only truly pipeline batches when told to. The JDBC URL in AppBootstrap sets reWriteBatchedInserts=true, which lets the PostgreSQL driver rewrite the batch into multi-row INSERT statements — often a 5–10x speedup over naive execution. The bootstrap tab also demonstrates the intended call site: build a list, hand it to saveAll, and read back the persisted rows with their ids.

The trade-offs are worth noting. Batching sacrifices per-row error granularity — a BatchUpdateException reports which entries failed via getUpdateCounts(), but recovery is coarser than single inserts. It also holds a transaction and connection longer, so BATCH_SIZE is a tuning knob balancing throughput against lock duration. For typical bulk-load paths, though, this pattern is the pragmatic default.


Related snips

go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Efficient JDBC Batch Inserts With addBatch, executeBatch, and Generated Keys — share card
Link copied