diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java index 24dee1ae5375c..7c64d67a2c2e7 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java @@ -160,7 +160,7 @@ public final class DotNames { // https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#basic // Should be in sync with org.hibernate.type.BasicTypeRegistry public static final Set HIBERNATE_PROVIDED_BASIC_TYPES = new HashSet<>(Arrays.asList( - STRING, CLASS, + STRING, CLASS, LIST, BOOLEAN, PRIMITIVE_BOOLEAN, INTEGER, PRIMITIVE_INTEGER, LONG, PRIMITIVE_LONG, diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java index 29e0131fb2b9f..7f02d18d66544 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java @@ -338,8 +338,28 @@ private int indexOfOrMaxValue(String methodName, String term) { } /** - * See: - * https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions + * Resolves a nested field within an entity class based on a given field path expression. + * This method traverses through the entity class and potentially its related classes, + * identifying and returning the appropriate field. It handles complex field paths that may + * include multiple levels of nested fields, separated by underscores ('_'). + * + * See: + * * https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions + * + * @param repositoryMethodDescription A description of the repository method, + * typically used for error reporting. + * @param fieldPathExpression The expression representing the path of the field within + * the entity class. Fields at different levels of nesting + * should be separated by underscores ('_'). + * @param fieldPathBuilder A StringBuilder used to construct and return the resolved field path. + * It will contain the fully qualified field path once the method completes. + * @return The {@link FieldInfo} object representing the resolved field. If the field cannot be resolved, + * an exception is thrown. + * @throws UnableToParseMethodException If the field cannot be resolved from the given + * field path expression, this exception is thrown + * with a detailed error message. + * @throws IllegalStateException If the resolved entity class referenced by the field is not found + * in the Quarkus index, or if a typed field could not be resolved properly. */ private FieldInfo resolveNestedField(String repositoryMethodDescription, String fieldPathExpression, StringBuilder fieldPathBuilder) { @@ -353,19 +373,30 @@ private FieldInfo resolveNestedField(String repositoryMethodDescription, String MutableReference> parentSuperClassInfos = new MutableReference<>(); int fieldStartIndex = 0; + ClassInfo parentFieldInfo = null; while (fieldStartIndex < fieldPathExpression.length()) { + // the underscore character is treated as reserved character to manually define traversal points. + // This means that path expression may have multiple levels separated by the '_' character. For example: person_address_city. if (fieldPathExpression.charAt(fieldStartIndex) == '_') { + //See issue #34395 + // For resolving correctly nested fields added using '_' we need to get the previous fieldInfo which will be the class containing the field starting by '_' in this loop. + DotName parentFieldInfoName; + if (fieldInfo != null && fieldInfo.type().kind() == Type.Kind.PARAMETERIZED_TYPE) { + parentFieldInfoName = fieldInfo.type().asParameterizedType().arguments().stream().findFirst().get().name(); + parentFieldInfo = indexView.getClassByName(parentFieldInfoName); + } fieldStartIndex++; if (fieldStartIndex >= fieldPathExpression.length()) { throw new UnableToParseMethodException(fieldNotResolvableMessage + offendingMethodMessage); } } - // the underscore character is treated as reserved character to manually define traversal points. int firstSeparator = fieldPathExpression.indexOf('_', fieldStartIndex); int fieldEndIndex = firstSeparator == -1 ? fieldPathExpression.length() : firstSeparator; while (fieldEndIndex >= fieldStartIndex) { - String simpleFieldName = lowerFirstLetter(fieldPathExpression.substring(fieldStartIndex, fieldEndIndex)); - fieldInfo = getFieldInfo(simpleFieldName, parentClassInfo, parentSuperClassInfos); + String fieldName = fieldPathExpression.substring(fieldStartIndex, fieldEndIndex); + String simpleFieldName = lowerFirstLetter(fieldName); + fieldInfo = getFieldInfo(simpleFieldName, parentFieldInfo == null ? parentClassInfo : parentFieldInfo, + parentSuperClassInfos); if (fieldInfo != null) { break; } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java index 6f11148faf8d3..6c3b27f163254 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java @@ -17,6 +17,10 @@ import org.jboss.jandex.MethodInfo; import org.junit.jupiter.api.Test; +import io.quarkus.spring.data.deployment.generics.ChildBase; +import io.quarkus.spring.data.deployment.generics.ParentBase; +import io.quarkus.spring.data.deployment.generics.ParentBaseRepository; + public class MethodNameParserTest { private final Class repositoryClass = PersonRepository.class; @@ -95,6 +99,32 @@ public void testFindAllBy_() throws Exception { assertThat(exception).hasMessageContaining("Person does not contain a field named: _"); } + @Test + public void testGenericsWithWildcard() throws Exception { + Class[] additionalClasses = new Class[] { ChildBase.class }; + + MethodNameParser.Result result = parseMethod(ParentBaseRepository.class, "countParentsByChildren_Nombre", + ParentBase.class, + additionalClasses); + assertThat(result).isNotNull(); + assertSameClass(result.getEntityClass(), ParentBase.class); + assertThat(result.getQuery()).isEqualTo("FROM ParentBase WHERE children.nombre = ?1"); + assertThat(result.getParamCount()).isEqualTo(1); + } + + @Test + public void shouldParseRepositoryMethodOverEntityContainingACollection() throws Exception { + Class[] additionalClasses = new Class[] { LoginEvent.class }; + + MethodNameParser.Result result = parseMethod(UserRepository.class, "countUsersByLoginEvents_Id", + User.class, + additionalClasses); + assertThat(result).isNotNull(); + assertSameClass(result.getEntityClass(), User.class); + assertThat(result.getQuery()).isEqualTo("FROM User WHERE loginEvents.id = ?1"); + assertThat(result.getParamCount()).isEqualTo(1); + } + private AbstractStringAssert assertSameClass(ClassInfo classInfo, Class aClass) { return assertThat(classInfo.name().toString()).isEqualTo(aClass.getName()); } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/UserRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/UserRepository.java index 61d15169d25e9..caef9e4231c8c 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/UserRepository.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/UserRepository.java @@ -28,4 +28,6 @@ public interface UserRepository extends JpaRepository { // purposely with compiled parameter name not matching the query to also test that @Param takes precedence User getUserByFullNameUsingNamedQueries(@Param("name") String arg); + + long countUsersByLoginEvents_Id(Long id); } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ChildBase.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ChildBase.java new file mode 100644 index 0000000000000..670487953ff85 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ChildBase.java @@ -0,0 +1,9 @@ +package io.quarkus.spring.data.deployment.generics; + +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public class ChildBase { + String nombre; + String detail; +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBase.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBase.java new file mode 100644 index 0000000000000..efb76254359bb --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBase.java @@ -0,0 +1,17 @@ +package io.quarkus.spring.data.deployment.generics; + +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; + +@MappedSuperclass +public class ParentBase { + String name; + String detail; + int age; + float test; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List children; +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBaseRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBaseRepository.java new file mode 100644 index 0000000000000..bb12c2c038cd1 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBaseRepository.java @@ -0,0 +1,7 @@ +package io.quarkus.spring.data.deployment.generics; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParentBaseRepository> extends JpaRepository { + long countParentsByChildren_Nombre(String name); +}