Add executeWithKey() to JPAInsertClause and HibernateInsertClause#1693
Add executeWithKey() to JPAInsertClause and HibernateInsertClause#1693zio0911 wants to merge 7 commits into
Conversation
b9a1975 to
2308d3e
Compare
|
@zio0911 Shouldn't a similar approach to escaping table and column names as in NativeSQLSerializer be used in JpaInsertNativeHelper? See for example: getTemplates().quoteIdentifier(column.name(), precededByDot). |
|
@kamilkrzywanski Good catch, thanks for the review. You're right — using raw identifiers can break when column/table names contain SQL reserved words, spaces, or dialect-specific characters. I'll update Will push the fix shortly. |
Apply dialect-specific identifier quoting to table and column names in JpaInsertNativeHelper, consistent with NativeSQLSerializer. This avoids issues when identifiers contain SQL reserved words, spaces, or dialect-specific characters. - buildNativeInsertSQL now accepts SQLTemplates parameter - Schema-qualified table names quote schema and table parts separately - JPAInsertClause and HibernateInsertClause pass SQLTemplates.DEFAULT - Deprecate the overload without SQLTemplates - Add test covering always-quote templates
|
Pushed the fix in 5decf59. Summary of changes:
|
5decf59 to
19ff4ce
Compare
Previously JpaInsertNativeHelper.buildNativeInsertSQL emitted one ? per
column without inspecting the value expression tree, so function
templates like dbo.encrypt({0}) were dropped from the generated SQL and
only the inner constant got bound. SQL and constants were also produced
by separate serialization passes (helper for SQL, JPQLSerializer for
constants), so the two sides could diverge.
Replace this with a single JpaNativeInsertSerializer extending
SQLSerializer that produces the SQL and constants together. Function
templates, paths, params, and constants now serialize correctly via the
SQLSerializer visitor, with plain ? placeholders for JDBC binding and
@Column/@table resolution following the NativeSQLSerializer pattern.
Add regression coverage in JpaNativeInsertSerializerTest plus
integration tests in JPAExecuteWithKeyTest and HibernateExecuteWithKeyTest
that verify the screenshot case (upper({0}) + "value" -> DB stores
"VALUE"), zero-arg and multi-arg templates, and identifier quoting.
|
The root cause was that the SQL was built separately by JpaInsertNativeHelper.buildNativeInsertSQL while constants were extracted from a different JPQLSerializer pass. The helper only 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 got bound — exactly what your debugger snapshot shows. I refactored this to a single serialization pass: a new JpaNativeInsertSerializer extends SQLSerializer produces the SQL and the constants list together. Because it reuses 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 is removed. Regression tests cover the screenshot case (upper({0}) + "value" → DB stores "VALUE"), zero-arg and multi-arg templates, and identifier quoting via custom SQLTemplates. On the broader concern about introducing SQL mapping into the JPA module: fair point. The mitigation is that no new dependency was added — NativeSQLSerializer already lives in this module — and the new serializer is only used on the executeWithKey path; execute() and toString() still go through JPQLSerializer. If you still feel it's the wrong tradeoff, I can narrow the scope so executeWithKey only accepts Constant/Param/Path values and explicitly rejects anything else. |
Extends executeWithKey() (single-row) with batched-key support so JPA users can issue a single multi-row INSERT and receive all generated keys, mirroring the SQL module's executeWithKeys(). JPAInsertClause / HibernateInsertClause: - addRow() finalizes the current values()/set() state as one row and clears it for the next row - executeWithKeys(Path<T>) and executeWithKeys(Class<T>) return List<T> of generated keys in row order; trailing values are auto-flushed - executeWithKey() now throws IllegalStateException when called after addRow() to guard the single-row contract JpaNativeInsertSerializer: - New serializeInsertRows() emits a single INSERT INTO t (...) VALUES (..),(..),... statement; legacy single-row serializeInsert() delegates to it JpaInsertNativeHelper: - executeAndReturnKeys() iterates the full getGeneratedKeys() ResultSet to collect every key in row order Tests cover multi-row key return, single-row List<T> path, the post-addRow guard on executeWithKey, and the empty-row guard on addRow.


