From 860b0e15c14da9196e1d8fe63c9a662bd1989459 Mon Sep 17 00:00:00 2001 From: Gabriel Basilio Brito Date: Tue, 14 Jan 2020 15:30:15 -0400 Subject: [PATCH 1/5] DATAJPA-1657 - Adding transaction requirement for @procedure's "InvalidDataAccessApiUsageException" will be thrown if the @procedure returns a ResultSet and no transaction is active. It is recommended to use @Transactional in this case --- .../data/jpa/repository/query/JpaQueryExecution.java | 2 ++ .../data/jpa/repository/support/JpaRepositoryFactory.java | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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 adbdd6d107..b8e7ded629 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 @@ -300,6 +300,8 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso */ 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[]) 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 7b1d0305c9..3ba6340fd4 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 @@ -37,6 +37,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.Procedure; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -92,7 +93,7 @@ public JpaRepositoryFactory(EntityManager entityManager) { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); addRepositoryProxyPostProcessor((factory, repositoryInformation) -> { - if (hasMethodReturningStream(repositoryInformation.getRepositoryInterface())) { + if (isTransactionNeeded(repositoryInformation.getRepositoryInterface())) { factory.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); } }); @@ -241,12 +242,13 @@ protected RepositoryComposition.RepositoryFragments getRepositoryFragments(Repos return fragments; } - 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; } } From 146343e54515415ac682ba3856117c05c1804761 Mon Sep 17 00:00:00 2001 From: Gabriel Basilio Brito Date: Tue, 14 Jan 2020 15:33:59 -0400 Subject: [PATCH 2/5] DATAJPA-1657 - Adding refCursor attribute to @Procedure It determines whether the procedure has a REF_CURSOR output parameter or not. Default is false --- .../data/jpa/repository/query/Procedure.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 cdfee7dcba..34343db985 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 @@ * @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 @@ * 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; } From aebd5b9944473fc12a0d218f80cf561d797b71c8 Mon Sep 17 00:00:00 2001 From: Gabriel Basilio Brito Date: Tue, 14 Jan 2020 15:41:50 -0400 Subject: [PATCH 3/5] DATAJPA-1657 - Creation of ProcedureParameter.class and refactoring Passing the stored procedure parameter properties around was not easily maintainable. I had to pass 3 Lists from place to place, each containing the 3 properties of the parameters (names, types and modes). I created a class that has the name, type and mode inside so it is cleaner to pass this values around. I couldn't name the class "StoredProcedureParameter" because javax.persistence package has an annotation named like that already --- .../repository/query/ProcedureParameter.java | 52 ++++++++++++++ .../query/StoredProcedureAttributeSource.java | 27 +++---- .../query/StoredProcedureAttributes.java | 71 +++++++++---------- 3 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java 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 0000000000..6d185e5025 --- /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 38dd10d0b5..338b5a5cc8 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 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 @@ * @author Mark Paluch * @author Diego Diez * @author Jeff Sheets + * @author Gabriel Basilio * @since 1.6 */ enum StoredProcedureAttributeSource { @@ -72,7 +74,7 @@ public StoredProcedureAttributes createFrom(Method method, JpaEntityMetadata + method); } - return new StoredProcedureAttributes(procedureName, procedure.outputParameterName(), method.getReturnType()); + return new StoredProcedureAttributes(procedureName, createOutputProcedureParameterFrom(method, procedure)); } /** @@ -102,28 +104,29 @@ private String deriveProcedureNameFrom(Method method, Procedure procedure) { 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 9d981b26b2..23424f8f49 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 @@ * @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; - } - private List completeOutputParameterNames(List outputParameterNames) { + if (namedStoredProcedure) { + this.outputProcedureParameters = outputProcedureParameters; + } else { + this.outputProcedureParameters = getParametersWithCompletedNames(outputProcedureParameters); + } + } - 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,26 +113,15 @@ public String getProcedureName() { * * @return */ - public List getOutputParameterNames() { - return outputParameterNames; - } - - /** - * Returns the types of the output parameters. - * - * @return - */ - public List> getOutputParameterTypes() { - return outputParameterTypes; + public boolean isNamedStoredProcedure() { + return namedStoredProcedure; } /** - * Returns whether the stored procedure is a named one. - * - * @return + * @return Returns the stored procedure output parameter list */ - public boolean isNamedStoredProcedure() { - return namedStoredProcedure; + public List getOutputProcedureParameters() { + return outputProcedureParameters; } /** @@ -141,6 +130,10 @@ public boolean isNamedStoredProcedure() { * @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)); } } From 61ca9c96feb1734997dc6d7597fb9c90fad5bab8 Mon Sep 17 00:00:00 2001 From: Gabriel Basilio Brito Date: Tue, 14 Jan 2020 15:55:10 -0400 Subject: [PATCH 4/5] DATAJPA-1657 - Return ResultSets from @Procedure with or without cursor javax.persistence.StoredProcedureQuery.execute method tells us if a ResultSet is being returned or not. This is the key to finally decide what to return: Output parameter values or javax.persistence.StoredProcedureQuery.getResultList(). I added a logic to return single entities from it if the method annotated with @Procedure does not have a Collection return type. When registering the procedure output parameters I needed to find a way to predict if the user is intending to return a ResultSet or not. I ended up doing it with this condition: If the method annotated with @Procedure has a collection return type or it doesn't have a collection but it has an entity return type then I know for sure that a ResultSet is intended to be returned because regular output parameters are NEVER returned in collections but in arrays (Object[]) and also regular output parameters don't return entities, but primitive types. --- .../repository/query/JpaQueryExecution.java | 20 ++++- .../query/StoredProcedureJpaQuery.java | 85 ++++++++++--------- 2 files changed, 65 insertions(+), 40 deletions(-) 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 b8e7ded629..732d8ebf99 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 @@ * @author Christoph Strobl * @author Nicolas Cirigliano * @author Jens Schauder + * @author Gabriel Basilio */ public abstract class JpaQueryExecution { @@ -313,7 +314,24 @@ protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAcce 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/StoredProcedureJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index d8e9dc38aa..c3f589ff54 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 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 @@ Object extractOutputValue(StoredProcedureQuery storedProcedureQuery) { return null; } - Map outputValues = new HashMap<>(); - List parameterNames = procedureAttributes.getOutputParameterNames(); - - for (int i = 0; i < parameterNames.size(); i++) { + List outputParameters = procedureAttributes.getOutputProcedureParameters(); - 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; - } - - private Object extractOutputParameter(StoredProcedureQuery storedProcedureQuery, Integer index) { + Map outputValues = new HashMap<>(); - String outputParameterName = procedureAttributes.getOutputParameterNames().get(index); - JpaParameters parameters = getQueryMethod().getParameters(); + for (int i = 0; i < outputParameters.size(); i++) { + ProcedureParameter outputParameter = outputParameters.get(i); + outputValues.put(outputParameter.getName(), extractOutputParameterValue(outputParameter, i, storedProcedureQuery)); + } - 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) { + + JpaParameters methodParameters = getQueryMethod().getParameters(); - return useNamedParameters && StringUtils.hasText(name) ? // - storedProcedureQuery.getOutputParameterValue(name) - : storedProcedureQuery.getOutputParameterValue(offset + index + 1); + return useNamedParameters && StringUtils.hasText(outputParameter.getName()) ? + storedProcedureQuery.getOutputParameterValue(outputParameter.getName()) + : storedProcedureQuery.getOutputParameterValue(methodParameters.getNumberOfParameters() + index + 1); } /** @@ -192,9 +183,7 @@ private StoredProcedureQuery newNamedStoredProcedureQuery() { 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 @@ private StoredProcedureQuery newAdhocStoredProcedureQuery() { 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(); + } } From 39bc20fa68f255828038fc24318fcf0c112b1091 Mon Sep 17 00:00:00 2001 From: Gabriel Basilio Brito Date: Tue, 14 Jan 2020 16:00:37 -0400 Subject: [PATCH 5/5] DATAJPA-1657 - Adding UnitTests I wrote unit tests but unfortunately we can't make integration tests for this change because the HSQL database dialect doesn't work with procedure returning ResultSets. But MySQL, Oracle, Postgres and SQL Server dialects do work with it. Check this project that tests with all those databases: https://github.com/GabrielBB/spring-data-jpa-procedure-tests --- .../StoredProcedureIntegrationTests.java | 3 +- ...oredProcedureAttributeSourceUnitTests.java | 201 +++++++++++++++--- .../StoredProcedureAttributesUnitTests.java | 7 +- 3 files changed, 183 insertions(+), 28 deletions(-) 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 7065af25fc..32634798bf 100644 --- a/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/StoredProcedureIntegrationTests.java @@ -46,6 +46,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Jens Schauder + * @author Gabriel Basilio * @see scripts/schema-stored-procedures.sql for procedure definitions. */ @Transactional @@ -53,7 +54,7 @@ @RunWith(SpringJUnit4ClassRunner.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 ad6ddd72e5..387444d68f 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,14 +21,17 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; 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; import javax.persistence.EntityManager; +import javax.persistence.ParameterMode; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.*; @@ -43,6 +46,7 @@ * @author Diego Diez * @author Jeff Sheets * @author Jens Schauder + * @author Gabriel Basilio * @since 1.6 */ @RunWith(MockitoJUnitRunner.class) @@ -65,10 +69,11 @@ public void setup() { public 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 @@ -77,9 +82,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictN 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 @@ -88,9 +95,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictP 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 @@ -99,9 +108,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictP 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 @@ -110,9 +121,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictP 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 @@ -121,9 +134,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithEx 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 @@ -132,9 +147,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithEx 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 @@ -143,11 +160,18 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithEx 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 @@ -155,9 +179,11 @@ public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithIm 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 @@ -166,9 +192,11 @@ public void aliasedStoredProcedure() { 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 @@ -177,9 +205,104 @@ public void aliasedStoredProcedure2() { 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) { @@ -255,6 +378,34 @@ interface DummyRepository { @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 f3c0eada8a..36ae3b3124 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 org.junit.Test; +import javax.persistence.ParameterMode; + /** * Unit tests for {@link StoredProcedureAttributes}. * @@ -31,7 +33,8 @@ public class StoredProcedureAttributesUnitTests { @Test // DATAJPA-681 public 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); } }