Skip to content

Commit

Permalink
Merge pull request #30581 from njr-11/28366-single-result-of-ElementC…
Browse files Browse the repository at this point in the history
…ollection-workaround

workarounds for selecting ElementCollection attributes
  • Loading branch information
njr-11 authored Jan 21, 2025
2 parents dac6134 + a363dac commit 56d85ff
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1016,3 +1016,37 @@ CWWKD1101.attr.subset.mismatch.explanation=When Java records are used to \
CWWKD1101.attr.subset.mismatch.useraction=Rename the record components to \
match valid entity attribute names, or use the Select annotation to explicitly \
request entity attributes by their name.

CWWKD1102.incompat.query.result=CWWKD1102E: The {0} method of the {1} repository \
has the {2} return type, but the Jakarta Persistence provider performed a \
query with results of the {3} type. Check whether the Jakarta Persistence \
provider allows querying for the types of entity attributes that the {0} method \
selects. Check for other causes of incompatibility if the Jakarta provider does \
allow querying for the types of entity attributes that the {0} method selects.
CWWKD1102.incompat.query.result.explanation=Jakarta Persistence providers do not \
allow returning all types of entity attributes. If the Jakarta provider does \
return the type of entity attributes that the method selected, another cause \
of incompatibility exists. For example, the Jakarta Persistence provider might \
have a bug or the repository method signature might have an error.
CWWKD1102.incompat.query.result.useraction=If the type of entity attributes that \
the method selects is not allowed, modify the query to retrieve the whole entity \
or a different set of entity attributes that the Jakarta Persistence provider \
allows. If the type of entity attributes that the method selects is allowed, \
check for other causes of incompatibility.

