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
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
SQLInsertClausesupportsaddBatch()+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 afor-loop of single-rowpersist()+flush()— N statements, N round-trips, no batching.The SQL module already supports
executeWithKey()/executeWithKeys(), but using it in a JPA project requires a separateSQLQueryFactoryconfiguration, 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:
After (with this feature) — everything stays in QueryDSL:
Proposed Solution
Add the following to both
JPAInsertClauseandHibernateInsertClause:executeWithKey(Path<T>)andexecuteWithKey(Class<T>)— single-row insert returning one generated keyaddRow()— finalize the currentvalues()/set()state as one row and clear it for the next rowexecuteWithKeys(Path<T>)andexecuteWithKeys(Class<T>) → List<T>— multi-row insert returning all generated keys in row orderSince 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 withStatement.RETURN_GENERATED_KEYS:@Table/@Columnannotations to build native SQL (same pattern asNativeSQLSerializer)INSERT INTO t (...) VALUES (..),(..),...statement;getGeneratedKeys()is iterated to collect all keys in row orderHibernateInsertClauseusesSession.doReturningWork()for JDBC accessJPAInsertClauseusesEntityManager.unwrap(Session.class).doReturningWork()executeWithKey()(singular) throwsIllegalStateExceptionwhen called afteraddRow()to guard single-row contracts from being silently violatedLimitations
INSERT ... SELECTsubqueries are not supported (throwsUnsupportedOperationException) for bothexecuteWithKeyandexecuteWithKeysVALUES (..),(..)uses standard SQL syntax — Oracle'sINSERT ALLform would need a dialect branch (follow-up)getGeneratedKeys()(MySQL Connector/J 8+, PostgreSQL JDBC 42+, H2 known to support this)@Table/@Columnannotations if using a custom HibernatePhysicalNamingStrategyJPAInsertClausecurrently relies on Hibernate as the JPA provider for JDBC connection access