Skip to content

Fix: Improve handling of contains() on JPA collections mapped with @Converter to basic types#1199

Merged
velo merged 4 commits intoOpenFeign:masterfrom
chadongmin:fix/jpa-converter-collection-contains
Jun 9, 2025
Merged

Fix: Improve handling of contains() on JPA collections mapped with @Converter to basic types#1199
velo merged 4 commits intoOpenFeign:masterfrom
chadongmin:fix/jpa-converter-collection-contains

Conversation

@chadongmin
Copy link
Copy Markdown
Contributor

Related Issue(s): #173 querydsl/querydsl , #377 openfeign/querydsl

Problem Description

When using QueryDSL's collection.contains(element) method on a Java Collection-typed entity attribute that is mapped to a basic database type (e.g., String, TEXT) via a JPA @Converter, QueryDSL currently generates a standard JPQL MEMBER OF clause (often via an internal Ops.IN to JPQLOps.MEMBER_OF translation).

Historically, some older Hibernate versions might have been less strict in validating this JPQL against such converted attributes, leading some users (like H-Lo in issue #3618, #377) to believe this pattern "worked." However, as pointed out by contributors like OrangeDog, and in line with JPA principles, an attribute converted to a basic type is not treated as a queryable "plural path" by the persistence provider for standard collection operators like MEMBER OF.

Consequently, modern JPA providers, especially Hibernate 6 and later with its stricter validation, now consistently reject this JPQL, resulting in a runtime org.hibernate.query.SemanticException: Operand of 'member of' operator must be a plural path. This exception, often wrapped, originates глубоко from the ORM layer, making it less obvious to users why their QueryDSL code, which appears type-safe, is failing.

Justification for This Change in QueryDSL

While some users (like H-Lo) might have previously found workarounds or experienced a more lenient behavior from older Hibernate versions, the current stricter validation by Hibernate is aligned with JPA's interpretation of how @Converter to a basic type alters an attribute's nature for querying standard collection operations. The core issue is that QueryDSL currently generates JPQL that is predictably incompatible with how JPA providers handle such converted collections for MEMBER OF operations.

This PR proposes that QueryDSL should proactively address this known incompatibility for the following reasons:

  1. Enhanced User Experience & Clearer Diagnostics (Fail-Fast):

    • Instead of allowing users to encounter a potentially cryptic, ORM-specific SemanticException at runtime, QueryDSL can provide an immediate, clear, and QueryDSL-specific QueryException.
    • This exception will precisely explain why the operation is unsupported for that specific attribute mapping (@Converter on a collection to a basic type) and guide the user towards understanding the limitation and considering alternatives (like native queries for DB-specific functions or re-evaluating their mapping if collection querying is crucial).
  2. Improved Abstraction and Predictability:

    • QueryDSL serves as an abstraction layer. By detecting this specific pitfall, QueryDSL upholds its role by shielding users from underlying ORM intricacies that lead to predictable failures.
    • It makes QueryDSL's behavior more predictable: if an operation is known to be unsupported by the persistence layer for a given mapping, QueryDSL should ideally signal this at its level.
  3. Alignment with Stricter ORM Validations:

    • As ORMs like Hibernate evolve and enforce stricter adherence to specifications or improve their validation, QueryDSL should also adapt to prevent the generation of queries that will no longer be accepted. This change makes QueryDSL a better citizen in the modern JPA ecosystem.
  4. Guidance Over Silent Failure or Unexpected Behavior:

    • Even if older Hibernate versions didn't throw an error, it's questionable, as OrangeDog pointed out, whether the MEMBER OF operation on a TEXT column (without database-specific functions) ever produced the semantically correct SQL for a true collection membership check. It might have "not failed" but not worked as truly intended either.
    • Providing a clear error is better than QueryDSL generating a query that either fails cryptically or, in hypothetical lenient scenarios, produces SQL that doesn't match the user's intent for a collection contains check.

This change is not about restoring a "broken" functionality that was once correctly supported, but rather about QueryDSL taking responsibility for generating valid and semantically appropriate queries for the given context, or clearly indicating when it cannot. This PR opts for the latter by providing explicit, early feedback.

Solution Implemented

This PR modifies the JPQLSerializer (specifically the visitAnyInPath method, which is involved when collection.contains() is translated from an Ops.IN operation on a collection path) in the querydsl-jpa module.

The changes are as follows:

  1. Detection of Problematic Mapping:

    • A new helper method, isCollectionPathWithConverterToBasicType(Path<?> path, jakarta.persistence.metamodel.Metamodel metamodel), uses the JPA Metamodel to determine if a given QueryDSL Path<?> corresponds to an entity attribute that is a Java Collection but whose PersistentAttributeType is BASIC (indicating a JPA @Converter to a basic database type).
  2. Early Exception (Fail-Fast):

    • Within visitAnyInPath, before calling super.visitOperation to generate the MEMBER OF clause, this check is performed.
    • If the problematic mapping is detected, QueryDSL now throws a com.querydsl.core.QueryException with an informative message.
  3. Normal Behavior for Valid Cases:

    • If the check determines the mapping is not problematic, the existing logic proceeds, ensuring normally mapped collections are unaffected.

Benefits of the Change

  • Improved User Experience: Clear, QueryDSL-specific error message indicating the incompatibility.
  • Fail-Fast: Problems identified earlier during JPQL serialization.
  • Increased Robustness: QueryDSL handles this specific JPA mapping pattern more gracefully.

How to Test

  • A new test case, JPAQueryConverterCollectionContainsTest.java, has been added.
  • It defines an entity with a Set<UserRoleInTest> attribute mapped using a custom @Converter (to a comma-delimited String).
  • The test method rolesContains_ShouldThrowQueryDSLException_WhenConverterIsUsedOnCollection asserts that a .contains() query on this path now throws the new com.querydsl.core.QueryException with the expected message.

Possible Future Enhancements (Out of Scope for this PR)

While this PR focuses on better diagnostics, future work could explore support for querying such converted collections via database-specific functions, if a generic and maintainable approach within QueryDSL is feasible. This PR provides the immediate benefit of clear error reporting for currently unsupported standard operations.


@chadongmin chadongmin force-pushed the fix/jpa-converter-collection-contains branch from 4db5be4 to ecc95b0 Compare June 4, 2025 10:16
@codecov
Copy link
Copy Markdown

codecov bot commented Jun 4, 2025

Codecov Report

Attention: Patch coverage is 61.29032% with 12 lines in your changes missing coverage. Please review.

Project coverage is 71.03%. Comparing base (63b4a04) to head (5c28ae7).

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...src/main/java/com/querydsl/jpa/JPQLSerializer.java 61.29% 5 Missing and 7 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1199      +/-   ##
============================================
+ Coverage     61.21%   71.03%   +9.82%     
+ Complexity       84       60      -24     
============================================
  Files           832      858      +26     
  Lines         32025    32389     +364     
  Branches       3590     3608      +18     
============================================
+ Hits          19603    23008    +3405     
+ Misses        11171     8150    -3021     
+ Partials       1251     1231      -20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@chadongmin chadongmin force-pushed the fix/jpa-converter-collection-contains branch 5 times, most recently from e67fefd to 2a7f839 Compare June 4, 2025 12:28
@chadongmin chadongmin force-pushed the fix/jpa-converter-collection-contains branch from 2a7f839 to 189aa79 Compare June 4, 2025 12:51
Comment on lines +637 to +641
type,
(operator == Ops.IN
? com.querydsl.jpa.JPQLOps.MEMBER_OF
: com.querydsl.jpa.JPQLOps.NOT_MEMBER_OF),
args);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid this code style change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback.
I’ve reverted the code style change to keep it consistent with the original formatting. 🙇‍♂️

