Skip to content

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

Description

@zio0911
  • I am willing to put in the work and submit a PR to resolve this issue.

Summary

This adds 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(...), which forced callers to use the key-returning method even when keys were not needed.

  2. Loop-friendly multi-row call shapeaddRow() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions