Summary
A derived MongoRepository query returning Set<T> works in regular JVM mode but fails during AOT compilation.
The generated implementation returns ExecutableFindOperation.TerminatingResults#all(), which produces List<T>, without adapting it to the declared Set<T> return type.
Minimal reproduction project
https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer
Environment
- Spring Boot: 4.1.0
- Spring Data MongoDB: 5.1.0
- Java: 25
- Gradle: 9.5.1
- GraalVM Build Tools: 1.1.1
Steps to reproduce
git clone https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer.git
cd spring-data-mongodb-aot-set-reproducer
./gradlew clean compileAotJava
A MongoDB instance is not required because the failure occurs while compiling the generated AOT sources.
The repository declares:
public interface PersonRepository extends MongoRepository<Person, String> {
Set<Person> findAllByName(String name);
}
Expected behavior
The AOT-generated repository should adapt the query result to Set<Person> and compile successfully, matching regular non-AOT repository behavior.
Actual behavior
AOT generates:
public Set<Person> findAllByName(String name) {
// ...
return finder.matching(filterQuery).all();
}
Compilation fails:
PersonRepositoryImpl__AotRepository.java:34: error:
incompatible types: List<Person> cannot be converted to Set<Person>
return finder.matching(filterQuery).all();
^
Workaround
Change the repository return type to List<T> and convert it to a Set in application code when required.
Additional context
The AOT generator selects all() for collection queries:
|
String terminatingMethod; |
|
|
|
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { |
|
terminatingMethod = "all()"; |
|
} else if (query.isCount()) { |
|
terminatingMethod = "count()"; |
|
} else if (query.isExists()) { |
|
terminatingMethod = "exists()"; |
|
} else if (queryMethod.isStreamQuery()) { |
|
terminatingMethod = "stream()"; |
|
} else { |
|
if (query.getQuery().isLimited()) { |
|
terminatingMethod = Optional.class.isAssignableFrom(methodReturn.toClass()) ? "first()" |
|
: "firstValue()"; |
|
} else { |
|
terminatingMethod = Optional.class.isAssignableFrom(methodReturn.toClass()) ? "one()" |
|
: "oneValue()"; |
|
} |
|
} |
|
|
|
if (queryMethod.isPageQuery()) { |
|
|
|
builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"), |
|
context.getPageableParameterName(), query.name()); |
|
} else if (queryMethod.isSliceQuery()) { |
|
builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class, |
|
context.localVariable("finder"), context.getPageableParameterName(), query.name()); |
|
} else if (queryMethod.isScrollQuery()) { |
|
|
|
String scrollPositionParameterName = context.getScrollPositionParameterName(); |
|
|
|
if (scrollPositionParameterName != null) { |
|
|
|
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), |
|
scrollPositionParameterName); |
|
} else { |
|
String pageableParameterName = context.getPageableParameterName(); |
|
if (pageableParameterName != null) { |
|
builder.addStatement("return $L.matching($L).scroll($L.toScrollPosition())", |
|
context.localVariable("finder"), query.name(), pageableParameterName); |
|
} else { |
|
builder.addStatement("return $L.matching($L).scroll($T.initial())", context.localVariable("finder"), |
|
query.name(), ScrollPosition.class); |
|
} |
|
} |
|
} else { |
|
|
|
if (query.isCount() && !ClassUtils.isAssignable(Long.class, methodReturn.getActualReturnClass())) { |
|
|
|
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); |
|
builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class, |
|
context.localVariable("finder"), query.name(), terminatingMethod, returnType); |
|
|
|
} else { |
|
|
|
CodeBlock resultBlock = CodeBlock.of("$L.matching($L).$L", context.localVariable("finder"), query.name(), |
|
terminatingMethod); |
|
|
|
builder.addStatement("return $L", MongoCodeBlocks.potentiallyWrapStreamable(methodReturn, resultBlock)); |
all() returns List<T>:
|
|
|
/** |
|
* Get all matching elements. |
|
* |
|
* @return never {@literal null}. |
|
*/ |
|
List<T> all(); |
The result adaptation handles Streamable, but not Set:
|
/** |
|
* Wraps the given {@link CodeBlock} representing an {@link Iterable} into a {@link Streamable} if the |
|
* {@link MethodReturn} indicates so. |
|
*/ |
|
public static CodeBlock potentiallyWrapStreamable(MethodReturn methodReturn, CodeBlock returningIterable) { |
|
|
|
Class<?> returnType = methodReturn.toClass(); |
|
|
|
if (returnType.equals(Streamable.class)) { |
|
return CodeBlock.of("$T.of($L)", Streamable.class, returningIterable); |
|
} |
|
|
|
if (ClassUtils.isAssignable(Streamable.class, returnType)) { |
|
|
|
return CodeBlock.of( |
|
"($1T) $2T.getSharedInstance().convert($3T.of($4L), $5T.valueOf($3T.class), $5T.valueOf($1T.class))", |
|
returnType, DefaultConversionService.class, Streamable.class, returningIterable, TypeDescriptor.class); |
|
} |
|
|
|
return returningIterable; |
A similar issue was fixed for Spring Data JPA:
spring-projects/spring-data-jpa#4094
Summary
A derived
MongoRepositoryquery returningSet<T>works in regular JVM mode but fails during AOT compilation.The generated implementation returns
ExecutableFindOperation.TerminatingResults#all(), which producesList<T>, without adapting it to the declaredSet<T>return type.Minimal reproduction project
https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer
Environment
Steps to reproduce
git clone https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer.git cd spring-data-mongodb-aot-set-reproducer ./gradlew clean compileAotJavaA MongoDB instance is not required because the failure occurs while compiling the generated AOT sources.
The repository declares:
Expected behavior
The AOT-generated repository should adapt the query result to
Set<Person>and compile successfully, matching regular non-AOT repository behavior.Actual behavior
AOT generates:
Compilation fails:
Workaround
Change the repository return type to
List<T>and convert it to aSetin application code when required.Additional context
The AOT generator selects
all()for collection queries:spring-data-mongodb/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java
Lines 102 to 160 in 702933c
all()returnsList<T>:spring-data-mongodb/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
Lines 139 to 145 in 702933c
The result adaptation handles
Streamable, but notSet:spring-data-mongodb/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
Lines 239 to 258 in 702933c
A similar issue was fixed for Spring Data JPA:
spring-projects/spring-data-jpa#4094