264 lines
18 KiB
Markdown
264 lines
18 KiB
Markdown
# 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)
|