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

Share this code

Here's the card — post it anywhere.

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