Commit fd29953
authored
Add executeWithKey() to JPAInsertClause and HibernateInsertClause (#1693)
## Summary
- Add `executeWithKey(Path<T>)` and `executeWithKey(Class<T>)` to
`JPAInsertClause` and `HibernateInsertClause`
- Add `addRow()` and `executeWithKeys(Path<T>) /
executeWithKeys(Class<T>) → List<T>` for **multi-row INSERT with batched
key return** (single statement, single round-trip)
- Bypass JPQL and execute native SQL INSERT via JDBC with
`Statement.RETURN_GENERATED_KEYS` to retrieve auto-generated keys
- Introduce `JpaNativeInsertSerializer` (extends `SQLSerializer`) for a
single-pass SQL + constants build, ensuring function templates, params,
and paths serialize correctly
- Add `JpaInsertNativeHelper` utility for `@Table`/`@Column` resolution
and JDBC binding (single- and multi-row execution)
- Add `doReturningWork()` to `SessionHolder` interface and all
implementations
Closes #1692
## Motivation
The SQL module's `SQLInsertClause` supports `executeWithKey()` (and
`executeWithKeys()` for batched inserts), but the JPA module does not.
This forces JPA users to fall back to `EntityManager.persist()` +
`flush()` for single-row inserts, and to a `for`-loop of N single-row
calls for bulk inserts — N statements, N round-trips, no batching.
Using the SQL module in a JPA project requires a separate
`SQLQueryFactory`, SQL-specific Q-classes, and managing two query
factories — excessive overhead just for insert key return.
## Before / After
**Before** — must break out of QueryDSL:
```java
// Single row
entityManager.persist(entity);
entityManager.flush();
Long seq = entity.getSeq();
// Bulk — N statements, N round-trips
List<Long> ids = new ArrayList<>();
for (var dto : dtos) {
entityManager.persist(toEntity(dto));
entityManager.flush();
ids.add(toEntity(dto).getSeq());
}
```
**After** — stays in QueryDSL:
```java
// Single row
Long seq = queryFactory.insert(role)
.set(role.name, dto.roleName())
.set(role.status, status)
.executeWithKey(role.seq);
// Bulk — 1 statement, 1 round-trip, all keys returned
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);
```
## Implementation
Since JPA's `Query.executeUpdate()` only returns affected row count, the
implementation:
1. Reads `@Table` / `@Column` annotations to build native SQL (same
pattern as `NativeSQLSerializer`)
2. Multi-row support emits a single `INSERT INTO t (...) VALUES
(..),(..),...` statement; `getGeneratedKeys()` is iterated to collect
all keys in row order
3. `HibernateInsertClause` uses `Session.doReturningWork()` for JDBC
access
4. `JPAInsertClause` uses
`EntityManager.unwrap(Session.class).doReturningWork()`
5. `executeWithKey()` (singular) throws `IllegalStateException` when
called after `addRow()` to guard single-row contracts from being
silently violated
### Single-pass serialization (regression fix)
The earlier draft built the SQL via a
`JpaInsertNativeHelper.buildNativeInsertSQL` step while constants were
extracted from a separate `JPQLSerializer` pass. The helper emitted one
`?` per column without inspecting the value expression tree, so a
function template like `dbo.encrypt({0})` was dropped from the SQL and
only the inner constant was bound — surfacing as plaintext stored in the
DB.
This is now a **single serialization pass**: `JpaNativeInsertSerializer`
extends `SQLSerializer` and produces the SQL and the constants list
together. By reusing the `SQLSerializer` visitor, function templates,
constants, params, and paths all serialize correctly. `@Column` /
`@Table` resolution follows the same pattern as `NativeSQLSerializer`,
and plain `?` placeholders come from `SQLSerializer.serializeConstant`'s
default behavior. The broken `buildNativeInsertSQL` path is removed.
Regression tests cover:
- Function template with bound constant: `upper({0})` + `"value"` → DB
stores `"VALUE"`
- Zero-argument templates and multi-argument templates
- Identifier quoting via a custom `SQLTemplates`
### On adding SQL serialization to the JPA module
A fair concern was raised about expanding SQL-mapping responsibilities
in the JPA module. Mitigations:
- **No new dependency.** `NativeSQLSerializer` already lives in this
module; the new serializer reuses the same infrastructure.
- **Narrow scope.** The serializer is only used on the `executeWithKey`
/ `executeWithKeys` path. `execute()` and `toString()` still go through
`JPQLSerializer` unchanged.
- **Fallback option.** If this still feels like the wrong tradeoff, the
scope can be narrowed so `executeWithKey` / `executeWithKeys` only
accept `Constant` / `Param` / `Path` values and explicitly reject
anything else (no template support).
## 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()` (verified on H2; MySQL Connector/J 8+,
PostgreSQL JDBC 42+ known to support this)
- Requires explicit `@Table` / `@Column` annotations if using a custom
Hibernate `PhysicalNamingStrategy`
- `JPAInsertClause` currently relies on Hibernate as the JPA provider
## Test plan
- [x] Unit tests for `JpaInsertNativeHelper` (table name resolution,
column name resolution, SQL generation, multi-row SQL generation)
- [x] Integration tests for `HibernateInsertClause.executeWithKey()`
(set style, columns/values style, class type, multiple inserts, column
annotation, subquery rejection)
- [x] Integration tests for `JPAInsertClause.executeWithKey()` (same
scenarios)
- [x] Integration tests for `executeWithKeys()` on both clauses
(multi-row returns all keys in order, single-row returns size-1 list,
`executeWithKey` rejected after `addRow`, `addRow` rejected with no
pending values)
- [x] Regression tests for function template handling (`upper({0})` +
constant, zero-arg templates, custom `SQLTemplates` identifier quoting)
- [x] All new tests pass (21 integration tests across both clauses)
- [x] Existing tests unaffected — single-row `executeWithKey` signature
and behavior unchanged (internal serializer refactor delegates the
legacy path to the multi-row implementation)
- [x] `./mvnw -Pdev verify` passes
- [x] Code formatted (`git-code-format` pre-commit hook)14 files changed
Lines changed: 1310 additions & 0 deletions
File tree
- querydsl-libraries/querydsl-jpa/src
- main/java/com/querydsl/jpa
- hibernate
- impl
- test
- java/com/querydsl/jpa
- domain
- resources/META-INF
Lines changed: 158 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
Lines changed: 159 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/DefaultSessionHolder.java
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
42 | 48 | | |
0 commit comments