@chadongmin chadongmin force-pushed the fix/jpa-converter-collection-contains branch 2 times, most recently from 34c7948 to 0b7cd58 Compare June 9, 2025 14:29
@velo velo merged commit 7876d3d into OpenFeign:master Jun 9, 2025
5 of 6 checks passed
velo added a commit that referenced this pull request Jun 9, 2025
* Add `TypeWrapperFactoryExpression` for Type-Safe Custom Number Mapping in Querydsl Aggregations (#1181)

* Introduce TypeWrapper to wrap Expression results into custom types

- Implement TypeWrapper<S, T> as a FactoryExpression
- Allows converting a source Expression (e.g. BigDecimal) to a domain type (e.g. Money)
- Prepares QueryDSL core to support custom aggregation projections without core API changes

* Add JPAQueryCustomTypeWrapperTest covering success and failure scenarios

- Verify IllegalArgumentException for unsupported custom types without wrapper
- Verify sum-and-wrap to Money via TypeWrapper in both direct and DTO projection use cases
- Ensure both positive and negative paths are exercised

* Fix: Improve handling of `contains()` on JPA collections mapped with `@Converter` to basic types (#1199)

* Fix: Early error for .contains() on @converter mapped JPA collections

* Test : add test code

* Add test cases to improve test coverage

* style: revert formatting change based on review feedback for consistency

* fix: correct escape & anchor handling in likeToRegex/regexToLike (#1174)

* fix: correct escape & anchor handling in likeToRegex/regexToLike
- treat \% \_ as literals, escape regex metachars, strip outer ^/$ only
- add unit tests to guarantee accurate LIKE ↔ regex round-trips

* Fix code format

Signed-off-by: Marvin Froeder <[email protected]>

---------

Signed-off-by: Marvin Froeder <[email protected]>
Co-authored-by: Marvin Froeder <[email protected]>

---------

Signed-off-by: Marvin Froeder <[email protected]>
Co-authored-by: 차동민 <[email protected]>
Co-authored-by: renechoi <[email protected]>
Co-authored-by: Marvin Froeder <[email protected]>
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.

2 participants