CWWKD1103.incompat.query.result=CWWKD1103E: The {0} method of the {1} repository \
has the {2} return type, but the Jakarta Persistence provider performed a \
{3} query with results of the {4} type. Check whether the Jakarta Persistence \
provider allows querying for the types of entity attributes that the {0} method \
selects. Check for other causes of incompatibility if the Jakarta provider does \
allow querying for the types of entity attributes that the {0} method selects.
CWWKD1103.incompat.query.result.explanation=Jakarta Persistence providers do not \
allow returning all types of entity attributes. If the Jakarta provider does \
return the type of entity attributes that the method selected, another cause \
of incompatibility exists. For example, the Jakarta Persistence provider might \
have a bug or the repository method signature might have an error.
CWWKD1103.incompat.query.result.useraction=If the type of entity attributes that \
the method selects is not allowed, modify the query to retrieve the whole entity \
or a different set of entity attributes that the Jakarta Persistence provider \
allows. If the type of entity attributes that the method selects is allowed, \
check for other causes of incompatibility.
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,22 @@ public static enum Type {
final Class<?> returnArrayType;

/**
* The type of a single result obtained by the query.
* For example, a single result of a query that returns List<MyEntity> is of the type MyEntity.
* The type of a single result obtained by the query. For example,
* A query that returns List<MyEntity> has singleType MyEntity.
* A query that returns List<ArrayList<String>> has singleType ArrayList<String>.
* A query that returns Optional<String[]> has singleType String[].
*/
final Class<?> singleType;

/**
* Element type of singleType when singleType is an array or collection.
* Null if singleType is not an array or collection. For example,
* A query that returns List<MyEntity> has singleTypeElementType null.
* A query that returns List<ArrayList<String>> has singleTypeElementType String.
* A query that returns Optional<String[]> has singleTypeElementType String.
*/
final Class<?> singleTypeElementType;

/**
* Positions of Sort, Sort[], and Order parameters.
* When there are no parameters specifying sort criteria dynamically,
Expand Down Expand Up @@ -265,25 +276,6 @@ public static enum Type {
*/
boolean validateResult;

/**
* Constructor for the withJPQL method.
*/
private QueryInfo(Class<?> repositoryInterface,
Method method,
Class<?> entityParamType,
boolean isOptional,
Class<?> multiType,
Class<?> returnArrayType,
Class<?> singleType) {
this.method = method;
this.entityParamType = entityParamType;
this.isOptional = isOptional;
this.multiType = multiType;
this.repositoryInterface = repositoryInterface;
this.returnArrayType = returnArrayType;
this.singleType = singleType;
}

/**
* Construct partially complete query information.
*
Expand Down Expand Up @@ -374,11 +366,18 @@ public QueryInfo(Class<?> repositoryInterface,

singleType = type;

if ((singleType.isArray() || Iterable.class.isAssignableFrom(singleType)) &&
++d < depth)
singleTypeElementType = returnTypeAtDepth.get(d);
else
singleTypeElementType = null;

if (trace && tc.isEntryEnabled())
Tr.exit(this, tc, "<init>", new Object[] { this,
"result isOptional? " + isOptional,
"result multiType: " + multiType,
"result singleType: " + singleType });
"result singleType: " + singleType,
" element: " + singleTypeElementType });
}

/**
Expand All @@ -392,9 +391,43 @@ public QueryInfo(Class<?> repositoryInterface, Method method, Type type) {
this.repositoryInterface = repositoryInterface;
this.returnArrayType = null;
this.singleType = null;
this.singleTypeElementType = null;
this.type = type;
}

/**
* Construct a copy of a source QueryInfo, but with different JPQL and sorts.
*
* @param source QueryInfo from which to copy.
* @param jpql JPQL to use instead of the JPQL from source.
* @param sorts Sorts to use instead of the sorts from source.
*/
QueryInfo(QueryInfo source, String jpql, List<Sort<Object>> sorts) {
entityInfo = source.entityInfo;
entityParamType = source.entityParamType;
entityVar = source.entityVar;
entityVar_ = source.entityVar_;
hasWhere = source.hasWhere;
isOptional = source.isOptional;
this.jpql = jpql;
jpqlAfterCursor = source.jpqlAfterCursor;
jpqlBeforeCursor = source.jpqlBeforeCursor;
jpqlCount = source.jpqlCount;
jpqlDelete = source.jpqlDelete;
jpqlParamCount = source.jpqlParamCount;
jpqlParamNames = source.jpqlParamNames;
maxResults = source.maxResults;
method = source.method;
multiType = source.multiType;
repositoryInterface = source.repositoryInterface;
returnArrayType = source.returnArrayType;
singleType = source.singleType;
singleTypeElementType = source.singleTypeElementType;
this.sorts = sorts;
type = source.type;
validateParams = source.validateParams;
}

/**
* Adds Sort criteria to the end of the tracked list of sort criteria.
*
Expand Down Expand Up @@ -578,7 +611,7 @@ int computeOffset(PageRequest pagination) {
* return type.
*
* @param value value to convert.
* @param type type to convert to.
* @param toType type to convert to.
* @param failIfNotConverted whether or not to fail if unable to convert.
* @return converted value.
*/
Expand Down Expand Up @@ -752,6 +785,12 @@ else if (value instanceof CharSequence) {
else if ("false".equalsIgnoreCase(str))
return false;
}
} else if (value instanceof List &&
Iterable.class.isAssignableFrom(toType)) {
return convertToIterable((List<?>) value,
toType,
singleTypeElementType,
null);
}

