Skip to content

Commit

Permalink
Merge pull request #42705 from aureamunoz/issue-spring-jpa-34395
Browse files Browse the repository at this point in the history
Fix for resolving entity fields based on collections and generics
  • Loading branch information
geoand authored Aug 28, 2024
2 parents 9667d01 + 3945148 commit d340996
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,29 @@ private int indexOfOrMaxValue(String methodName, String term) {
}

/**
* 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) {
Expand All @@ -353,19 +374,30 @@ private FieldInfo resolveNestedField(String repositoryMethodDescription, String

MutableReference<List<ClassInfo>> 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;
}
Expand All @@ -390,6 +422,8 @@ private FieldInfo resolveNestedField(String repositoryMethodDescription, String
if (fieldInfo.type().kind() == Type.Kind.TYPE_VARIABLE) {
typed = true;
parentClassName = getParentNameFromTypedFieldViaHierarchy(fieldInfo, mappedSuperClassInfos);
} else if (fieldInfo.type().kind() == Type.Kind.PARAMETERIZED_TYPE) {
parentClassName = fieldInfo.type().asParameterizedType().arguments().stream().findFirst().get().name();
} else {
parentClassName = fieldInfo.type().name();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ public interface UserRepository extends JpaRepository<User, String> {

// 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.spring.data.deployment.generics;

import jakarta.persistence.MappedSuperclass;

@MappedSuperclass
public class ChildBase {
String nombre;
String detail;
}
Original file line number Diff line number Diff line change
@@ -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<T extends ChildBase> {
String name;
String detail;
int age;
float test;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<T> children;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.spring.data.deployment.generics;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ParentBaseRepository<T extends ParentBase<?>> extends JpaRepository<T, Long> {
long countParentsByChildren_Nombre(String name);
}

0 comments on commit d340996

Please sign in to comment.