diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java index 94901eed3ae61..a3144d1aa574a 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java @@ -72,7 +72,6 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu if (limit != null) { // create a custom page object that will limit the results by the limit size page = methodCreator.newInstance(MethodDescriptor.ofConstructor(Page.class, int.class), methodCreator.load(limit)); - } else if (pageableParameterIndex != null) { page = methodCreator.invokeStaticMethod( MethodDescriptor.ofMethod(TypesConverter.class, "toPanachePage", Page.class, Pageable.class), @@ -98,8 +97,10 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu ResultHandle singleResult = tryBlock.invokeInterfaceMethod( MethodDescriptor.ofMethod(PanacheQuery.class, panacheQueryMethodToUse, Object.class), panacheQuery); + ResultHandle casted = tryBlock.checkCast(singleResult, entityClassInfo.name().toString()); tryBlock.returnValue(casted); + CatchBlockCreator catchBlock = tryBlock.addCatch(NoResultException.class); catchBlock.returnValue(catchBlock.loadNull()); @@ -116,11 +117,24 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu ResultHandle singleResult = tryBlock.invokeInterfaceMethod( MethodDescriptor.ofMethod(PanacheQuery.class, panacheQueryMethodToUse, Object.class), panacheQuery); - ResultHandle casted = tryBlock.checkCast(singleResult, entityClassInfo.name().toString()); - ResultHandle optional = tryBlock.invokeStaticMethod( - MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), - casted); - tryBlock.returnValue(optional); + + if (customResultType == null) { + ResultHandle casted = tryBlock.checkCast(singleResult, entityClassInfo.name().toString()); + ResultHandle optional = tryBlock.invokeStaticMethod( + MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), + casted); + tryBlock.returnValue(optional); + } else { + ResultHandle customResult = tryBlock.invokeStaticMethod( + MethodDescriptor.ofMethod(customResultType.toString(), "convert_" + methodName, + customResultType.toString(), + Object[].class.getName()), + singleResult); + ResultHandle optional = tryBlock.invokeStaticMethod( + MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), + customResult); + tryBlock.returnValue(optional); + } CatchBlockCreator catchBlock = tryBlock.addCatch(NoResultException.class); ResultHandle emptyOptional = catchBlock.invokeStaticMethod( MethodDescriptor.ofMethod(Optional.class, "empty", Optional.class)); @@ -134,13 +148,14 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu MethodDescriptor.ofMethod(PanacheQuery.class, "list", List.class), panacheQuery); } else { + ResultHandle stream = methodCreator.invokeInterfaceMethod( MethodDescriptor.ofMethod(PanacheQuery.class, "stream", Stream.class), panacheQuery); // Function to convert Object[] to the custom type (using the generated static convert method) - FunctionCreator function = methodCreator.createFunction(Function.class); - BytecodeCreator funcBytecode = function.getBytecode(); + FunctionCreator customResultMappingFunction = methodCreator.createFunction(Function.class); + BytecodeCreator funcBytecode = customResultMappingFunction.getBytecode(); ResultHandle obj = funcBytecode.invokeStaticMethod( MethodDescriptor.ofMethod(customResultType.toString(), "convert_" + methodName, customResultType.toString(), @@ -150,7 +165,7 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu stream = methodCreator.invokeInterfaceMethod( MethodDescriptor.ofMethod(Stream.class, "map", Stream.class, Function.class), - stream, function.getInstance()); + stream, customResultMappingFunction.getInstance()); // Re-collect the stream into a list ResultHandle collector = methodCreator.invokeStaticMethod( @@ -213,12 +228,32 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu } methodCreator.returnValue(sliceResult); - } else if (isIntLongOrBoolean(returnType)) { ResultHandle singleResult = methodCreator.invokeInterfaceMethod( MethodDescriptor.ofMethod(PanacheQuery.class, "singleResult", Object.class), panacheQuery); methodCreator.returnValue(singleResult); + } else if (customResultType != null) { + // when limit is specified we don't want to fail when there are multiple results, we just want to return the first one + String panacheQueryMethodToUse = (limit != null) ? "firstResult" : "singleResult"; + + TryBlock tryBlock = methodCreator.tryBlock(); + ResultHandle singleResult = tryBlock.invokeInterfaceMethod( + MethodDescriptor.ofMethod(PanacheQuery.class, panacheQueryMethodToUse, Object.class), + panacheQuery); + + ResultHandle customResult = tryBlock.invokeStaticMethod( + MethodDescriptor.ofMethod(customResultType.toString(), "convert_" + methodName, + customResultType.toString(), + Object[].class.getName()), + singleResult); + + tryBlock.returnValue(customResult); + + CatchBlockCreator catchBlock = tryBlock.addCatch(NoResultException.class); + catchBlock.returnValue(catchBlock.loadNull()); + + tryBlock.returnValue(customResult); } else { throw new IllegalArgumentException( "Return type of method " + methodName + " of Repository " + repositoryClassInfo diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java index 8003af5ee0939..321a119d88b9b 100644 --- a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieRepository.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -61,9 +62,21 @@ public interface MovieRepository extends CrudRepository { @Query("SELECT DISTINCT m.rating FROM Movie m where m.rating != null") List findAllRatings(); + @Query("SELECT title, rating from Movie where title = ?1") + Optional findOptionalRatingByTitle(String title); + + @Query("SELECT title, rating FROM Movie WHERE title = ?1") + MovieRating findRatingByTitle(String title); + interface MovieCountByRating { String getRating(); Long getCount(); } + + interface MovieRating { + String getTitle(); + + String getRating(); + } } diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieResource.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieResource.java index 16100a24bcada..f8cdcfe65c1d3 100644 --- a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieResource.java +++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/MovieResource.java @@ -4,6 +4,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Optional; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -114,7 +115,6 @@ public void setRatingToNullForTitle(@PathParam("title") String title) { @Produces("application/json") public List countByRating() { List list = movieRepository.countByRating(); - // #6205 - Make sure elements in list have been properly cast to the target object type. // If the type is wrong (Object array), this will throw a ClassNotFoundException MovieRepository.MovieCountByRating first = list.get(0); @@ -123,6 +123,24 @@ public List countByRating() { return list; } + @GET + @Path("/rating/forTitle/{title}") + @Produces("application/json") + public MovieRepository.MovieRating titleRating(@PathParam("title") String title) { + MovieRepository.MovieRating result = movieRepository.findRatingByTitle(title); + Objects.requireNonNull(result); + return result; + } + + @GET + @Path("/rating/opt/forTitle/{title}") + @Produces("application/json") + public Optional optionalTitleRating(@PathParam("title") String title) { + Optional result = movieRepository.findOptionalRatingByTitle(title); + System.out.println(result); + return result; + } + @GET @Path("/ratings") @Produces("application/json") diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/MovieResourceTest.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/MovieResourceTest.java index 6b8021834ca3a..7c467478b828b 100644 --- a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/MovieResourceTest.java +++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/MovieResourceTest.java @@ -199,6 +199,23 @@ void testFindAllRatings() { .body(containsString("PG-13")); } + @Test + void testFindRatingByTitle() { + when().get("/movie/rating/forTitle/Interstellar").then() + .statusCode(200) + .body(containsString("Interstellar")) + .body(containsString("PG-13")) + .body(not(containsString("duration"))); + } + + @Test + void testFindOptionalRatingByTitle() { + when().get("/movie/rating/opt/forTitle/Aladdin").then() + .statusCode(200) + .body(containsString("Aladdin")) + .body(not(containsString("duration"))); + } + @Test void testNewMovie() { long id = 999L;