diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 59f161293d..82482cd99c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.core.convert.ConversionService; @@ -339,6 +340,7 @@ protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAcce StoredProcedureJpaQuery query = (StoredProcedureJpaQuery) jpaQuery; StoredProcedureQuery procedure = query.createQuery(accessor); + Class returnType = query.getQueryMethod().getReturnType(); try { @@ -350,7 +352,9 @@ protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAcce throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION); } - return collectionQuery ? procedure.getResultList() : procedure.getSingleResult(); + if (!Map.class.isAssignableFrom(returnType)) { + return collectionQuery ? procedure.getResultList() : procedure.getSingleResult(); + } } return query.extractOutputValue(procedure); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java index a489b490c2..2acc5f83c1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java @@ -15,14 +15,15 @@ */ package org.springframework.data.jpa.repository.query; +import jakarta.persistence.StoredProcedureQuery; + import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -import jakarta.persistence.StoredProcedureQuery; - import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -138,7 +139,12 @@ public boolean hasReturnValue() { if (getOutputProcedureParameters().isEmpty()) return false; - Class outputType = getOutputProcedureParameters().get(0).getType(); - return !(void.class.equals(outputType) || Void.class.equals(outputType)); + for (ProcedureParameter parameter : getOutputProcedureParameters()) { + if (!ClassUtils.isVoidType(parameter.getType())) { + return true; + } + } + + return false; } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java index 47bcccd99d..d10a3b83d1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java @@ -57,6 +57,7 @@ * @author Greg Turnquist * @author Yanming Zhou * @author Thorben Janssen + * @author Mark Paluch */ @Transactional @ExtendWith(SpringExtension.class) @@ -140,7 +141,7 @@ void testEntityListFromNamedProcedure() { new Employee(4, "Gabriel")); } - @Test // 3460 + @Test // GH-3460 void testPositionalInOutParameter() { Map results = repository.positionalInOut(1, 2); @@ -149,6 +150,15 @@ void testPositionalInOutParameter() { assertThat(results.get("3")).isEqualTo(3); } + @Test // GH-3460 + void supportsMultipleOutParameters() { + + Map results = repository.multiple_out(5); + + assertThat(results).containsEntry("result1", 5).containsEntry("result2", 10); + assertThat(results).containsKey("some_cursor"); + } + @Entity @NamedStoredProcedureQuery( // name = "get_employees_postgres", // @@ -160,6 +170,13 @@ void testPositionalInOutParameter() { name = "Employee.noResultSet", // procedureName = "get_employees_count", // parameters = { @StoredProcedureParameter(mode = ParameterMode.OUT, name = "results", type = Integer.class) }) + @NamedStoredProcedureQuery( // + name = "Employee.multiple_out", // + procedureName = "multiple_out", // + parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, name = "someNumber", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, name = "some_cursor", type = void.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "result1", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "result2", type = Integer.class) }) @NamedStoredProcedureQuery( // name = "positional_inout", // procedureName = "positional_inout_parameter_issue3460", // @@ -243,6 +260,9 @@ public interface EmployeeRepositoryWithRefCursor extends JpaRepository multiple_out(int someNumber); + @Procedure(name = "get_employees_postgres", refCursor = true) List entityListFromNamedProcedure(); diff --git a/spring-data-jpa/src/test/resources/scripts/postgres-stored-procedures.sql b/spring-data-jpa/src/test/resources/scripts/postgres-stored-procedures.sql index ceafa45f00..cb08f096ce 100644 --- a/spring-data-jpa/src/test/resources/scripts/postgres-stored-procedures.sql +++ b/spring-data-jpa/src/test/resources/scripts/postgres-stored-procedures.sql @@ -51,4 +51,17 @@ $BODY$ BEGIN outParam = 3; END; -$BODY$;; \ No newline at end of file +$BODY$;; + +CREATE OR REPLACE PROCEDURE multiple_out(IN someNumber integer, OUT some_cursor REFCURSOR, + OUT result1 integer, OUT result2 integer) + LANGUAGE 'plpgsql' +AS +$BODY$ +BEGIN + result1 = 1 * someNumber; + result2 = 2 * someNumber; + + OPEN some_cursor FOR SELECT COUNT(*) FROM employee; +END; +$BODY$;; diff --git a/src/main/antora/modules/ROOT/pages/jpa/stored-procedures.adoc b/src/main/antora/modules/ROOT/pages/jpa/stored-procedures.adoc index 7eae20f798..a285153d78 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/stored-procedures.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/stored-procedures.adoc @@ -94,4 +94,28 @@ Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); If the stored procedure getting called has a single out parameter that parameter may be returned as the return value of the method. If there are multiple out parameters specified in a `@NamedStoredProcedureQuery` annotation those can be returned as a `Map` with the key being the parameter name given in the `@NamedStoredProcedureQuery` annotation. -NOTE: Note that if the stored procedure returns a `ResultSet` then any `OUT` parameters are omitted as Java can only return a single method return value. +NOTE: Note that if the stored procedure returns a `ResultSet` then any `OUT` parameters are omitted as Java can only return a single method return value unless the method declares a `Map` return type. + +The following example shows how to obtain multiple `OUT` parameters if the stored procedure has multiple `OUT` parameters and is registered as `@NamedStoredProcedureQuery`. `@NamedStoredProcedureQuery` registration is required to provide parameter metadata. + +.StoredProcedure metadata definitions on an entity. +==== +[source,java] +---- +@Entity +@NamedStoredProcedureQuery(name = "User.multiple_out_parameters", procedureName = "multiple_out_parameters", parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, name = "some_cursor", type = void.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) }) +public class User {} +---- +==== + +.Returning multiple OUT parameters +==== +[source,java] +---- +@Procedure(name = "User.multiple_out_parameters") +Map returnsMultipleOutParameters(@Param("arg") Integer arg); +---- +====