if (failIfNotConverted) {
Expand Down Expand Up @@ -796,15 +835,26 @@ private void convertFail(Number value, long min, long max) {
* Convert the results list into an Iterable of the specified type.
*
* @param results results of a find or save operation.
* @param elementType the type of each element if a find operation.
* Can be NULL if a save operation.
* @param iterableType the desired type of Iterable.
* @param elementType the type of each element, or null.
* Always null if not a find operation.
* @param query the query if available.
* Always null if not a find operation.
* @return results converted to an Iterable of the specified type.
*/
@Trivial
final Iterable<?> convertToIterable(List<?> results,
Class<?> iterableType,
Class<?> elementType,
Class<?> iterableType) {
jakarta.persistence.Query query) {
final boolean trace = TraceComponent.isAnyTracingEnabled();
if (trace && tc.isEntryEnabled())
Tr.entry(this, tc, "convertToIterable",
loggable(results),
"to " + iterableType.getName(),
elementType == null ? "of ?" : ("of " + elementType.getName()),
query);

Collection<Object> list;
if (iterableType.isInterface()) {
if (iterableType.isAssignableFrom(ArrayList.class))
Expand Down Expand Up @@ -856,13 +906,26 @@ else if (iterableType.isAssignableFrom(LinkedHashSet.class))
Object[] a = (Object[]) results.get(0);
for (int i = 0; i < a.length; i++) {
Object element = a[i];
if (!elementType.isInstance(element))
if (elementType != null && !elementType.isInstance(element))
element = convert(element, elementType, true);
list.add(element);
}
} else {
list.addAll(results);
for (Object element : results) {
if (elementType != null && !elementType.isInstance(element)) {
Object converted = convert(element, elementType, false);
// EclipseLink returns wrong values when selecting
// ElementCollection attributes instead of rejecting it as
// unsupported. Raise an error instead.
if (converted == element)
throw excIncompatibleQueryResult(results, query);
}
list.add(element);
}
}

if (trace && tc.isEntryEnabled())
Tr.exit(this, tc, "convertToIterable", loggable(list));
return list;
}

Expand Down Expand Up @@ -1045,6 +1108,40 @@ private MappingException excExtraMethodArgNamedParams(Set<String> extras,
method.getAnnotation(Query.class).value());
}

/**
* Constructs an UnsupportedOperationException for an error where the
* repository method return type does not match the query results.
* On reason this might happen is when EclipseLink returns wrong values
* when selecting ElementCollection attributes instead of rejecting
* it as unsupported.
*
* @param results list of at least 1 result.
* @param query jakarta.persistence.Query, a String, or null.
* @return UnsupportedOperationException.
*/
@Trivial
UnsupportedOperationException excIncompatibleQueryResult(List<?> results,
Object query) {
String r = results.getClass().getName() +
"<" + results.get(0).getClass().getName() + ">";

if (query == null)
return exc(UnsupportedOperationException.class,
"CWWKD1102.incompat.query.result",
method.getName(),
repositoryInterface.getName(),
method.getGenericReturnType().getTypeName(),
r);
else
return exc(UnsupportedOperationException.class,
"CWWKD1103.incompat.query.result",
method.getName(),
repositoryInterface.getName(),
method.getGenericReturnType().getTypeName(),
query instanceof String ? query : query.getClass().getName(),
r);
}

/**
* Check if the cause of the lacking named parameter is a mispositioned
* special parameter. If so, raises UnsupportedOperationException.
Expand Down Expand Up @@ -1275,7 +1372,7 @@ else if (multiType.isInstance(results))
else if (Stream.class.equals(multiType))
returnValue = results.stream();
else if (Iterable.class.isAssignableFrom(multiType))
returnValue = convertToIterable(results, null, multiType);
returnValue = convertToIterable(results, multiType, null, null);
else if (Iterator.class.equals(multiType))
returnValue = results.iterator();
else
Expand Down Expand Up @@ -3763,7 +3860,7 @@ else if (multiType.isInstance(results))
else if (Stream.class.equals(multiType))
returnValue = results.stream();
else if (Iterable.class.isAssignableFrom(multiType))
returnValue = convertToIterable(results, null, multiType);
returnValue = convertToIterable(results, multiType, null, null);
else if (Iterator.class.equals(multiType))
returnValue = results.iterator();
else
Expand Down Expand Up @@ -4263,7 +4360,7 @@ else if (multiType.isInstance(results))
else if (Stream.class.equals(multiType))
returnValue = results.stream();
else if (Iterable.class.isAssignableFrom(multiType))
returnValue = convertToIterable(results, null, multiType);
returnValue = convertToIterable(results, multiType, null, null);
else if (Iterator.class.equals(multiType))
returnValue = results.iterator();
else
Expand Down Expand Up @@ -5043,35 +5140,4 @@ void validateSort(Sort<?> sort) {
repositoryInterface.getName());
}
}

/**
* Copy of query information, but with updated JPQL and sort criteria.
*/
QueryInfo withJPQL(String jpql, List<Sort<Object>> sorts) {
QueryInfo q = new QueryInfo( //
repositoryInterface, //
method, //
entityParamType, //
isOptional, //
multiType, //
returnArrayType, //
singleType);
q.entityInfo = entityInfo;
q.entityVar = entityVar;
q.entityVar_ = entityVar_;
q.hasWhere = hasWhere;
q.jpql = jpql;
q.jpqlAfterCursor = jpqlAfterCursor;
q.jpqlBeforeCursor = jpqlBeforeCursor;
q.jpqlCount = jpqlCount;
q.jpqlDelete = jpqlDelete;
q.maxResults = maxResults;
q.jpqlParamCount = jpqlParamCount;
q.jpqlParamNames = jpqlParamNames;
q.sorts = sorts;
q.type = type;
q.validateParams = validateParams;
q.validateParams = validateResult;
return q;
}
}
Loading

0 comments on commit 56d85ff

Please sign in to comment.