# 03 — Inserting objects efficiently [← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md) Backs [ankurm.com: Inserting objects efficiently](https://ankurm.com/mastering-hibernate-7-the-ultimate-guide-to-inserting-objects-efficiently/). Sources: [`InsertIdentityRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java) / [`InsertSequenceRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java), entities [`WidgetIdentity`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java) / [`WidgetSequence`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java). Run both with `./scripts/run.sh insert-identity` and `./scripts/run.sh insert-sequence`. Transcripts: [`docs/output/insert-identity.txt`](output/insert-identity.txt), [`docs/output/insert-sequence.txt`](output/insert-sequence.txt). ## The one-line difference that matters Both entities persist 30 rows in a single transaction with **identical** settings: ```yaml hibernate.jdbc.batch_size: 25 hibernate.order_inserts: true ``` `WidgetIdentity` uses `@GeneratedValue(strategy = GenerationType.IDENTITY)`. `WidgetSequence` uses `@GeneratedValue(strategy = GenerationType.SEQUENCE)` with a matching `allocationSize = 25`. That's the entire diff between the two entity classes. ## What Hibernate's own statistics say happened | | `entityInsertCount` | `prepareStatementCount` | |---|---|---| | `WidgetIdentity` (IDENTITY) | 30 | **30** | | `WidgetSequence` (SEQUENCE) | 30 | **4** | `hibernate.jdbc.batch_size=25` did nothing at all for the `IDENTITY` run — every one of the 30 inserts is its own round trip to the database (`prepareStatementCount` equals `entityInsertCount`). With `SEQUENCE`, Hibernate knows the id before the row is written, so it can queue inserts and batch them: 30 rows at a batch size of 25 means two insert batches (25 + 5), plus two calls to pull the next block of ids from `widget_seq` (the sequence's `allocationSize` is also 25, so the first 25 ids come from one call and the remaining 5 force a second) — four prepared statements total, for the same 30 rows. ## What surprised me building this The number that surprised me was not "IDENTITY doesn't batch" — that's documented, if you know to look for it. It was seeing `prepareStatementCount` for `SEQUENCE` land at exactly **4**, not 2. It's obvious in hindsight — `allocationSize` governs how often the sequence itself gets hit, independently of `batch_size` governing how the inserts get grouped — but "obvious in hindsight" and "what I would have guessed beforehand" are different things, and the gap between them is exactly what running this instead of describing it catches. If `allocationSize` had been left at JPA's default of `50` instead of matching `batch_size` at `25`, the sequence would only need one call for all 30 ids, and `prepareStatementCount` would drop to 3 — a change to a number that has nothing to do with batching, moving a number that looks like it's entirely about batching. The practical version of this: if a switch to `SEQUENCE` doesn't produce the batching win the Hibernate docs promise, checking `hibernate.generate_statistics=true` and reading `prepareStatementCount` directly answers "is it actually batching" in a way that reading the `hibernate.jdbc.batch_size` value in a config file cannot — the config says what was requested, not what happened. ## Long-tail edge cases not covered above - **`save()` vs `persist()`** — `save()` is Hibernate's own pre-JPA API and still works, but returns the generated id immediately rather than `void`, and can be called outside a transaction (where it will fail later, confusingly, at flush time). `persist()` is the JPA-portable choice; there's no scenario for this in this repo because the observable difference is in the method signature and portability, not in captured runtime behaviour. - **Bulk inserts via `StatelessSession`** — bypasses the persistence context and lifecycle callbacks entirely; worth a dedicated repository of its own rather than a profile bolted onto this one, since the interesting failure modes (cascades silently not firing, no dirty checking) need a scenario built around triggering them specifically. - **Native SQL batch inserts via `createNativeQuery` + `addBatch`** — sidesteps Hibernate's own batching machinery altogether; the batching behaviour at that point is entirely the JDBC driver's, not Hibernate's, which is a different post. ## Full captured transcripts ### `insert-identity.txt` ```console Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) Hibernate: create sequence book_seq start with 1 increment by 50 Hibernate: create sequence widget_seq start with 1 increment by 25 --- inserting 30 WidgetIdentity rows (GenerationType.IDENTITY) --- Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-1] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-2] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-3] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-4] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-5] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-6] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-7] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-8] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-9] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-10] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-11] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-12] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-13] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-14] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-15] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-16] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-17] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-18] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-19] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-20] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-21] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-22] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-23] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-24] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-25] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-26] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-27] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-28] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-29] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) binding parameter (1:VARCHAR) <- [identity-30] entityInsertCount = 30 prepareStatementCount = 30 (with IDENTITY, expect prepareStatementCount to land close to entityInsertCount -- each insert has to go to the database immediately to hand back the generated key, so there is nothing left for hibernate.jdbc.batch_size to batch) ``` ### `insert-sequence.txt` ```console Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) Hibernate: create sequence book_seq start with 1 increment by 50 Hibernate: create sequence widget_seq start with 1 increment by 25 --- inserting 30 WidgetSequence rows (GenerationType.SEQUENCE, allocationSize=25) --- Hibernate: select next value for widget_seq Hibernate: select next value for widget_seq Hibernate: select next value for widget_seq Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-1] binding parameter (2:BIGINT) <- [1] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-2] binding parameter (2:BIGINT) <- [2] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-3] binding parameter (2:BIGINT) <- [3] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-4] binding parameter (2:BIGINT) <- [4] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-5] binding parameter (2:BIGINT) <- [5] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-6] binding parameter (2:BIGINT) <- [6] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-7] binding parameter (2:BIGINT) <- [7] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-8] binding parameter (2:BIGINT) <- [8] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-9] binding parameter (2:BIGINT) <- [9] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-10] binding parameter (2:BIGINT) <- [10] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-11] binding parameter (2:BIGINT) <- [11] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-12] binding parameter (2:BIGINT) <- [12] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-13] binding parameter (2:BIGINT) <- [13] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-14] binding parameter (2:BIGINT) <- [14] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-15] binding parameter (2:BIGINT) <- [15] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-16] binding parameter (2:BIGINT) <- [16] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-17] binding parameter (2:BIGINT) <- [17] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-18] binding parameter (2:BIGINT) <- [18] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-19] binding parameter (2:BIGINT) <- [19] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-20] binding parameter (2:BIGINT) <- [20] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-21] binding parameter (2:BIGINT) <- [21] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-22] binding parameter (2:BIGINT) <- [22] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-23] binding parameter (2:BIGINT) <- [23] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-24] binding parameter (2:BIGINT) <- [24] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-25] binding parameter (2:BIGINT) <- [25] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-26] binding parameter (2:BIGINT) <- [26] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-27] binding parameter (2:BIGINT) <- [27] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-28] binding parameter (2:BIGINT) <- [28] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-29] binding parameter (2:BIGINT) <- [29] Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) binding parameter (1:VARCHAR) <- [sequence-30] binding parameter (2:BIGINT) <- [30] entityInsertCount = 30 prepareStatementCount = 4 (with SEQUENCE, the id is known before the row is written, so Hibernate can defer and batch the inserts -- expect prepareStatementCount well below entityInsertCount) ``` [← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md)