Skip to content

Auto-convert @Enumerated values in native JPA INSERT paths (#1883) - #1884

Open
zio0911 wants to merge 1 commit into
OpenFeign:masterfrom
zio0911:feature/jpa-insert-enum-auto-convert-1883
Open

Auto-convert @Enumerated values in native JPA INSERT paths (#1883)#1884
zio0911 wants to merge 1 commit into
OpenFeign:masterfrom
zio0911:feature/jpa-insert-enum-auto-convert-1883

Conversation

@zio0911

@zio0911 zio0911 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1883.

JPAInsertClause / HibernateInsertClause route through a native SQL path for executeWithKey(...), executeWithKeys(...), and multi-row execute() (accumulated via addRow()). On that path, enum values passed to set(EnumPath<E>, E) or values(E) were handed to PreparedStatement.setObject(...) as-is, so how they got stored depended entirely on the JDBC driver — MySQL usually stored .name() by accident, but H2 / HSQLDB / other drivers could fail or store the wrong representation.

The native path now inspects the target column's @Enumerated annotation and converts enum values before binding.

Behavioral changes

  • @Enumerated(EnumType.STRING) → bind enum.name()
  • @Enumerated(EnumType.ORDINAL) → bind enum.ordinal()
  • Enum field with no @Enumerated → bind enum.ordinal() (JPA specification default)
  • @Convert(converter = ...) on an enum field → IllegalStateException with a message pointing the caller to convert the value themselves at the call site (custom AttributeConverter instances cannot be honored on a native path that bypasses JPA)

Conversion is applied uniformly to both set(path, value) and columns(...).values(value) call shapes, and to every row of a multi-row INSERT accumulated via addRow(). The JPQL path (single-row execute() without templates, UPDATE, DELETE, SELECT) is unchanged — Hibernate already handles enum conversion there.

Call site before / after

Before — cast tricks and manual conversion at every call site:

QLogExportJobEntity q = QLogExportJobEntity.logExportJobEntity;
Path<String> jobType = (Path<String>) (Path<?>) q.jobType;
Path<String> status = (Path<String>) (Path<?>) q.status;
Long id = queryFactory.insert(q)
    .set(jobType, entity.getJobType().name())
    .set(status, entity.getStatus().name())
    ...
    .executeWithKey(q.jobId);

After — natural, type-safe call:

QLogExportJobEntity q = QLogExportJobEntity.logExportJobEntity;
Long id = queryFactory.insert(q)
    .set(q.jobType, entity.getJobType())    // auto-converted to name()
    .set(q.status, entity.getStatus())      // auto-converted to name()
    ...
    .executeWithKey(q.jobId);

Scope

In scope

  • JPAInsertClause / HibernateInsertClause on the native SQL execution path:
    • executeWithKey(...) / executeWithKeys(...)
    • execute() when it routes to the native path (multi-row via addRow(), and single-row when the JPQL path is bypassed for template-value INSERTs)
  • Both set(path, value) and columns(...).values(value) call shapes
  • Multi-row inserts accumulated via addRow()

Out of scope

  • JPQL INSERT / UPDATE / DELETE / SELECT — Hibernate already handles enum conversion there
  • Reading enum values back from result sets (all read paths use JPQL / Hibernate)
  • @Convert(converter = ...) on enum fields — a clear runtime error steers the caller to convert the value at the call site
  • querydsl-sql (non-JPA) — separate module, separate binding pipeline

Design notes

  • The mapping is resolved by inspecting Path.getAnnotatedElement() for @Enumerated and @Convert, using the same reflection surface JpaNativeInsertSerializer already uses to resolve @Column names. No new dependency on Hibernate-specific APIs.
  • Conversion is a small pure step applied to the value list right after effectiveValues(...) and just before serialization / binding, so all four executors share a single implementation.
  • Non-enum values, expression templates that do not resolve to a bare Enum, and columns whose target does not expose an annotated element are returned unchanged — no regression on existing paths.

Test plan

New tests in JPAExecuteWithKeyTest and HibernateExecuteWithKeyTest:

  • @Enumerated(EnumType.STRING) — stored as name()
  • @Enumerated(EnumType.ORDINAL) — stored as ordinal() (single row, multi-row via addRow(), and columns()/values() style)
  • Enum field with no @Enumerated — stored as ordinal() (JPA default)
  • Mixed STRING + ORDINAL enum columns in a single row — each is converted according to its own annotation
  • @Convert(converter = ...) on an enum field — IllegalStateException with an actionable message
  • @Convert workaround (converter output supplied at the call site) — continues to work
  • A second enum (Priority) reinforces that STRING/ORDINAL branches are generic across enum types, not specific to the Status enum used in the primary tests

Regression: ./mvnw -pl querydsl-libraries/querydsl-jpa -Pno-databases test — 437 tests pass, 0 failures.

…#1883)

Native INSERT paths (executeWithKey, executeWithKeys, multi-row execute)
were passing enum values straight to PreparedStatement.setObject(...),
leaving conversion up to the JDBC driver — MySQL usually stored .name()
by accident, but other drivers fail or store the wrong representation.

The native path now inspects the target column's @Enumerated annotation
and converts enum values accordingly before binding:

  - @Enumerated(EnumType.STRING)  -> bind enum.name()
  - @Enumerated(EnumType.ORDINAL) -> bind enum.ordinal()
  - Enum field with no @Enumerated -> bind enum.ordinal() (JPA default)
  - @convert(converter=...) on an enum field -> IllegalStateException
    with an actionable message; custom AttributeConverters cannot be
    honored on a native path that bypasses JPA.

Conversion is applied uniformly to both set()-style and columns()/values()
call shapes, and to every row of a multi-row INSERT accumulated via
addRow(). The JPQL path (single-row execute() without templates, UPDATE,
DELETE, SELECT) is unchanged — Hibernate already handles enum conversion
there.

Adds test coverage in JPAExecuteWithKeyTest and HibernateExecuteWithKeyTest
for STRING/ORDINAL/default/@convert cases, both call shapes, and the
multi-row addRow() path. GeneratedKeyEntity gains nullable enum columns
so the additions are backwards-compatible with existing tests, and a
top-level GeneratedKeyStatusCodeConverter is added so EclipseLink can
discover the converter during persistence-unit predeployment.
@zio0911
zio0911 force-pushed the feature/jpa-insert-enum-auto-convert-1883 branch from e89d73e to d98ccc7 Compare August 11, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-convert @Enumerated values in native JPA INSERT paths (executeWithKey, executeWithKeys, multi-row execute)

2 participants