Skip to content

Add executeWithKey() support to JPAInsertClause and HibernateInsertClause #1692

@zio0911

Description

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

Motivation

QueryDSL provides a readable, type-safe query API for select, update, and delete operations in JPA projects. However, there is no way to retrieve generated keys after an insert in the JPA module. This forces developers to fall back to EntityManager.persist() + flush() just for insert + key return, breaking the consistent QueryDSL style.

The same gap also blocks bulk inserts with key return. The SQL module's SQLInsertClause supports addBatch() + executeWithKeys() to issue a single multi-row INSERT and return all generated keys in row order. The JPA module has no equivalent, so JPA users must fall back to a for-loop of single-row persist() + flush() — N statements, N round-trips, no batching.

The SQL module already supports executeWithKey() / executeWithKeys(), but using it in a JPA project requires a separate SQLQueryFactory configuration, SQL-specific Q-classes generated from the DB schema, and managing two different query factories. This is excessive overhead just for insert key return.

Before / After

Before (current) — must break out of QueryDSL for insert + key return:

@Autowired JPAQueryFactory queryFactory;

// insert + key return → forced to use EntityManager
entityManager.persist(entity);
entityManager.flush();
Long seq = entity.getSeq();

// back to QueryDSL for update
queryFactory.update(role)
    .set(role.role, "ROLE_" + seq)
    .where(role.seq.eq(seq))
    .execute();

// bulk insert + key return → N statements, N round-trips, no batching
List<Long> ids = new ArrayList<>();
for (var dto : dtos) {
    var entity = toEntity(dto);
    entityManager.persist(entity);
    entityManager.flush();
    ids.add(entity.getSeq());
}

After (with this feature) — everything stays in QueryDSL:

@Autowired JPAQueryFactory queryFactory;

// insert + single key return
Long seq = queryFactory.insert(role)
    .set(role.name, dto.roleName())
    .set(role.status, status)
    .set(role.description, dto.description())
    .set(role.createdBy, actorSeq)
    .set(role.createdAt, LocalDateTime.now())
    .executeWithKey(role.seq);

// update with generated key
queryFactory.update(role)
    .set(role.role, "ROLE_" + seq)
    .where(role.seq.eq(seq))
    .execute();

// bulk insert + all keys returned — 1 statement, 1 round-trip
List<Long> ids = queryFactory.insert(member)
    .columns(member.name, member.email)
    .values("Alice", "a@x.com").addRow()
    .values("Bob",   "b@x.com").addRow()
    .values("Carol", "c@x.com")           // trailing addRow optional
    .executeWithKeys(member.seq);

Proposed Solution

Add the following to both JPAInsertClause and HibernateInsertClause:

  • executeWithKey(Path<T>) and executeWithKey(Class<T>) — single-row insert returning one generated key
  • addRow() — finalize the current values() / set() state as one row and clear it for the next row
  • executeWithKeys(Path<T>) and executeWithKeys(Class<T>) → List<T> — multi-row insert returning all generated keys in row order

Since JPA's Query.executeUpdate() only returns affected row count and has no API for generated keys, the implementation bypasses JPQL and executes a native SQL INSERT via JDBC with Statement.RETURN_GENERATED_KEYS:

  • Reads @Table / @Column annotations to build native SQL (same pattern as NativeSQLSerializer)
  • Multi-row support emits a single INSERT INTO t (...) VALUES (..),(..),... statement; getGeneratedKeys() is iterated to collect all keys in row order
  • HibernateInsertClause uses Session.doReturningWork() for JDBC access
  • JPAInsertClause uses EntityManager.unwrap(Session.class).doReturningWork()
  • executeWithKey() (singular) throws IllegalStateException when called after addRow() to guard single-row contracts from being silently violated

Limitations

  • INSERT ... SELECT subqueries are not supported (throws UnsupportedOperationException) for both executeWithKey and executeWithKeys
  • Multi-row VALUES (..),(..) uses standard SQL syntax — Oracle's INSERT ALL form would need a dialect branch (follow-up)
  • Multi-row key retrieval relies on the JDBC driver returning all rows from getGeneratedKeys() (MySQL Connector/J 8+, PostgreSQL JDBC 42+, H2 known to support this)
  • Requires explicit @Table / @Column annotations if using a custom Hibernate PhysicalNamingStrategy
  • JPAInsertClause currently relies on Hibernate as the JPA provider for JDBC connection access

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions