Summary
This adds two related improvements to multi-row INSERT support via addRow() on JPAInsertClause / HibernateInsertClause:
-
Multi-row execution without returning keys — a plain execute() now emits a single native INSERT INTO t (...) VALUES (..),(..),... when rows were accumulated via addRow(), returning the affected row count. Previously this only worked through executeWithKeys(...), which forced callers to use the key-returning method even when keys were not needed.
-
Loop-friendly multi-row call shape — addRow() now captures the effective column paths on its first call, and executors fall back to those captured paths when inserts/columns are empty. This lets users write a natural for-loop that calls set(...).addRow() at the end of every iteration, with no "first row" flag and no trailing-row-in-buffer trick.
Motivation
addRow() was usable only in a narrow combination of styles:
- A plain
execute() ignored the accumulated rows entirely (only the trailing un-flushed row was inserted, and in some shapes the JPQL path was taken with a null sub-query → NPE). So even when keys were not needed, callers had to call executeWithKeys(...) and discard the returned keys just to take the native multi-row path.
- With
set()-style (no columns(...)), calling addRow() at the end of every iteration left both the inserts map and the columns list empty after the loop. executeWithKeys(...) then threw "No columns specified for insert", because the only source of column paths in set()-style was inserts.keySet(), which addRow() had cleared.
To make multi-row INSERT actually usable, callers had to leave the last row in the buffer and guard addRow() with a "first row" flag — both of which are non-obvious and easy to get wrong.
Call site shapes that now work
// set()-style, trailing addRow(), no keys needed
var insert = queryFactory.insert(entity);
for (var r : rows) {
insert
.set(entity.a, r.a())
.set(entity.b, r.b())
.addRow(); // closes this iteration's row
}
long count = insert.execute();
// columns()/values()-style, trailing addRow(), keys returned
var keys = queryFactory.insert(entity)
.columns(entity.a, entity.b)
.values(1, "x").addRow()
.values(2, "y").addRow()
.executeWithKeys(entity.id);
No "first row" flag, no buffer-retention trick, and no need to call a key-returning method to get a multi-row INSERT.
Behavioral changes
execute() on JPAInsertClause / HibernateInsertClause:
- When
rows (accumulated via addRow()) is non-empty and no subQuery is set, emits a single native multi-row INSERT INTO t (...) VALUES (..),(..),... and returns the affected row count.
- All other cases (single-row plain INSERTs,
INSERT ... SELECT) keep their existing path.
addRow():
- Now captures the effective column paths once, on its first call. The captured paths are used as a fallback by
execute() (multi-row path) and executeWithKeys(...) when the current-state inserts/columns are both empty.
Out of scope / clarifications
addRow() builds a single SQL statement with multiple VALUES tuples. This is not JDBC batching (PreparedStatement.addBatch() / executeBatch(), i.e. many statements grouped into one round-trip). The Javadoc was clarified accordingly.
- Single-row inserts without
addRow() are unchanged.
INSERT ... SELECT (select(SubQueryExpression)) is unchanged and still rejects addRow().
Environment
- querydsl-jpa: 7.x (current master)
- Hibernate: 6.x / 7.x
- Java: 17+
- Tested against H2 (in-memory) for both
JPAInsertClause and HibernateInsertClause execution paths.
Summary
This adds two related improvements to multi-row INSERT support via
addRow()onJPAInsertClause/HibernateInsertClause:Multi-row execution without returning keys — a plain
execute()now emits a single nativeINSERT INTO t (...) VALUES (..),(..),...when rows were accumulated viaaddRow(), returning the affected row count. Previously this only worked throughexecuteWithKeys(...), which forced callers to use the key-returning method even when keys were not needed.Loop-friendly multi-row call shape —
addRow()now captures the effective column paths on its first call, and executors fall back to those captured paths wheninserts/columnsare empty. This lets users write a natural for-loop that callsset(...).addRow()at the end of every iteration, with no "first row" flag and no trailing-row-in-buffer trick.Motivation
addRow()was usable only in a narrow combination of styles:execute()ignored the accumulated rows entirely (only the trailing un-flushed row was inserted, and in some shapes the JPQL path was taken with a null sub-query → NPE). So even when keys were not needed, callers had to callexecuteWithKeys(...)and discard the returned keys just to take the native multi-row path.set()-style (nocolumns(...)), callingaddRow()at the end of every iteration left both theinsertsmap and thecolumnslist empty after the loop.executeWithKeys(...)then threw"No columns specified for insert", because the only source of column paths inset()-style wasinserts.keySet(), whichaddRow()had cleared.To make multi-row INSERT actually usable, callers had to leave the last row in the buffer and guard
addRow()with a "first row" flag — both of which are non-obvious and easy to get wrong.Call site shapes that now work
No "first row" flag, no buffer-retention trick, and no need to call a key-returning method to get a multi-row INSERT.
Behavioral changes
execute()onJPAInsertClause/HibernateInsertClause:rows(accumulated viaaddRow()) is non-empty and nosubQueryis set, emits a single native multi-rowINSERT INTO t (...) VALUES (..),(..),...and returns the affected row count.INSERT ... SELECT) keep their existing path.addRow():execute()(multi-row path) andexecuteWithKeys(...)when the current-stateinserts/columnsare both empty.Out of scope / clarifications
addRow()builds a single SQL statement with multipleVALUEStuples. This is not JDBC batching (PreparedStatement.addBatch()/executeBatch(), i.e. many statements grouped into one round-trip). The Javadoc was clarified accordingly.addRow()are unchanged.INSERT ... SELECT(select(SubQueryExpression)) is unchanged and still rejectsaddRow().Environment
JPAInsertClauseandHibernateInsertClauseexecution paths.