From 11789fee40c3bcd234a55288d08de112f65146fa Mon Sep 17 00:00:00 2001 From: Auri Munoz Date: Tue, 29 Oct 2024 11:47:20 +0100 Subject: [PATCH] Support left join left joins when the search criterion involves a nested field. Support 'And' and 'Or' clauses together in method names. Refactors. --- .../data/deployment/MethodNameParser.java | 152 ++++++++++- .../spring/data/deployment/Customer.java | 118 +++++++++ .../data/deployment/CustomerRepository.java | 86 +++++++ .../CustomerRepositoryDerivedMethodsTest.java | 237 ++++++++++++++++++ .../spring/data/deployment/LoginEvent.java | 1 + .../data/deployment/MethodNameParserTest.java | 56 ++++- .../spring/data/deployment/Person.java | 96 +++++++ .../data/deployment/PersonRepository.java | 8 +- .../data/deployment/UserRepository.java | 7 +- ...ositoryNestedFieldsDerivedMethodsTest.java | 90 +++++++ .../fields}/generics/ChildBase.java | 2 +- .../fields}/generics/ParentBase.java | 2 +- .../generics/ParentBaseRepository.java | 3 +- .../src/test/resources/import_customers.sql | 27 ++ .../it/spring/data/jpa/generics/Child.java | 40 +++ .../spring/data/jpa/generics/ChildBase.java | 9 + .../it/spring/data/jpa/generics/Father.java | 40 +++ .../spring/data/jpa/generics/FatherBase.java | 17 ++ .../jpa/generics/FatherBaseRepository.java | 9 + .../data/jpa/generics/FatherRepository.java | 4 + .../data/jpa/generics/FatherResource.java | 27 ++ .../src/main/resources/import.sql | 23 ++ .../data/jpa/generics/FatherResourceIT.java | 8 + .../data/jpa/generics/FatherResourceTest.java | 21 ++ 24 files changed, 1055 insertions(+), 28 deletions(-) create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Customer.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepository.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepositoryDerivedMethodsTest.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/CustomerRepositoryNestedFieldsDerivedMethodsTest.java rename extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/{ => nested/fields}/generics/ChildBase.java (66%) rename extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/{ => nested/fields}/generics/ParentBase.java (85%) rename extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/{ => nested/fields}/generics/ParentBaseRepository.java (63%) create mode 100644 extensions/spring-data-jpa/deployment/src/test/resources/import_customers.sql create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Child.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/ChildBase.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Father.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBase.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBaseRepository.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherRepository.java create mode 100644 integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherResource.java create mode 100644 integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceIT.java create mode 100644 integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceTest.java 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 e7212fb3864751..9fb498ce5d691d 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 @@ -6,7 +6,12 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.persistence.Id; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; @@ -81,6 +86,9 @@ public Result parse(MethodInfo methodInfo) { ClassInfo repositoryClassInfo = methodInfo.declaringClass(); String repositoryMethodDescription = "'" + methodName + "' of repository '" + repositoryClassInfo + "'"; QueryType queryType = getType(methodName); + String entityAlias = getEntityName().toLowerCase(); + String fromClause = "FROM " + getEntityName() + " AS " + entityAlias; + String joinClause = ""; if (queryType == null) { throw new UnableToParseMethodException("Method " + repositoryMethodDescription + " cannot be parsed. Did you forget to annotate the method with '@Query'?"); @@ -167,22 +175,19 @@ public Result parse(MethodInfo methodInfo) { List parts = Collections.singletonList(afterByPart); // default when no 'And' or 'Or' exists boolean containsAnd = containsLogicOperator(afterByPart, "And"); boolean containsOr = containsLogicOperator(afterByPart, "Or"); - if (containsAnd && containsOr) { - throw new UnableToParseMethodException( - "'And' and 'Or' clauses cannot be mixed in a method name - Try specifying the Query with the @Query annotation. Offending method is " - + repositoryMethodDescription + "."); - } - if (containsAnd) { - parts = Arrays.asList(afterByPart.split("And")); - } else if (containsOr) { - parts = Arrays.asList(afterByPart.split("Or")); + String[] partsArray = parts.toArray(new String[0]); + //Spring supports mixing clauses 'And' and 'Or' together in method names + if (containsAnd || containsOr) { + List words = splitAndIncludeRegex(afterByPart, "And", "Or"); + partsArray = words.toArray(new String[0]); } MutableReference> mappedSuperClassInfoRef = MutableReference.of(mappedSuperClassInfos); StringBuilder where = new StringBuilder(); int paramsCount = 0; - for (String part : parts) { - if (part.isEmpty()) { + for (int i = 0; i < partsArray.length; i++) { + String part = partsArray[i]; + if (part.isEmpty() || part.equals("And") || part.equals("Or")) { continue; } String fieldName; @@ -204,6 +209,26 @@ public Result parse(MethodInfo methodInfo) { StringBuilder fieldPathBuilder = new StringBuilder(fieldName.length() + 5); fieldInfo = resolveNestedField(repositoryMethodDescription, fieldName, fieldPathBuilder); fieldName = fieldPathBuilder.toString(); + String topLevelFieldName = getTopLevelFieldName(fieldName); + String childEntityAlias = topLevelFieldName; + if (fieldInfo != null) { + //logic to find the field mapping with the parent (with entityClass) + FieldInfo relatedParentFieldInfo = indexView + .getClassByName(fieldInfo.declaringClass().name().toString()).fields() + .stream() + .filter(fi -> fi.type().name().equals(DotName.createSimple(entityClass.name().toString()))) + .findFirst().orElse(null); + if (relatedParentFieldInfo != null) { + joinClause = " LEFT JOIN " + topLevelFieldName + " " + childEntityAlias + " ON " + + entityAlias + "." + getIdFieldInfo(entityClass).name() + " = " + + topLevelFieldName + "." + relatedParentFieldInfo.name() + "." + + getIdFieldInfo(entityClass).name(); + } else { + // Fallback for cases where the relationship is not explicit + joinClause = " LEFT JOIN " + entityAlias + "." + topLevelFieldName + " " + childEntityAlias; + } + + } } validateFieldWithOperation(operation, fieldInfo, fieldName, repositoryMethodDescription); if ((ignoreCase || allIgnoreCase) && !DotNames.STRING.equals(fieldInfo.type().name())) { @@ -213,7 +238,10 @@ public Result parse(MethodInfo methodInfo) { } if (where.length() > 0) { - where.append(containsAnd ? " AND " : " OR "); + if (containsAnd && partsArray[i - 1].equals("And")) + where.append(" AND "); + if (containsOr && partsArray[i - 1].equals("Or")) + where.append(" OR "); } String upperPrefix = (ignoreCase || allIgnoreCase) ? "UPPER(" : ""; @@ -328,10 +356,96 @@ public Result parse(MethodInfo methodInfo) { } String whereQuery = where.toString().isEmpty() ? "" : " WHERE " + where.toString(); - return new Result(entityClass, "FROM " + getEntityName() + whereQuery, queryType, paramsCount, sort, + fromClause += joinClause; + return new Result(entityClass, fromClause + whereQuery, queryType, paramsCount, sort, topCount); } + /** + * Splits a given input string based on multiple regular expressions and includes each match in the result, + * maintaining the order of parts and separators as they appear in the original string. + * + *

+ * This method allows you to provide multiple regex patterns that will be combined with an OR (`|`) operator, + * so any match from the list of patterns will act as a delimiter. The result will include both the segments + * of the input string that are between matches, as well as the matches themselves, in the order they appear. + * + * @param input the string to be split + * @param regexes one or more regular expression patterns to use as delimiters + * @return a list of strings representing the parts of the input string split by the specified regex patterns, + * with matches themselves included in the output list + * + *

+ * Example usage: + * + *

{@code
+     * String input = "StatusAndCustomerIdAndColor";
+     * List result = splitAndIncludeRegex(input, "And");
+     * // result: [Status, And, CustomerId, And, Color]
+     * }
+ */ + private List splitAndIncludeRegex(String input, String... regexes) { + List result = new ArrayList<>(); + StringBuilder patternBuilder = new StringBuilder(); + + // Create a pattern that combines all regex + for (String regex : regexes) { + if (patternBuilder.length() > 0) { + patternBuilder.append("|"); + } + patternBuilder.append("(").append(regex).append(")"); + } + Pattern pattern = Pattern.compile(patternBuilder.toString()); + Matcher matcher = pattern.matcher(input); + + int lastIndex = 0; + while (matcher.find()) { + // Add the part before the matching + if (matcher.start() > lastIndex) { + result.add(input.substring(lastIndex, matcher.start())); + } + + // Add the regex + result.add(matcher.group()); + lastIndex = matcher.end(); + } + + // Add the last part if exists + if (lastIndex < input.length()) { + result.add(input.substring(lastIndex)); + } + + return result; + } + + /*** + * This method extracts the first part of an input string (up to the first dot), corresponding to the class containing a + * field with the provided name. + * Or returns the entire input if no dot is found. + * + * @param input: whole path of a nested field + *

+ * Example usage: + * + *

{@code
+     * String input = "customer.name";
+     * List result = getTopLevelFieldName(input);
+     * // result: customer
+     * }
+ * + */ + private String getTopLevelFieldName(String input) { + if (input == null) { + return null; + } + int dotIndex = input.indexOf('.'); + // if dot not found the whole string is returned + if (dotIndex == -1) { + return input; + } + return input.substring(0, dotIndex); + } + private int indexOfOrMaxValue(String methodName, String term) { int index = methodName.indexOf(term); return index != -1 ? index : Integer.MAX_VALUE; @@ -577,6 +691,18 @@ private FieldInfo getFieldInfo(String fieldName, ClassInfo entityClass, return fieldInfo; } + /** + * Retrieves the first field in the given entity class that is annotated with `@Id`. + * + * @param entityClass the `ClassInfo` object representing the entity class. + * @return the `FieldInfo` of the first field annotated with `@Id`. + * @throws NoSuchElementException if no field with the `@Id` annotation is found. + */ + private FieldInfo getIdFieldInfo(ClassInfo entityClass) { + return entityClass.fields().stream() + .filter(fieldInfo -> fieldInfo.hasAnnotation(DotName.createSimple(Id.class.getName()))).findFirst().get(); + } + private List getSuperClassInfos(IndexView indexView, ClassInfo entityClass) { List mappedSuperClassInfoElements = new ArrayList<>(3); Type superClassType = entityClass.superClassType(); diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Customer.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Customer.java new file mode 100644 index 00000000000000..d2674a34559f97 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Customer.java @@ -0,0 +1,118 @@ +package io.quarkus.spring.data.deployment; + +import java.time.ZonedDateTime; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Entity +public class Customer { + + @Id + @GeneratedValue + private Integer id; + private String name; + private Integer age; + private ZonedDateTime birthDate; + private Boolean active; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "address_id", referencedColumnName = "id") + private Address address; + + public Customer() { + } + + public Customer(String name, Integer age, ZonedDateTime birthDate, Boolean active) { + this.name = name; + this.age = age; + this.birthDate = birthDate; + this.active = active; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public ZonedDateTime getBirthDate() { + return birthDate; + } + + public void setBirthDate(ZonedDateTime birthDate) { + this.birthDate = birthDate; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + @Entity + public static class Address { + @Id + @GeneratedValue + private Integer id; + + private String zipCode; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "country_id", referencedColumnName = "id") + private Country country; + + public Address(String zipCode, Country country) { + this.zipCode = zipCode; + this.country = country; + } + + public Address() { + + } + } + + @Entity + public static class Country { + @Id + @GeneratedValue + private Integer id; + + private String name; + + private String isoCode; + + public Country(String name, String isoCode) { + this.name = name; + this.isoCode = isoCode; + } + + public Country() { + + } + } +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepository.java new file mode 100644 index 00000000000000..cbc23814150509 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepository.java @@ -0,0 +1,86 @@ +package io.quarkus.spring.data.deployment; + +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomerRepository extends JpaRepository { + + List findByName(String name); + + List findByNameIs(String name); + + List findByNameEquals(String name); + + List findByNameIsNull(); + + List findByNameNot(String name); + + List findByNameIsNot(String name); + + List findByNameStartingWith(String name); + + List findByNameEndingWith(String name); + + List findByNameContaining(String name); + + List findByNameLike(String name); + + List findByAgeLessThan(Integer age); + + List findByAgeLessThanEqual(Integer age); + + List findByAgeGreaterThan(Integer age); + + List findByAgeGreaterThanEqual(Integer age); + + List findByAgeBetween(Integer startAge, Integer endAge); + + List findByBirthDateAfter(ZonedDateTime birthDate); + + List findByBirthDateBefore(ZonedDateTime birthDate); + + List findByActiveTrue(); + + List findByActiveFalse(); + + List findByAgeIn(Collection ages); + + List findByNameOrAge(String name, Integer age); + + List findAllByNameAndAgeAndActive(String name, int age, boolean active); + + List findAllByNameAndAgeOrActive(String name, int age, boolean active); + + List findAllByNameOrAgeOrActive(String name, int age, boolean active); + + List findByNameOrAgeAndActive(String name, Integer age, Boolean active); + + List findByNameOrderByName(String name); + + List findByNameOrderByNameDesc(String name); + + List findByNameIsNotNull(); + + List findByNameOrderByNameAsc(String name); + + //-------------------nested fields-------------------------------- + List findAllByAddressZipCode(String zipCode); + + List findAllByAddressCountryIsoCode(String isoCode); + + List findAllByAddressCountry(String country); + + List findAllByAddress_Country(String country); + + List findAllByAddress_CountryIsoCode(String isoCode); + + List findAllByAddress_Country_IsoCode(String isoCode); + + long countCustomerByAddressCountryName(String name); + + long countCustomerByAddress_ZipCode(String zipCode); + +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepositoryDerivedMethodsTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepositoryDerivedMethodsTest.java new file mode 100644 index 00000000000000..19abad3c38b024 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/CustomerRepositoryDerivedMethodsTest.java @@ -0,0 +1,237 @@ +package io.quarkus.spring.data.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; + +import jakarta.transaction.Transactional; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; + +import io.quarkus.test.QuarkusUnitTest; + +class CustomerRepositoryDerivedMethodsTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("import_customers.sql", "import.sql") + .addClasses(Customer.class, CustomerRepository.class)) + .withConfigurationResource("application.properties"); + + private static final ZonedDateTime BIRTHDATE = ZonedDateTime.now(); + + @Autowired + private CustomerRepository customerRepository; + + @Test + @Transactional + void whenFindByNameThenReturnsCorrectResult() { + + assertEquals(2, customerRepository.findByName("Adam") + .size()); + + assertEquals(2, customerRepository.findByNameIs("Adam") + .size()); + + assertEquals(2, customerRepository.findByNameEquals("Adam") + .size()); + + assertEquals(1, customerRepository.findByNameIsNull() + .size()); + assertEquals(5, customerRepository.findByNameIsNotNull() + .size()); + } + + @Test + @Transactional + void whenFindingByNameNotAdamThenReturnsCorrectResult() { + + assertEquals(3, customerRepository.findByNameNot("Adam") + .size()); + assertEquals(3, customerRepository.findByNameIsNot("Adam") + .size()); + + } + + @Test + @Transactional + void whenFindByNameStartingWith_thenReturnsCorrectResult() { + + assertEquals(2, customerRepository.findByNameStartingWith("A") + .size()); + } + + @Test + @Transactional + void whenFindByNameLikePatternThenReturnsCorrectResult() { + + assertEquals(2, customerRepository.findByNameLike("%im%") + .size()); + } + + @Test + @Transactional + void whenFindByNameEndingWith_thenReturnsCorrectResult() { + + assertEquals(2, customerRepository.findByNameEndingWith("e") + .size()); + } + + @Test + @Transactional + void whenByNameContaining_thenReturnsCorrectResult() { + + assertEquals(1, customerRepository.findByNameContaining("v") + .size()); + } + + @Test + @Transactional + void whenFindingByNameEndingWithMThenReturnsThree() { + + assertEquals(3, customerRepository.findByNameEndingWith("m") + .size()); + } + + @Test + @Transactional + void whenByAgeLessThanThenReturnsCorrectResult() { + + assertEquals(3, customerRepository.findByAgeLessThan(25) + .size()); + } + + @Test + @Transactional + void whenByAgeLessThanEqualThenReturnsCorrectResult() { + + assertEquals(4, customerRepository.findByAgeLessThanEqual(25) + .size()); + } + + @Test + @Transactional + void whenByAgeGreaterThan25ThenReturnsTwo() { + assertEquals(2, customerRepository.findByAgeGreaterThan(25) + .size()); + } + + @Test + @Transactional + void whenFindingByAgeGreaterThanEqual25ThenReturnsThree() { + + assertEquals(3, customerRepository.findByAgeGreaterThanEqual(25) + .size()); + } + + @Test + @Transactional + void whenFindingByAgeBetween20And30ThenReturnsFour() { + + assertEquals(4, customerRepository.findByAgeBetween(20, 30) + .size()); + } + + @Test + @Transactional + void whenFindingByBirthDateAfterYesterdayThenReturnsCorrectResult() { + + final ZonedDateTime yesterday = BIRTHDATE.minusDays(1); + assertEquals(6, customerRepository.findByBirthDateAfter(yesterday) + .size()); + } + + @Test + @Transactional + void whenByBirthDateBeforeThenReturnsCorrectResult() { + + final ZonedDateTime yesterday = BIRTHDATE.minusDays(1); + assertEquals(0, customerRepository.findByBirthDateBefore(yesterday) + .size()); + } + + @Test + @Transactional + void whenByActiveTrueThenReturnsCorrectResult() { + + assertEquals(2, customerRepository.findByActiveTrue() + .size()); + } + + @Test + @Transactional + void whenByActiveFalseThenReturnsCorrectResult() { + + assertEquals(4, customerRepository.findByActiveFalse() + .size()); + } + + @Test + @Transactional + void whenByAgeInThenReturnsCorrectResult() { + + final List ages = Arrays.asList(20, 25); + assertEquals(3, customerRepository.findByAgeIn(ages) + .size()); + } + + @Test + @Transactional + void whenByNameOrAge() { + + assertEquals(3, customerRepository.findByNameOrAge("Adam", 20) + .size()); + } + + @Test + @Transactional + void whenByNameOrAgeAndActive() { + + assertEquals(2, customerRepository.findByNameOrAgeAndActive("Adam", 20, false) + .size()); + } + + @Test + @Transactional + void whenByNameAndAgeAndActive() { + + assertEquals(1, customerRepository.findAllByNameAndAgeAndActive("Adam", 20, false) + .size()); + } + + @Test + @Transactional + void whenByNameOrAgeOrActive() { + + assertEquals(3, customerRepository.findAllByNameOrAgeOrActive("Adam", 20, true) + .size()); + } + + @Test + @Transactional + void whenByNameAndAgeOrActive() { + + assertEquals(3, customerRepository.findAllByNameAndAgeOrActive("Adam", 20, true) + .size()); + } + + @Test + @Transactional + void whenByNameOrderByName() { + + assertEquals(2, customerRepository.findByNameOrderByName("Adam") + .size()); + assertEquals(2, customerRepository.findByNameOrderByNameDesc("Adam") + .size()); + assertEquals(2, customerRepository.findByNameOrderByNameAsc("Adam") + .size()); + } + +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/LoginEvent.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/LoginEvent.java index ea04e5b0bd2f34..c4de6cbfb8a948 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/LoginEvent.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/LoginEvent.java @@ -15,6 +15,7 @@ public class LoginEvent { private Long id; @ManyToOne + // @JoinColumn(name = "user_id", referencedColumnName = "userId") private User user; private ZonedDateTime zonedDateTime; 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 6c3b27f1632540..4b4d6ee512e5ae 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,9 +17,9 @@ 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; +import io.quarkus.spring.data.deployment.nested.fields.generics.ChildBase; +import io.quarkus.spring.data.deployment.nested.fields.generics.ParentBase; +import io.quarkus.spring.data.deployment.nested.fields.generics.ParentBaseRepository; public class MethodNameParserTest { @@ -33,8 +33,9 @@ public void testFindAllByAddressZipCode() throws Exception { additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), entityClass); - assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.zipCode = ?1"); assertThat(result.getParamCount()).isEqualTo(1); + assertThat(result.getQuery()) + .isEqualTo("FROM Person AS person LEFT JOIN person.address address WHERE address.zipCode = ?1"); } @Test @@ -43,17 +44,48 @@ public void testFindAllByAddressCountry() throws Exception { additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), entityClass); - assertThat(result.getQuery()).isEqualTo("FROM Person WHERE addressCountry = ?1"); + assertThat(result.getQuery()).isEqualTo("FROM Person AS person WHERE addressCountry = ?1"); assertThat(result.getParamCount()).isEqualTo(1); } + @Test + public void findAllByNameOrAgeOrActive() throws Exception { + MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByNameOrAgeOrActive", entityClass, + additionalClasses); + assertThat(result).isNotNull(); + assertSameClass(result.getEntityClass(), entityClass); + assertThat(result.getQuery()).isEqualTo("FROM Person AS person WHERE name = ?1 OR age = ?2 OR active = ?3"); + assertThat(result.getParamCount()).isEqualTo(3); + } + + @Test + public void findAllByNameAndAgeOrActive() throws Exception { + MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByNameAndAgeOrActive", entityClass, + additionalClasses); + assertThat(result).isNotNull(); + assertSameClass(result.getEntityClass(), entityClass); + assertThat(result.getQuery()).isEqualTo("FROM Person AS person WHERE name = ?1 AND age = ?2 OR active = ?3"); + assertThat(result.getParamCount()).isEqualTo(3); + } + + @Test + public void findAllByNameAndAgeAndActive() throws Exception { + MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByNameAndAgeAndActive", entityClass, + additionalClasses); + assertThat(result).isNotNull(); + assertSameClass(result.getEntityClass(), entityClass); + assertThat(result.getQuery()).isEqualTo("FROM Person AS person WHERE name = ?1 AND age = ?2 AND active = ?3"); + assertThat(result.getParamCount()).isEqualTo(3); + } + @Test public void testFindAllByAddress_Country() throws Exception { MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddress_Country", entityClass, additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), entityClass); - assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country = ?1"); + assertThat(result.getQuery()) + .isEqualTo("FROM Person AS person LEFT JOIN person.address address WHERE address.country = ?1"); assertThat(result.getParamCount()).isEqualTo(1); } @@ -70,7 +102,8 @@ public void testFindAllByAddress_CountryIsoCode() throws Exception { additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), entityClass); - assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country.isoCode = ?1"); + assertThat(result.getQuery()) + .isEqualTo("FROM Person AS person LEFT JOIN person.address address WHERE address.country.isoCode = ?1"); assertThat(result.getParamCount()).isEqualTo(1); } @@ -80,7 +113,8 @@ public void testFindAllByAddress_Country_IsoCode() throws Exception { additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), entityClass); - assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country.isoCode = ?1"); + assertThat(result.getQuery()) + .isEqualTo("FROM Person AS person LEFT JOIN person.address address WHERE address.country.isoCode = ?1"); assertThat(result.getParamCount()).isEqualTo(1); } @@ -108,7 +142,8 @@ public void testGenericsWithWildcard() throws Exception { additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), ParentBase.class); - assertThat(result.getQuery()).isEqualTo("FROM ParentBase WHERE children.nombre = ?1"); + assertThat(result.getQuery()) + .isEqualTo("FROM ParentBase AS parentbase LEFT JOIN parentbase.children children WHERE children.nombre = ?1"); assertThat(result.getParamCount()).isEqualTo(1); } @@ -121,8 +156,9 @@ public void shouldParseRepositoryMethodOverEntityContainingACollection() throws additionalClasses); assertThat(result).isNotNull(); assertSameClass(result.getEntityClass(), User.class); - assertThat(result.getQuery()).isEqualTo("FROM User WHERE loginEvents.id = ?1"); assertThat(result.getParamCount()).isEqualTo(1); + assertThat(result.getQuery()).isEqualTo( + "FROM User AS user LEFT JOIN loginEvents loginEvents ON user.userId = loginEvents.user.userId WHERE loginEvents.id = ?1"); } private AbstractStringAssert assertSameClass(ClassInfo classInfo, Class aClass) { diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java index e780ed933071de..7c3b04250a7b01 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java @@ -1,33 +1,129 @@ package io.quarkus.spring.data.deployment; +import java.time.ZonedDateTime; + +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; @Entity public class Person { @Id + @GeneratedValue private Integer id; + private String name; + private Integer age; + private ZonedDateTime birthDate; + private Boolean active; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "address_id", referencedColumnName = "id") private Address address; private String addressCountry; + public Person() { + } + + public Person(String name, Integer age, ZonedDateTime birthDate, Boolean active) { + this.name = name; + this.age = age; + this.birthDate = birthDate; + this.active = active; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public ZonedDateTime getBirthDate() { + return birthDate; + } + + public void setBirthDate(ZonedDateTime birthDate) { + this.birthDate = birthDate; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public String getAddressCountry() { + return addressCountry; + } + + public void setAddressCountry(String addressCountry) { + this.addressCountry = addressCountry; + } + @Entity public static class Address { @Id + @GeneratedValue private Integer id; private String zipCode; + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "country_id", referencedColumnName = "id") private Country country; + + public Address(String zipCode, Country country) { + this.zipCode = zipCode; + this.country = country; + } + + public Address() { + + } } @Entity public static class Country { @Id + @GeneratedValue private Integer id; + private String name; + private String isoCode; + + public Country(String name, String isoCode) { + this.name = name; + this.isoCode = isoCode; + } + + public Country() { + + } } } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java index 8f8f32c5f27a23..1492220aa70d24 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java @@ -4,13 +4,19 @@ import org.springframework.data.repository.Repository; -// issue 13067: +// issue 13067: This repo is used to test the MethodNameParser class. See MethodNameParserTest class public interface PersonRepository extends Repository { List findAllByAddressZipCode(String zipCode); List findAllByAddressCountry(String zipCode); + List findAllByNameAndAgeAndActive(String name, int age, boolean active); + + List findAllByNameAndAgeOrActive(String name, int age, boolean active); + + List findAllByNameOrAgeOrActive(String name, int age, boolean active); + List findAllByAddress_Country(String zipCode); List findAllByAddressCountryIsoCode(String zipCode); 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 caef9e4231c8c3..bcae62964d8f55 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 @@ -1,5 +1,7 @@ package io.quarkus.spring.data.deployment; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -29,5 +31,8 @@ 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); + // issue 34395: This method is used to test the MethodNameParser class. See MethodNameParserTest class + long countUsersByLoginEvents_Id(long id); + + List findAllByLoginEvents_Id(long id); } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/CustomerRepositoryNestedFieldsDerivedMethodsTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/CustomerRepositoryNestedFieldsDerivedMethodsTest.java new file mode 100644 index 00000000000000..0fa1a81014d957 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/CustomerRepositoryNestedFieldsDerivedMethodsTest.java @@ -0,0 +1,90 @@ +package io.quarkus.spring.data.deployment.nested.fields; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.transaction.Transactional; + +import org.hibernate.query.QueryArgumentException; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; + +import io.quarkus.spring.data.deployment.Customer; +import io.quarkus.spring.data.deployment.CustomerRepository; +import io.quarkus.test.QuarkusUnitTest; + +class CustomerRepositoryNestedFieldsDerivedMethodsTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("import_customers.sql", "import.sql") + .addClasses(Customer.class, CustomerRepository.class)) + .withConfigurationResource("application.properties"); + + @Autowired + private CustomerRepository customerRepository; + + @Test + @Transactional + void findByAddressZipCode() { + + List allByAddressZipCode = customerRepository.findAllByAddressZipCode("28004"); + assertEquals(2, allByAddressZipCode.size()); + assertThat(allByAddressZipCode.stream().map(c -> c.getName()).collect(Collectors.toList())) + .containsExactlyInAnyOrder("Adam", "Tim"); + } + + @Test + @Transactional + void findByAddressCountryIsoCode() { + + assertEquals(2, customerRepository.findAllByAddressCountryIsoCode("ES") + .size()); + + assertEquals(2, customerRepository.findAllByAddress_CountryIsoCode("ES") + .size()); + + assertEquals(2, customerRepository.findAllByAddress_Country_IsoCode("ES") + .size()); + } + + @Test + @Transactional + void findByAddressCountry() { + + QueryArgumentException exception = assertThrows(QueryArgumentException.class, + () -> customerRepository.findAllByAddressCountry("Spain")); + assertThat(exception).hasMessageContaining("Argument [Spain] of type [java.lang.String] did not match parameter type"); + assertThrows(QueryArgumentException.class, + () -> customerRepository.findAllByAddress_Country("Spain")); + assertThat(exception).hasMessageContaining("Argument [Spain] of type [java.lang.String] did not match parameter type"); + + } + + @Test + @Transactional + void shouldCountSpanishCustomers() { + + long spanishCustomers = customerRepository.countCustomerByAddressCountryName("Spain"); + Assertions.assertEquals(2, spanishCustomers); + + } + + @Test + @Transactional + void shouldCountCustomersByZipCode() { + + long spanishCustomers = customerRepository.countCustomerByAddress_ZipCode("28004"); + Assertions.assertEquals(2, spanishCustomers); + + } +} 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/nested/fields/generics/ChildBase.java similarity index 66% rename from extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ChildBase.java rename to extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/generics/ChildBase.java index 670487953ff85d..0d1f74b03e0b27 100644 --- 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/nested/fields/generics/ChildBase.java @@ -1,4 +1,4 @@ -package io.quarkus.spring.data.deployment.generics; +package io.quarkus.spring.data.deployment.nested.fields.generics; import jakarta.persistence.MappedSuperclass; 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/nested/fields/generics/ParentBase.java similarity index 85% rename from extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBase.java rename to extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/generics/ParentBase.java index efb76254359bba..8f1d7459704f1f 100644 --- 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/nested/fields/generics/ParentBase.java @@ -1,4 +1,4 @@ -package io.quarkus.spring.data.deployment.generics; +package io.quarkus.spring.data.deployment.nested.fields.generics; import java.util.List; 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/nested/fields/generics/ParentBaseRepository.java similarity index 63% rename from extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/generics/ParentBaseRepository.java rename to extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/nested/fields/generics/ParentBaseRepository.java index bb12c2c038cd13..480e6a66634d6f 100644 --- 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/nested/fields/generics/ParentBaseRepository.java @@ -1,7 +1,8 @@ -package io.quarkus.spring.data.deployment.generics; +package io.quarkus.spring.data.deployment.nested.fields.generics; import org.springframework.data.jpa.repository.JpaRepository; +// Issue 34395: this repo is used in MethodNameParserTest public interface ParentBaseRepository> extends JpaRepository { long countParentsByChildren_Nombre(String name); } diff --git a/extensions/spring-data-jpa/deployment/src/test/resources/import_customers.sql b/extensions/spring-data-jpa/deployment/src/test/resources/import_customers.sql new file mode 100644 index 00000000000000..d0af8e6f20fe90 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/resources/import_customers.sql @@ -0,0 +1,27 @@ +-- ALTER TABLE "User$Address" RENAME TO address; +-- ALTER TABLE "User$Country" RENAME TO country; + +INSERT INTO "Customer$Country" (id, name, isocode) +VALUES + (1, 'United States', 'US'), + (2, 'France', 'FR'), + (3, 'Germany', 'DE'), + (4, 'Spain', 'ES'); + +-- Insert sample data for Address +INSERT INTO "Customer$Address" (id, zipcode, country_id) +VALUES + (1, '10001', 1), -- US address + (2, '75001', 2), -- French address + (3, '10115', 3), -- German address + (4, '28004', 4); -- Spanish address + +-- Insert sample data for User +INSERT INTO Customer (id, name, age, birthdate, active, address_id) +VALUES + (1,'Adam', 25, now(), TRUE,4), + (2,'Adam', 20, now(), FALSE, 2), + (3,'Eve', 20, now(), TRUE, 1), + (4,null, 30, now(), FALSE,3), + (5,'Tim', 38, now(), FALSE, 4), + (6,'Timothée', 19, now(), FALSE, 2); \ No newline at end of file diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Child.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Child.java new file mode 100644 index 00000000000000..c64c466c9987d0 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Child.java @@ -0,0 +1,40 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +import org.hibernate.Hibernate; + +@Entity +public class Child extends ChildBase { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) + return false; + Child that = (Child) o; + return id != null && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/ChildBase.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/ChildBase.java new file mode 100644 index 00000000000000..00ccedb45451d0 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/ChildBase.java @@ -0,0 +1,9 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public class ChildBase { + String name; + String detail; +} diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Father.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Father.java new file mode 100644 index 00000000000000..7a448e61bed211 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/Father.java @@ -0,0 +1,40 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +import org.hibernate.Hibernate; + +@Entity +public class Father extends FatherBase { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) + return false; + Father that = (Father) o; + return id != null && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBase.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBase.java new file mode 100644 index 00000000000000..aff1892bc4db57 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBase.java @@ -0,0 +1,17 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; + +@MappedSuperclass +public class FatherBase { + String name; + String detail; + int age; + float test; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List children; +} diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBaseRepository.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBaseRepository.java new file mode 100644 index 00000000000000..8ec91c0421074f --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherBaseRepository.java @@ -0,0 +1,9 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface FatherBaseRepository> extends JpaRepository { + long countParentsByChildren_Name(String name); +} \ No newline at end of file diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherRepository.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherRepository.java new file mode 100644 index 00000000000000..bc212e03f5875c --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherRepository.java @@ -0,0 +1,4 @@ +package io.quarkus.it.spring.data.jpa.generics; + +public interface FatherRepository extends FatherBaseRepository { +} \ No newline at end of file diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherResource.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherResource.java new file mode 100644 index 00000000000000..3a560b4779e0d7 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/generics/FatherResource.java @@ -0,0 +1,27 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/rest/fathers") +@ApplicationScoped +public class FatherResource { + private final FatherBaseRepository fatherRepository; + + public FatherResource(FatherBaseRepository fatherRepository) { + this.fatherRepository = fatherRepository; + } + + @GET + @Path("/{childName}") + @Produces(MediaType.APPLICATION_JSON) + public long getParents(@PathParam("childName") String childName) { + long nameFathers = fatherRepository.countParentsByChildren_Name(childName); + return nameFathers; + } + +} diff --git a/integration-tests/spring-data-jpa/src/main/resources/import.sql b/integration-tests/spring-data-jpa/src/main/resources/import.sql index 6fce9bd2a3b346..ac2b9c4f362eeb 100644 --- a/integration-tests/spring-data-jpa/src/main/resources/import.sql +++ b/integration-tests/spring-data-jpa/src/main/resources/import.sql @@ -91,3 +91,26 @@ INSERT INTO CatalogValue(id, key_, displayName, type) VALUES (1, 'DE-BY', 'Bavar INSERT INTO CatalogValue(id, key_, displayName, type) VALUES (2, 'DE-SN', 'Saxony', 'federalState'); INSERT INTO CatalogValue(id, key_, displayName, type) VALUES (3, 'DE', 'Germany', 'country'); INSERT INTO CatalogValue(id, key_, displayName, type) VALUES (4, 'FR', 'France', 'country'); + +INSERT INTO Father (id, name, detail, age, test) VALUES (1, 'John Doe', 'Graphic designer and enjoys cooking', 40, 75.5); +INSERT INTO Father (id, name, detail, age, test) VALUES (2, 'Jane Smith', 'Software engineer and mother of two', 35, 82.3); +INSERT INTO Father (id, name, detail, age, test) VALUES (3, 'Sarah Johnson', 'She is a nurse and loves sport', 50, 92.7); + +-- Insert sample data for Child associated with each Parent +INSERT INTO Child (id, name, detail) VALUES (1, 'Alice', 'Loves drawing and painting'); +INSERT INTO Child (id, name, detail) VALUES (2, 'Bob', 'Plays the violin and loves animals'); +INSERT INTO Child (id, name, detail) VALUES (3, 'Eve', 'Loves drawing and painting'); +INSERT INTO Child (id, name, detail) VALUES (4, 'Diana', 'Plays basketball and chess'); +INSERT INTO Child (id, name, detail) VALUES (5, 'Eve', 'Enjoys building robots and coding'); +INSERT INTO Child (id, name, detail) VALUES (6, 'Frank', 'Writes short stories and poems'); +INSERT INTO Child (id, name, detail) VALUES (7, 'Grace', 'Interested in astronomy and space'); +INSERT INTO Child (id, name, detail) VALUES (8, 'Eve', 'Enjoys skateboarding and video games'); + +INSERT INTO father_child (father_id, children_id) VALUES (1,1); +INSERT INTO father_child (father_id, children_id) VALUES (1,2); +INSERT INTO father_child (father_id, children_id) VALUES (1,3); +INSERT INTO father_child (father_id, children_id) VALUES (2,4); +INSERT INTO father_child (father_id, children_id) VALUES (2,5); +INSERT INTO father_child (father_id, children_id) VALUES (3,6); +INSERT INTO father_child (father_id, children_id) VALUES (3,7); +INSERT INTO father_child (father_id, children_id) VALUES (3,8); \ No newline at end of file diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceIT.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceIT.java new file mode 100644 index 00000000000000..0f5e0addc0f6cc --- /dev/null +++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class FatherResourceIT extends FatherResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceTest.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceTest.java new file mode 100644 index 00000000000000..fd8278a7c83670 --- /dev/null +++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/generics/FatherResourceTest.java @@ -0,0 +1,21 @@ +package io.quarkus.it.spring.data.jpa.generics; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FatherResourceTest { + + @Test + public void shouldReturnEveParents() { + given() + .when().get("/rest/fathers/Eve") + .then() + .statusCode(200).body(containsString("3")); + } + +}