Summary
executeWithKey(Path<T>)andexecuteWithKey(Class<T>)toJPAInsertClauseandHibernateInsertClauseaddRow()andexecuteWithKeys(Path<T>) / executeWithKeys(Class<T>) → List<T>for multi-row INSERT with batched key return (single statement, single round-trip)Statement.RETURN_GENERATED_KEYSto retrieve auto-generated keysJpaNativeInsertSerializer(extendsSQLSerializer) for a single-pass SQL + constants build, ensuring function templates, params, and paths serialize correctlyJpaInsertNativeHelperutility for@Table/@Columnresolution and JDBC binding (single- and multi-row execution)doReturningWork()toSessionHolderinterface and all implementationsCloses #1692
Motivation
The SQL module's
SQLInsertClausesupportsexecuteWithKey()(andexecuteWithKeys()for batched inserts), but the JPA module does not. This forces JPA users to fall back toEntityManager.persist()+flush()for single-row inserts, and to afor-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:
After — stays in QueryDSL:
Implementation
Since JPA's
Query.executeUpdate()only returns affected row count, the implementation:@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 violatedSingle-pass serialization (regression fix)
The earlier draft built the SQL via a
JpaInsertNativeHelper.buildNativeInsertSQLstep while constants were extracted from a separateJPQLSerializerpass. The helper emitted one?per column without inspecting the value expression tree, so a function template likedbo.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:
JpaNativeInsertSerializerextendsSQLSerializerand produces the SQL and the constants list together. By reusing theSQLSerializervisitor, function templates, constants, params, and paths all serialize correctly.@Column/@Tableresolution follows the same pattern asNativeSQLSerializer, and plain?placeholders come fromSQLSerializer.serializeConstant's default behavior. The brokenbuildNativeInsertSQLpath is removed.Regression tests cover:
upper({0})+"value"→ DB stores"VALUE"SQLTemplatesOn adding SQL serialization to the JPA module
A fair concern was raised about expanding SQL-mapping responsibilities in the JPA module. Mitigations:
NativeSQLSerializeralready lives in this module; the new serializer reuses the same infrastructure.executeWithKey/executeWithKeyspath.execute()andtoString()still go throughJPQLSerializerunchanged.executeWithKey/executeWithKeysonly acceptConstant/Param/Pathvalues and explicitly reject anything else (no template support).Limitations
INSERT ... SELECTsubqueries are not supported (throwsUnsupportedOperationException) for bothexecuteWithKeyandexecuteWithKeysVALUES (..),(..)uses standard SQL syntax — Oracle'sINSERT ALLform would need a dialect branch (follow-up)getGeneratedKeys()(verified on H2; MySQL Connector/J 8+, PostgreSQL JDBC 42+ known to support this)@Table/@Columnannotations if using a custom HibernatePhysicalNamingStrategyJPAInsertClausecurrently relies on Hibernate as the JPA providerTest plan
JpaInsertNativeHelper(table name resolution, column name resolution, SQL generation, multi-row SQL generation)HibernateInsertClause.executeWithKey()(set style, columns/values style, class type, multiple inserts, column annotation, subquery rejection)JPAInsertClause.executeWithKey()(same scenarios)executeWithKeys()on both clauses (multi-row returns all keys in order, single-row returns size-1 list,executeWithKeyrejected afteraddRow,addRowrejected with no pending values)upper({0})+ constant, zero-arg templates, customSQLTemplatesidentifier quoting)executeWithKeysignature and behavior unchanged (internal serializer refactor delegates the legacy path to the multi-row implementation)./mvnw -Pdev verifypassesgit-code-formatpre-commit hook)