Skip to content

Extend multi-row INSERT (addRow) to execute() and loop-friendly call shapes (#1825)#1826

Open
zio0911 wants to merge 2 commits into
OpenFeign:masterfrom
zio0911:feature/jpa-addrow-without-keys
Open

Extend multi-row INSERT (addRow) to execute() and loop-friendly call shapes (#1825)#1826
zio0911 wants to merge 2 commits into
OpenFeign:masterfrom
zio0911:feature/jpa-addrow-without-keys

Conversation

@zio0911

@zio0911 zio0911 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1825. Two related improvements to multi-row INSERT support via addRow() on JPAInsertClause / HibernateInsertClause:

  1. 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(...), forcing callers to use a key-returning method even when keys were not needed.
  2. Loop-friendly multi-row call shapeaddRow() now captures effective column paths on its first call. execute() (multi-row path) and executeWithKeys(...) fall back to those captured paths when the current-state inserts/columns are empty. This makes the natural for-loop with set(...).addRow() at the end of every iteration work, without a "first row" flag and without keeping the last row in the buffer.

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();
}
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);

Behavioral changes

  • JPAInsertClause.execute() / HibernateInsertClause.execute():
    • If 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, template-value INSERTs already routed through native) keep their existing path.
  • addRow():
    • Captures effective column paths once, on its first call. Executors use those captured paths as fallback when inserts/columns are both empty (i.e. the trailing iteration was also closed with addRow()).
  • JpaInsertNativeHelper.requireSqlModule() (added): native insert paths depend on the optional querydsl-sql module; guards them with an actionable IllegalStateException instead of an opaque NoClassDefFoundError when querydsl-sql is absent from the classpath.
  • addRow() Javadoc clarified: builds a single multi-row VALUES statement, not JDBC batching.

Out of scope / clarifications

  • addRow() builds a single SQL statement with multiple VALUES tuples. It is not JDBC batching (PreparedStatement.addBatch() / executeBatch()).
  • Single-row inserts without addRow() are unchanged.
  • INSERT ... SELECT (select(SubQueryExpression)) is unchanged and still rejects addRow().

Test plan

  • New unit/integration tests in JPAExecuteWithKeyTest and HibernateExecuteWithKeyTest covering both new shapes:
    • execute_multi_row_set_style_with_trailing_addRowset() + trailing addRow() + execute() (no keys)
    • executeWithKeys_multi_row_set_style_with_trailing_addRow — same shape + key return
    • execute_multi_row_without_keys_inserts_all_rowscolumns()/values() + addRow + execute()
    • execute_multi_row_in_a_loop_with_trailing_addRow — loop-friendly form with columns()/values()
  • ./mvnw -pl querydsl-libraries/querydsl-jpa -Pno-databases test — 414 tests pass, 0 failures.

zio0911 added 2 commits June 25, 2026 10:19
Previously addRow() only produced a multi-row INSERT when keys were
returned via executeWithKeys(); a plain execute() ignored the
accumulated rows and inserted only the trailing row.

- execute() now emits a single native INSERT INTO t (...) VALUES
  (..),(..),... when rows were accumulated via addRow(), in both
  JPAInsertClause and HibernateInsertClause; a trailing un-flushed row
  is treated as the last row (loop-friendly, no first-row bookkeeping).
- Add JpaInsertNativeHelper.requireSqlModule(): the native insert paths
  depend on the optional querydsl-sql module, so guard them with an
  actionable IllegalStateException instead of a bare NoClassDefFoundError
  when querydsl-sql is absent from the classpath.
- Clarify addRow() Javadoc: it builds a single multi-row VALUES
  statement, not JDBC batching.
…penFeign#1825)

set()-style + a trailing addRow() at the end of every loop iteration left
both the inserts map and the columns list empty after the loop, so executors
threw "No columns specified for insert". Callers had to keep a "first row"
flag and intentionally leave the last row in the buffer to recover the
column list from inserts.keySet().

addRow() now captures the effective column paths on its first call.
executeMultiRow() and executeWithKeys() fall back to those captured paths
when the current-state inserts/columns are empty.

Combined with the prior change that routes execute() to a native multi-row
INSERT when rows were accumulated, the loop-friendly shape

  for (var r : rows) { insert.set(...).set(...).addRow(); }
  insert.execute();   // or executeWithKeys(...) when keys are needed

now works for both JPAInsertClause and HibernateInsertClause with no
first-row bookkeeping and no buffer-retention trick.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend multi-row INSERT (addRow) to the non-key-returning execute() and to loop-friendly call shapes

1 participant