diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index ed7473ecb..ac09f871d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -53,6 +53,7 @@ import org.springframework.util.ReflectionUtils; * @author Christoph Strobl * @author Nicolas Cirigliano * @author Jens Schauder + * @author Gabriel Basilio */ public abstract class JpaQueryExecution { @@ -301,6 +302,8 @@ public abstract class JpaQueryExecution { */ static class ProcedureExecution extends JpaQueryExecution { + private static final String NO_SURROUNDING_TRANSACTION = "You're trying to execute a @Procedure method without a surrounding transaction that keeps the connection open so that the ResultSet can actually be consumed. Make sure the consumer code uses @Transactional or any other way of declaring a (read-only) transaction."; + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[]) @@ -312,7 +315,24 @@ public abstract class JpaQueryExecution { StoredProcedureJpaQuery storedProcedureJpaQuery = (StoredProcedureJpaQuery) jpaQuery; StoredProcedureQuery storedProcedure = storedProcedureJpaQuery.createQuery(accessor); - storedProcedure.execute(); + + boolean returnsResultSet = storedProcedure.execute(); + + if (returnsResultSet) { + if (!SurroundingTransactionDetectorMethodInterceptor.INSTANCE.isSurroundingTransactionActive()) + throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION); + + List result = storedProcedure.getResultList(); + + if (!storedProcedureJpaQuery.getQueryMethod().isCollectionQuery()) { + if (result.isEmpty()) + return null; + if (result.size() == 1) + return result.get(0); + } + + return result; + } return storedProcedureJpaQuery.extractOutputValue(storedProcedure); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/Procedure.java b/src/main/java/org/springframework/data/jpa/repository/query/Procedure.java index e60a6c6e1..13007cc25 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/Procedure.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/Procedure.java @@ -26,6 +26,7 @@ import java.lang.annotation.Target; * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl + * @author Gabriel Basilio * @since 1.6 */ @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @@ -51,4 +52,9 @@ public @interface Procedure { * The name of the outputParameter, defaults to {@code ""}. */ String outputParameterName() default ""; + + /** + * Whether the procedure returns a Ref Cursor from the database {@code false}. + */ + boolean refCursor() default false; } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java b/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java new file mode 100644 index 000000000..6d185e502 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.jpa.repository.query; + +import org.springframework.lang.Nullable; + +import javax.persistence.ParameterMode; + +/** + * This class represents a Stored Procedure Parameter + * and an instance of the annotation {@link javax.persistence.StoredProcedureParameter}. + * + * @author Gabriel Basilio + */ +public class ProcedureParameter { + + private final String name; + private final ParameterMode mode; + private final Class type; + + public ProcedureParameter(@Nullable String name, ParameterMode mode, Class type) { + this.name = name; + this.mode = mode; + this.type = type; + } + + public String getName() { + return name; + } + + public ParameterMode getMode() { + return mode; + } + + public Class getType() { + return type; + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java index 34630e7ef..0160ad65e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java @@ -22,6 +22,7 @@ import java.util.List; import javax.persistence.NamedStoredProcedureQueries; import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; import javax.persistence.StoredProcedureParameter; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -38,6 +39,7 @@ import org.springframework.util.StringUtils; * @author Mark Paluch * @author Diego Diez * @author Jeff Sheets + * @author Gabriel Basilio * @since 1.6 */ enum StoredProcedureAttributeSource { @@ -72,7 +74,7 @@ enum StoredProcedureAttributeSource { + method); } - return new StoredProcedureAttributes(procedureName, procedure.outputParameterName(), method.getReturnType()); + return new StoredProcedureAttributes(procedureName, createOutputProcedureParameterFrom(method, procedure)); } /** @@ -102,28 +104,29 @@ enum StoredProcedureAttributeSource { private StoredProcedureAttributes newProcedureAttributesFrom(Method method, NamedStoredProcedureQuery namedStoredProc, Procedure procedure) { - List outputParameterNames = new ArrayList<>(); - List> outputParameterTypes = new ArrayList<>(); + List outputParameters = new ArrayList<>(); if (!procedure.outputParameterName().isEmpty()) { // we give the output parameter definition from the @Procedure annotation precedence - outputParameterNames.add(procedure.outputParameterName()); + outputParameters.add(createOutputProcedureParameterFrom(method, procedure)); } else { // try to discover the output parameter - List outputParameters = extractOutputParametersFrom(namedStoredProc); + List namedProcedureOutputParameters = extractOutputParametersFrom(namedStoredProc); - for (StoredProcedureParameter outputParameter : outputParameters) { - outputParameterNames.add(outputParameter.name()); - outputParameterTypes.add(outputParameter.type()); + for (StoredProcedureParameter outputParameter : namedProcedureOutputParameters) { + outputParameters.add(new ProcedureParameter( + outputParameter.name(), outputParameter.mode(), outputParameter.type())); } } - if (outputParameterTypes.isEmpty()) { - outputParameterTypes.add(method.getReturnType()); - } + return new StoredProcedureAttributes(namedStoredProc.name(), outputParameters, true); + } - return new StoredProcedureAttributes(namedStoredProc.name(), outputParameterNames, outputParameterTypes, true); + private ProcedureParameter createOutputProcedureParameterFrom(Method method, Procedure procedure) { + return new ProcedureParameter(procedure.outputParameterName(), + procedure.refCursor() ? ParameterMode.REF_CURSOR : ParameterMode.OUT, + method.getReturnType()); } private List extractOutputParametersFrom(NamedStoredProcedureQuery namedStoredProc) { diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java index fbc891cb7..7c885fc13 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java @@ -33,6 +33,7 @@ import java.util.stream.IntStream; * @author Mark Paluch * @author Jeff Sheets * @author Jens Schauder + * @author Gabriel Basilio * @since 1.6 */ class StoredProcedureAttributes { @@ -42,52 +43,51 @@ class StoredProcedureAttributes { private final boolean namedStoredProcedure; private final String procedureName; - private final List outputParameterNames; - private final List> outputParameterTypes; + private final List outputProcedureParameters; /** * Creates a new {@link StoredProcedureAttributes}. * * @param procedureName must not be {@literal null}. - * @param outputParameterName may be {@literal null}. - * @param outputParameterType must not be {@literal null}. */ - StoredProcedureAttributes(String procedureName, @Nullable String outputParameterName, - Class outputParameterType) { - this(procedureName, Collections.singletonList(outputParameterName), Collections.singletonList(outputParameterType), false); + StoredProcedureAttributes(String procedureName, ProcedureParameter parameter) { + this(procedureName, Collections.singletonList(parameter), false); } /** * Creates a new {@link StoredProcedureAttributes}. * * @param procedureName must not be {@literal null}. - * @param outputParameterNames may be empty, but not {@literal null}. - * @param outputParameterTypes must not be empty, and cannot be a single element of {@literal null}. * @param namedStoredProcedure flag signaling if the stored procedure has a name. */ - StoredProcedureAttributes(String procedureName, List outputParameterNames, - List> outputParameterTypes, boolean namedStoredProcedure) { + StoredProcedureAttributes(String procedureName, List outputProcedureParameters, boolean namedStoredProcedure) { Assert.notNull(procedureName, "ProcedureName must not be null!"); - Assert.notNull(outputParameterNames, "OutputParameterNames must not be null!"); - Assert.notEmpty(outputParameterTypes, "OutputParameterTypes must not be empty!"); - Assert.isTrue(outputParameterTypes.size() != 1 || outputParameterTypes.get(0) != null, "OutputParameterTypes must not have size 1 with a null value"); + Assert.notNull(outputProcedureParameters, "OutputProcedureParameters must not be null!"); + Assert.isTrue(outputProcedureParameters.size() != 1 || outputProcedureParameters.get(0) != null, "ProcedureParameters must not have size 1 with a null value"); this.procedureName = procedureName; - this.outputParameterNames = namedStoredProcedure - ? outputParameterNames - : completeOutputParameterNames(outputParameterNames); - this.outputParameterTypes = outputParameterTypes; this.namedStoredProcedure = namedStoredProcedure; + + if (namedStoredProcedure) { + this.outputProcedureParameters = outputProcedureParameters; + } else { + this.outputProcedureParameters = getParametersWithCompletedNames(outputProcedureParameters); + } } - private List completeOutputParameterNames(List outputParameterNames) { - - return IntStream.range(0, outputParameterNames.size()) // - .mapToObj(i -> completeOutputParameterName(i, outputParameterNames.get(i))) // + private List getParametersWithCompletedNames(List procedureParameters) { + return IntStream.range(0, procedureParameters.size()) + .mapToObj(i -> getParameterWithCompletedName(procedureParameters.get(i), i)) .collect(Collectors.toList()); } + private ProcedureParameter getParameterWithCompletedName(ProcedureParameter parameter, int i) { + return new ProcedureParameter( + completeOutputParameterName(i, parameter.getName()), + parameter.getMode(), parameter.getType()); + } + private String completeOutputParameterName(int i, String paramName) { return StringUtils.hasText(paramName) // @@ -113,34 +113,27 @@ class StoredProcedureAttributes { * * @return */ - public List getOutputParameterNames() { - return outputParameterNames; - } - - /** - * Returns the types of the output parameters. - * - * @return - */ - public List> getOutputParameterTypes() { - return outputParameterTypes; - } - - /** - * Returns whether the stored procedure is a named one. - * - * @return - */ public boolean isNamedStoredProcedure() { return namedStoredProcedure; } + /** + * @return Returns the stored procedure output parameter list + */ + public List getOutputProcedureParameters() { + return outputProcedureParameters; + } + /** * Returns whether the stored procedure will produce a result. * * @return */ public boolean hasReturnValue() { - return !(outputParameterTypes.size() == 1 && (void.class.equals(outputParameterTypes.get(0)) || Void.class.equals(outputParameterTypes.get(0)))); + if (getOutputProcedureParameters().isEmpty()) + return false; + + Class outputType = getOutputProcedureParameters().get(0).getType(); + return !(void.class.equals(outputType) || Void.class.equals(outputType)); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index 0839fd0b7..caacc7adc 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -23,6 +23,7 @@ import java.util.stream.IntStream; import javax.persistence.EntityManager; import javax.persistence.NamedStoredProcedureQuery; import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; import javax.persistence.StoredProcedureQuery; import javax.persistence.TypedQuery; @@ -132,42 +133,32 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { return null; } - Map outputValues = new HashMap<>(); - List parameterNames = procedureAttributes.getOutputParameterNames(); + List outputParameters = procedureAttributes.getOutputProcedureParameters(); - for (int i = 0; i < parameterNames.size(); i++) { - - String name = parameterNames.get(i); - outputValues.put(name, extractOutputParameter(storedProcedureQuery, i)); + if (outputParameters.size() == 1) { + return extractOutputParameterValue(outputParameters.get(0), 0, storedProcedureQuery); } - return outputValues.size() == 1 ? outputValues.values().iterator().next() : outputValues; - } + Map outputValues = new HashMap<>(); - private Object extractOutputParameter(StoredProcedureQuery storedProcedureQuery, Integer index) { + for (int i = 0; i < outputParameters.size(); i++) { + ProcedureParameter outputParameter = outputParameters.get(i); + outputValues.put(outputParameter.getName(), extractOutputParameterValue(outputParameter, i, storedProcedureQuery)); + } - String outputParameterName = procedureAttributes.getOutputParameterNames().get(index); - JpaParameters parameters = getQueryMethod().getParameters(); - - return extractOutputParameterValue(storedProcedureQuery, outputParameterName, index, - parameters.getNumberOfParameters()); + return outputValues; } /** - * extract the value of an output parameter either by name or by index. - * - * @param storedProcedureQuery the query object of the stored procedure. - * @param name the name of the output parameter - * @param index index of the output parameter - * @param offset for index based access the index after which to find the output parameter values - * @return the value + * @return The value of an output parameter either by name or by index. */ - private Object extractOutputParameterValue(StoredProcedureQuery storedProcedureQuery, String name, Integer index, - int offset) { + private Object extractOutputParameterValue(ProcedureParameter outputParameter, Integer index, StoredProcedureQuery storedProcedureQuery) { - return useNamedParameters && StringUtils.hasText(name) ? // - storedProcedureQuery.getOutputParameterValue(name) - : storedProcedureQuery.getOutputParameterValue(offset + index + 1); + JpaParameters methodParameters = getQueryMethod().getParameters(); + + return useNamedParameters && StringUtils.hasText(outputParameter.getName()) ? + storedProcedureQuery.getOutputParameterValue(outputParameter.getName()) + : storedProcedureQuery.getOutputParameterValue(methodParameters.getNumberOfParameters() + index + 1); } /** @@ -192,9 +183,7 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { private StoredProcedureQuery newAdhocStoredProcedureQuery() { JpaParameters params = getQueryMethod().getParameters(); - String procedureName = procedureAttributes.getProcedureName(); - - StoredProcedureQuery procedureQuery = getEntityManager().createStoredProcedureQuery(procedureName); + StoredProcedureQuery procedureQuery = createAdhocStoredProcedureQuery(); for (JpaParameter param : params) { @@ -214,23 +203,41 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { if (procedureAttributes.hasReturnValue()) { - ParameterMode mode = ParameterMode.OUT; + ProcedureParameter procedureOutput = procedureAttributes.getOutputProcedureParameters().get(0); - IntStream.range(0, procedureAttributes.getOutputParameterTypes().size()).forEach(i -> { - Class outputParameterType = procedureAttributes.getOutputParameterTypes().get(i); + /* If the stored procedure returns a ResultSet without using REF_CURSOR, + it is not necessary to declare an output parameter */ + if ((isResultSetProcedure() && procedureOutput.getMode() == ParameterMode.REF_CURSOR) || !isResultSetProcedure()) { if (useNamedParameters) { - - String outputParameterName = procedureAttributes.getOutputParameterNames().get(i); - procedureQuery.registerStoredProcedureParameter(outputParameterName, outputParameterType, mode); - + procedureQuery.registerStoredProcedureParameter(procedureOutput.getName(), procedureOutput.getType(), procedureOutput.getMode()); } else { - procedureQuery.registerStoredProcedureParameter(params.getNumberOfParameters() + i + 1, outputParameterType, - mode); + // Output parameter should be after the input parameters + int outputParameterIndex = params.getNumberOfParameters() + 1; + procedureQuery.registerStoredProcedureParameter(outputParameterIndex, procedureOutput.getType(), + procedureOutput.getMode()); } - }); + } } return procedureQuery; } + + private StoredProcedureQuery createAdhocStoredProcedureQuery() { + String procedureName = procedureAttributes.getProcedureName(); + + if (getQueryMethod().isQueryForEntity()) { + return getEntityManager().createStoredProcedureQuery(procedureName, + getQueryMethod().getEntityInformation().getJavaType()); + } + + return getEntityManager().createStoredProcedureQuery(procedureName); + } + + /** + * @return true if the stored procedure will use a ResultSet to return data and not output parameters + */ + private boolean isResultSetProcedure() { + return getQueryMethod().isCollectionQuery() || getQueryMethod().isQueryForEntity(); + } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index a10de113d..4147b1860 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -25,9 +25,7 @@ import java.util.stream.Stream; import javax.persistence.EntityManager; import javax.persistence.Tuple; -import com.querydsl.core.types.EntityPath; import org.slf4j.Logger; - import org.springframework.beans.factory.BeanFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; @@ -40,6 +38,7 @@ import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -59,6 +58,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; +import com.querydsl.core.types.EntityPath; + /** * JPA specific generic repository factory. * @@ -97,7 +98,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); addRepositoryProxyPostProcessor((factory, repositoryInformation) -> { - if (hasMethodReturningStream(repositoryInformation.getRepositoryInterface())) { + if (isTransactionNeeded(repositoryInformation.getRepositoryInterface())) { factory.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); } }); @@ -272,12 +273,12 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { return RepositoryFragments.empty(); } - private static boolean hasMethodReturningStream(Class repositoryClass) { + private static boolean isTransactionNeeded(Class repositoryClass) { Method[] methods = ReflectionUtils.getAllDeclaredMethods(repositoryClass); for (Method method : methods) { - if (Stream.class.isAssignableFrom(method.getReturnType())) { + if (Stream.class.isAssignableFrom(method.getReturnType()) || method.isAnnotationPresent(Procedure.class)) { return true; } } @@ -293,7 +294,8 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { * * @author Oliver Gierke * @since 2.0.5 - * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141 + * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141 */ private static class EclipseLinkProjectionQueryCreationListener implements QueryCreationListener { diff --git a/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java index e0931eb2d..c02dc7f1c 100644 --- a/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java @@ -47,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Thomas Darimont * @author Oliver Gierke * @author Jens Schauder + * @author Gabriel Basilio * @see scripts/schema-stored-procedures.sql for procedure definitions. */ @Transactional @@ -54,7 +55,7 @@ import org.springframework.transaction.annotation.Transactional; @ExtendWith(SpringExtension.class) public class StoredProcedureIntegrationTests { - private static final String NOT_SUPPORTED = "Stored procedures with ResultSets are currently not supported for any JPA provider"; + private static final String NOT_SUPPORTED = "Stored procedures with REF_CURSOR are currently not supported by HSQL dialect"; @PersistenceContext EntityManager em; @Autowired DummyRepository repository; diff --git a/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java index 73cc474bc..caf306b16 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java @@ -21,9 +21,11 @@ import static org.mockito.Mockito.*; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import javax.persistence.EntityManager; +import javax.persistence.ParameterMode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,8 +34,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.core.annotation.AliasFor; +import org.springframework.data.jpa.domain.sample.Dummy; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.repository.query.Param; import org.springframework.util.ReflectionUtils; @@ -47,6 +49,7 @@ import org.springframework.util.ReflectionUtils; * @author Diego Diez * @author Jeff Sheets * @author Jens Schauder + * @author Gabriel Basilio * @since 1.6 */ @ExtendWith(MockitoExtension.class) @@ -54,8 +57,7 @@ import org.springframework.util.ReflectionUtils; class StoredProcedureAttributeSourceUnitTests { private StoredProcedureAttributeSource creator; - @Mock - JpaEntityMetadata entityMetadata; + @Mock JpaEntityMetadata entityMetadata; @BeforeEach void setup() { @@ -70,10 +72,11 @@ class StoredProcedureAttributeSourceUnitTests { void shouldCreateStoredProcedureAttributesFromProcedureMethodWithImplicitProcedureName() { StoredProcedureAttributes attr = creator.createFrom(method("plus1inout", Integer.class), entityMetadata); - + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); } @Test // DATAJPA-455 @@ -82,9 +85,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); } @Test // DATAJPA-455 @@ -93,9 +98,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); } @Test // DATAJPA-455 @@ -104,9 +111,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("explicitPlus1inoutViaProcedureNameAlias", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); } @Test // DATAJPA-1297 @@ -115,9 +124,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator.createFrom( method("explicitPlus1inoutViaProcedureNameAliasAndOutputParameterName", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); } @Test // DATAJPA-455 @@ -126,9 +137,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("entityAnnotatedCustomNamedProcedurePlus1IO", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); } @Test // DATAJPA-707 @@ -137,9 +150,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("entityAnnotatedCustomNamedProcedureOutputParamNamePlus1IO", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("override"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("override"); } @Test // DATAJPA-707 @@ -148,11 +163,18 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("entityAnnotatedCustomNamedProcedurePlus1IO2", Integer.class), entityMetadata); + ProcedureParameter firstOutputParameter = attr.getOutputProcedureParameters().get(0); + ProcedureParameter secondOutputParameter = attr.getOutputProcedureParameters().get(1); + assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO2"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); - assertThat(attr.getOutputParameterTypes().get(1)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(1)).isEqualTo("res2"); + + assertThat(firstOutputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(firstOutputParameter.getType()).isEqualTo(Integer.class); + assertThat(firstOutputParameter.getName()).isEqualTo("res"); + + assertThat(secondOutputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(secondOutputParameter.getType()).isEqualTo(Integer.class); + assertThat(secondOutputParameter.getName()).isEqualTo("res2"); } @Test // DATAJPA-455 @@ -160,9 +182,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator.createFrom(method("plus1", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("User.plus1"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); } @Test // DATAJPA-871 @@ -171,9 +195,11 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("plus1inoutWithComposedAnnotationOverridingProcedureName", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); } @Test // DATAJPA-871 @@ -182,9 +208,102 @@ class StoredProcedureAttributeSourceUnitTests { StoredProcedureAttributes attr = creator .createFrom(method("plus1inoutWithComposedAnnotationOverridingName", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); assertThat(attr.getProcedureName()).isEqualTo("User.plus1"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); + } + + @Test // DATAJPA-1657 + public void testSingleEntityFrom1RowResultSetAndNoInput() { + + StoredProcedureAttributes attr = creator.createFrom(method("singleEntityFrom1RowResultSetAndNoInput"), + entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("0_input_1_row_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Dummy.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + @Test // DATAJPA-1657 + public void testSingleEntityFrom1RowResultSetWithInput() { + + StoredProcedureAttributes attr = creator.createFrom(method("singleEntityFrom1RowResultSetWithInput", Integer.class), + entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("1_input_1_row_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Dummy.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + @Test // DATAJPA-1657 + public void testEntityListFromResultSetWithNoInput() { + + StoredProcedureAttributes attr = creator.createFrom(method("entityListFromResultSetWithNoInput"), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("0_input_1_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(List.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + // + @Test // DATAJPA-1657 + public void testEntityListFromResultSetWithInput() { + + StoredProcedureAttributes attr = creator.createFrom(method("entityListFromResultSetWithInput", Integer.class), + entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("1_input_1_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(List.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + @Test // DATAJPA-1657 + public void testGenericObjectListFromResultSetWithInput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("genericObjectListFromResultSetWithInput", Integer.class), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("1_input_1_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(List.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + @Test // DATAJPA-1657 + public void testEntityListFromResultSetWithInputAndNamedOutput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFromResultSetWithInputAndNamedOutput", Integer.class), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("1_input_1_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(List.class); + assertThat(outputParameter.getName()).isEqualTo("dummies"); + } + + @Test // DATAJPA-1657 + public void testEntityListFromResultSetWithInputAndNamedOutputAndCursor() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFromResultSetWithInputAndNamedOutputAndCursor", Integer.class), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("1_input_1_resultset"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.REF_CURSOR); + assertThat(outputParameter.getType()).isEqualTo(List.class); + assertThat(outputParameter.getName()).isEqualTo("dummies"); } private static Method method(String name, Class... paramTypes) { @@ -195,8 +314,7 @@ class StoredProcedureAttributeSourceUnitTests { * @author Thomas Darimont */ @SuppressWarnings("unused") - private - interface DummyRepository { + private interface DummyRepository { /** * Explicitly mapped to a procedure with name "plus1inout" in database. @@ -235,8 +353,7 @@ class StoredProcedureAttributeSourceUnitTests { Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); /** - * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. - * With a outputParameterName + * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. With a outputParameterName */ @Procedure(name = "User.plus1IO", outputParameterName = "override") // DATAJPA-707 @@ -261,6 +378,34 @@ class StoredProcedureAttributeSourceUnitTests { @ComposedProcedureUsingAliasFor(emProcedureName = "User.plus1") Integer plus1inoutWithComposedAnnotationOverridingName(Integer arg); + + @Procedure("0_input_1_row_resultset") + // DATAJPA-1657 + Dummy singleEntityFrom1RowResultSetAndNoInput(); + + @Procedure("1_input_1_row_resultset") + // DATAJPA-1657 + Dummy singleEntityFrom1RowResultSetWithInput(Integer arg); + + @Procedure("0_input_1_resultset") + // DATAJPA-1657 + List entityListFromResultSetWithNoInput(); + + @Procedure("1_input_1_resultset") + // DATAJPA-1657 + List entityListFromResultSetWithInput(Integer arg); + + @Procedure("1_input_1_resultset") + // DATAJPA-1657 + List genericObjectListFromResultSetWithInput(Integer arg); + + @Procedure(value = "1_input_1_resultset", outputParameterName = "dummies") + // DATAJPA-1657 + List entityListFromResultSetWithInputAndNamedOutput(Integer arg); + + @Procedure(value = "1_input_1_resultset", outputParameterName = "dummies", refCursor = true) + // DATAJPA-1657 + List entityListFromResultSetWithInputAndNamedOutputAndCursor(Integer arg); } @SuppressWarnings("unused") diff --git a/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributesUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributesUnitTests.java index c63e22b22..3d1bedbbe 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributesUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributesUnitTests.java @@ -20,6 +20,8 @@ import static org.springframework.data.jpa.repository.query.StoredProcedureAttri import org.junit.jupiter.api.Test; +import javax.persistence.ParameterMode; + /** * Unit tests for {@link StoredProcedureAttributes}. * @@ -31,7 +33,8 @@ class StoredProcedureAttributesUnitTests { @Test // DATAJPA-681 void usesSyntheticOutputParameterNameForAdhocProcedureWithoutOutputName() { - StoredProcedureAttributes attributes = new StoredProcedureAttributes("procedure", null, Long.class); - assertThat(attributes.getOutputParameterNames().get(0)).isEqualTo(SYNTHETIC_OUTPUT_PARAMETER_NAME); + ProcedureParameter outputParameter = new ProcedureParameter(null, ParameterMode.OUT, Long.class); + StoredProcedureAttributes attributes = new StoredProcedureAttributes("procedure", outputParameter); + assertThat(attributes.getOutputProcedureParameters().get(0).getName()).isEqualTo(SYNTHETIC_OUTPUT_PARAMETER_NAME); } }