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..9645074da1 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 { @@ -300,6 +301,7 @@ 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[]) @@ -311,9 +313,19 @@ protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAcce StoredProcedureJpaQuery storedProcedureJpaQuery = (StoredProcedureJpaQuery) jpaQuery; StoredProcedureQuery storedProcedure = storedProcedureJpaQuery.createQuery(accessor); - storedProcedure.execute(); - return storedProcedureJpaQuery.extractOutputValue(storedProcedure); + boolean returnsResultSet = storedProcedure.execute(); + + if(returnsResultSet) { + if(!SurroundingTransactionDetectorMethodInterceptor.INSTANCE.isSurroundingTransactionActive()) + throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION); + + List result = storedProcedure.getResultList(); + return result.size() == 1 + && !storedProcedureJpaQuery.getQueryMethod().isCollectionQuery() ? result.get(0) : result; + } + + return storedProcedureJpaQuery.extractOutputParametersValues(storedProcedure); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index fc53496655..3344efbd80 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -57,373 +57,374 @@ */ public class JpaQueryMethod extends QueryMethod { - /** - * @see JPA - * 2.0 Specification 2.2 Persistent Fields and Properties Page 23 - Top paragraph. - */ - private static final Set> NATIVE_ARRAY_TYPES; - private static final StoredProcedureAttributeSource storedProcedureAttributeSource = StoredProcedureAttributeSource.INSTANCE; - - static { - - Set> types = new HashSet<>(); - types.add(byte[].class); - types.add(Byte[].class); - types.add(char[].class); - types.add(Character[].class); - - NATIVE_ARRAY_TYPES = Collections.unmodifiableSet(types); - } - - private final QueryExtractor extractor; - private final Method method; - - private @Nullable StoredProcedureAttributes storedProcedureAttributes; - private final Lazy lockModeType; - private final Lazy queryHints; - private final Lazy jpaEntityGraph; - private final Lazy modifying; - private final Lazy isNativeQuery; - private final Lazy isCollectionQuery; - private final Lazy isProcedureQuery; - private final Lazy> entityMetadata; - - /** - * Creates a {@link JpaQueryMethod}. - * - * @param method must not be {@literal null} - * @param metadata must not be {@literal null} - * @param factory must not be {@literal null} - * @param extractor must not be {@literal null} - */ - public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, - QueryExtractor extractor) { - - super(method, metadata, factory); - - Assert.notNull(method, "Method must not be null!"); - Assert.notNull(extractor, "Query extractor must not be null!"); - - this.method = method; - this.extractor = extractor; - this.lockModeType = Lazy - .of(() -> (LockModeType) Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class)) // - .map(AnnotationUtils::getValue) // - .orElse(null)); - - this.queryHints = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class)); - this.modifying = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, Modifying.class)); - this.jpaEntityGraph = Lazy.of(() -> { - - EntityGraph entityGraph = AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class); - - if (entityGraph == null) { - return null; - } - - return new JpaEntityGraph(entityGraph, getNamedQueryName()); - }); - this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class)); - this.isCollectionQuery = Lazy - .of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(method.getReturnType())); - this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null); - this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass())); - - Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()), - String.format("Modifying method must not contain %s!", Parameters.TYPES)); - assertParameterNamesInAnnotatedQuery(); - } - - private void assertParameterNamesInAnnotatedQuery() { - - String annotatedQuery = getAnnotatedQuery(); - - if (!DeclaredQuery.of(annotatedQuery).hasNamedParameter()) { - return; - } - - for (Parameter parameter : getParameters()) { - - if (!parameter.isNamedParameter()) { - continue; - } - - if (StringUtils.isEmpty(annotatedQuery) - || !annotatedQuery.contains(String.format(":%s", parameter.getName().get())) - && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) { - throw new IllegalStateException( - String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'!", - method, parameter.getName(), annotatedQuery)); - } - } - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#getEntityInformation() - */ - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public JpaEntityMetadata getEntityInformation() { - return this.entityMetadata.get(); - } - - /** - * Returns whether the finder is a modifying one. - * - * @return - */ - @Override - public boolean isModifyingQuery() { - return modifying.getNullable() != null; - } - - /** - * Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints} - * - * @return - */ - List getHints() { - - QueryHints hints = this.queryHints.getNullable(); - if (hints != null) { - return Arrays.asList(hints.value()); - } - - return Collections.emptyList(); - } - - /** - * Returns the {@link LockModeType} to be used for the query. - * - * @return - */ - @Nullable - LockModeType getLockModeType() { - return lockModeType.getNullable(); - } - - /** - * Returns the {@link EntityGraph} to be used for the query. - * - * @return - * @since 1.6 - */ - @Nullable - JpaEntityGraph getEntityGraph() { - return jpaEntityGraph.getNullable(); - } - - /** - * Returns whether the potentially configured {@link QueryHint}s shall be applied when triggering the count query for - * pagination. - * - * @return - */ - boolean applyHintsToCountQuery() { - - QueryHints hints = this.queryHints.getNullable(); - return hints != null ? hints.forCounting() : false; - } - - /** - * Returns the {@link QueryExtractor}. - * - * @return - */ - QueryExtractor getQueryExtractor() { - return extractor; - } - - /** - * Returns the actual return type of the method. - * - * @return - */ - Class getReturnType() { - return method.getReturnType(); - } - - /** - * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found - * nor the attribute was specified. - * - * @return - */ - @Nullable - String getAnnotatedQuery() { - - String query = getAnnotationValue("value", String.class); - return StringUtils.hasText(query) ? query : null; - } - - /** - * Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if - * neither the annotation found nor the attribute was specified. - * - * @return - * @throws IllegalStateException if no {@link Query} annotation is present or the query is empty. - * @since 2.0 - */ - String getRequiredAnnotatedQuery() throws IllegalStateException { - - String query = getAnnotatedQuery(); - - if (query != null) { - return query; - } - - throw new IllegalStateException(String.format("No annotated query found for query method %s!", getName())); - } - - /** - * Returns the countQuery string declared in a {@link Query} annotation or {@literal null} if neither the annotation - * found nor the attribute was specified. - * - * @return - */ - @Nullable - String getCountQuery() { - - String countQuery = getAnnotationValue("countQuery", String.class); - return StringUtils.hasText(countQuery) ? countQuery : null; - } - - /** - * Returns the count query projection string declared in a {@link Query} annotation or {@literal null} if neither the - * annotation found nor the attribute was specified. - * - * @return - * @since 1.6 - */ - @Nullable - String getCountQueryProjection() { - - String countProjection = getAnnotationValue("countProjection", String.class); - return StringUtils.hasText(countProjection) ? countProjection : null; - } - - /** - * Returns whether the backing query is a native one. - * - * @return - */ - boolean isNativeQuery() { - return this.isNativeQuery.get(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#getNamedQueryName() - */ - @Override - public String getNamedQueryName() { - - String annotatedName = getAnnotationValue("name", String.class); - return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName(); - } - - /** - * Returns the name of the {@link NamedQuery} that shall be used for count queries. - * - * @return - */ - String getNamedCountQueryName() { - - String annotatedName = getAnnotationValue("countName", String.class); - return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count"; - } - - /** - * Returns whether we should flush automatically for modifying queries. - * - * @return whether we should flush automatically. - */ - boolean getFlushAutomatically() { - return getMergedOrDefaultAnnotationValue("flushAutomatically", Modifying.class, Boolean.class); - } - - /** - * Returns whether we should clear automatically for modifying queries. - * - * @return whether we should clear automatically. - */ - boolean getClearAutomatically() { - return getMergedOrDefaultAnnotationValue("clearAutomatically", Modifying.class, Boolean.class); - } - - /** - * Returns the {@link Query} annotation's attribute casted to the given type or default value if no annotation - * available. - * - * @param attribute - * @param type - * @return - */ - private T getAnnotationValue(String attribute, Class type) { - return getMergedOrDefaultAnnotationValue(attribute, Query.class, type); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class targetType) { - - Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType); - if (annotation == null) { - return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute)); - } - - return targetType.cast(AnnotationUtils.getValue(annotation, attribute)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) - */ - @Override - protected JpaParameters createParameters(Method method) { - return new JpaParameters(method); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#getParameters() - */ - @Override - public JpaParameters getParameters() { - return (JpaParameters) super.getParameters(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery() - */ - @Override - public boolean isCollectionQuery() { - return this.isCollectionQuery.get(); - } - - /** - * Return {@literal true} if the method contains a {@link Procedure} annotation. - * - * @return - */ - public boolean isProcedureQuery() { - return this.isProcedureQuery.get(); - } - - /** - * Returns a new {@link StoredProcedureAttributes} representing the stored procedure meta-data for this - * {@link JpaQueryMethod}. - * - * @return - */ - StoredProcedureAttributes getProcedureAttributes() { - - if (storedProcedureAttributes == null) { - this.storedProcedureAttributes = storedProcedureAttributeSource.createFrom(method, getEntityInformation()); - } - - return storedProcedureAttributes; - } + /** + * @see JPA + * 2.0 Specification 2.2 Persistent Fields and Properties Page 23 - Top paragraph. + */ + private static final Set> NATIVE_ARRAY_TYPES; + private static final StoredProcedureAttributeSource storedProcedureAttributeSource = StoredProcedureAttributeSource.INSTANCE; + + static { + + Set> types = new HashSet<>(); + types.add(byte[].class); + types.add(Byte[].class); + types.add(char[].class); + types.add(Character[].class); + + NATIVE_ARRAY_TYPES = Collections.unmodifiableSet(types); + } + + private final QueryExtractor extractor; + private final Method method; + + private @Nullable + StoredProcedureAttributes storedProcedureAttributes; + private final Lazy lockModeType; + private final Lazy queryHints; + private final Lazy jpaEntityGraph; + private final Lazy modifying; + private final Lazy isNativeQuery; + private final Lazy isCollectionQuery; + private final Lazy isProcedureQuery; + private final Lazy> entityMetadata; + + /** + * Creates a {@link JpaQueryMethod}. + * + * @param method must not be {@literal null} + * @param metadata must not be {@literal null} + * @param factory must not be {@literal null} + * @param extractor must not be {@literal null} + */ + public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + QueryExtractor extractor) { + + super(method, metadata, factory); + + Assert.notNull(method, "Method must not be null!"); + Assert.notNull(extractor, "Query extractor must not be null!"); + + this.method = method; + this.extractor = extractor; + this.lockModeType = Lazy + .of(() -> (LockModeType) Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class)) // + .map(AnnotationUtils::getValue) // + .orElse(null)); + + this.queryHints = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class)); + this.modifying = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, Modifying.class)); + this.jpaEntityGraph = Lazy.of(() -> { + + EntityGraph entityGraph = AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class); + + if (entityGraph == null) { + return null; + } + + return new JpaEntityGraph(entityGraph, getNamedQueryName()); + }); + this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class)); + this.isCollectionQuery = Lazy + .of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(method.getReturnType())); + this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null); + this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass())); + + Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()), + String.format("Modifying method must not contain %s!", Parameters.TYPES)); + assertParameterNamesInAnnotatedQuery(); + } + + private void assertParameterNamesInAnnotatedQuery() { + + String annotatedQuery = getAnnotatedQuery(); + + if (!DeclaredQuery.of(annotatedQuery).hasNamedParameter()) { + return; + } + + for (Parameter parameter : getParameters()) { + + if (!parameter.isNamedParameter()) { + continue; + } + + if (StringUtils.isEmpty(annotatedQuery) + || !annotatedQuery.contains(String.format(":%s", parameter.getName().get())) + && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) { + throw new IllegalStateException( + String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'!", + method, parameter.getName(), annotatedQuery)); + } + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getEntityInformation() + */ + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public JpaEntityMetadata getEntityInformation() { + return this.entityMetadata.get(); + } + + /** + * Returns whether the finder is a modifying one. + * + * @return + */ + @Override + public boolean isModifyingQuery() { + return modifying.getNullable() != null; + } + + /** + * Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints} + * + * @return + */ + List getHints() { + + QueryHints hints = this.queryHints.getNullable(); + if (hints != null) { + return Arrays.asList(hints.value()); + } + + return Collections.emptyList(); + } + + /** + * Returns the {@link LockModeType} to be used for the query. + * + * @return + */ + @Nullable + LockModeType getLockModeType() { + return lockModeType.getNullable(); + } + + /** + * Returns the {@link EntityGraph} to be used for the query. + * + * @return + * @since 1.6 + */ + @Nullable + JpaEntityGraph getEntityGraph() { + return jpaEntityGraph.getNullable(); + } + + /** + * Returns whether the potentially configured {@link QueryHint}s shall be applied when triggering the count query for + * pagination. + * + * @return + */ + boolean applyHintsToCountQuery() { + + QueryHints hints = this.queryHints.getNullable(); + return hints != null ? hints.forCounting() : false; + } + + /** + * Returns the {@link QueryExtractor}. + * + * @return + */ + QueryExtractor getQueryExtractor() { + return extractor; + } + + /** + * Returns the actual return type of the method. + * + * @return + */ + Class getReturnType() { + return method.getReturnType(); + } + + /** + * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found + * nor the attribute was specified. + * + * @return + */ + @Nullable + String getAnnotatedQuery() { + + String query = getAnnotationValue("value", String.class); + return StringUtils.hasText(query) ? query : null; + } + + /** + * Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if + * neither the annotation found nor the attribute was specified. + * + * @return + * @throws IllegalStateException if no {@link Query} annotation is present or the query is empty. + * @since 2.0 + */ + String getRequiredAnnotatedQuery() throws IllegalStateException { + + String query = getAnnotatedQuery(); + + if (query != null) { + return query; + } + + throw new IllegalStateException(String.format("No annotated query found for query method %s!", getName())); + } + + /** + * Returns the countQuery string declared in a {@link Query} annotation or {@literal null} if neither the annotation + * found nor the attribute was specified. + * + * @return + */ + @Nullable + String getCountQuery() { + + String countQuery = getAnnotationValue("countQuery", String.class); + return StringUtils.hasText(countQuery) ? countQuery : null; + } + + /** + * Returns the count query projection string declared in a {@link Query} annotation or {@literal null} if neither the + * annotation found nor the attribute was specified. + * + * @return + * @since 1.6 + */ + @Nullable + String getCountQueryProjection() { + + String countProjection = getAnnotationValue("countProjection", String.class); + return StringUtils.hasText(countProjection) ? countProjection : null; + } + + /** + * Returns whether the backing query is a native one. + * + * @return + */ + boolean isNativeQuery() { + return this.isNativeQuery.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getNamedQueryName() + */ + @Override + public String getNamedQueryName() { + + String annotatedName = getAnnotationValue("name", String.class); + return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName(); + } + + /** + * Returns the name of the {@link NamedQuery} that shall be used for count queries. + * + * @return + */ + String getNamedCountQueryName() { + + String annotatedName = getAnnotationValue("countName", String.class); + return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count"; + } + + /** + * Returns whether we should flush automatically for modifying queries. + * + * @return whether we should flush automatically. + */ + boolean getFlushAutomatically() { + return getMergedOrDefaultAnnotationValue("flushAutomatically", Modifying.class, Boolean.class); + } + + /** + * Returns whether we should clear automatically for modifying queries. + * + * @return whether we should clear automatically. + */ + boolean getClearAutomatically() { + return getMergedOrDefaultAnnotationValue("clearAutomatically", Modifying.class, Boolean.class); + } + + /** + * Returns the {@link Query} annotation's attribute casted to the given type or default value if no annotation + * available. + * + * @param attribute + * @param type + * @return + */ + private T getAnnotationValue(String attribute, Class type) { + return getMergedOrDefaultAnnotationValue(attribute, Query.class, type); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class targetType) { + + Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType); + if (annotation == null) { + return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute)); + } + + return targetType.cast(AnnotationUtils.getValue(annotation, attribute)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) + */ + @Override + protected JpaParameters createParameters(Method method) { + return new JpaParameters(method); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getParameters() + */ + @Override + public JpaParameters getParameters() { + return (JpaParameters) super.getParameters(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery() + */ + @Override + public boolean isCollectionQuery() { + return this.isCollectionQuery.get(); + } + + /** + * Return {@literal true} if the method contains a {@link Procedure} annotation. + * + * @return + */ + public boolean isProcedureQuery() { + return this.isProcedureQuery.get(); + } + + /** + * Returns a new {@link StoredProcedureAttributes} representing the stored procedure meta-data for this + * {@link JpaQueryMethod}. + * + * @return + */ + StoredProcedureAttributes getProcedureAttributes() { + + if (storedProcedureAttributes == null) { + this.storedProcedureAttributes = storedProcedureAttributeSource.createFrom(method, getEntityInformation()); + } + + return storedProcedureAttributes; + } } 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..fef0ac184b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -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; } 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..7cfa04205f --- /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..36815b8d99 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -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,180 +39,180 @@ * @author Mark Paluch * @author Diego Diez * @author Jeff Sheets + * @author Gabriel Basilio * @since 1.6 */ enum StoredProcedureAttributeSource { - INSTANCE; - - /** - * Creates a new {@link StoredProcedureAttributes} from the given {@link Method} and {@link JpaEntityMetadata}. - * - * @param method must not be {@literal null} - * @param entityMetadata must not be {@literal null} - * @return - */ - public StoredProcedureAttributes createFrom(Method method, JpaEntityMetadata entityMetadata) { - - Assert.notNull(method, "Method must not be null!"); - Assert.notNull(entityMetadata, "EntityMetadata must not be null!"); - - Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class); - Assert.notNull(procedure, "Method must have an @Procedure annotation!"); - - NamedStoredProcedureQuery namedStoredProc = tryFindAnnotatedNamedStoredProcedureQuery(method, entityMetadata, - procedure); - - if (namedStoredProc != null) { - return newProcedureAttributesFrom(method, namedStoredProc, procedure); - } - - String procedureName = deriveProcedureNameFrom(method, procedure); - if (StringUtils.isEmpty(procedureName)) { - throw new IllegalArgumentException("Could not determine name of procedure for @Procedure annotated method: " - + method); - } - - return new StoredProcedureAttributes(procedureName, procedure.outputParameterName(), method.getReturnType()); - } - - /** - * Tries to derive the procedure name from the given {@link Procedure}, falls back to the name of the given - * {@link Method}. - * - * @param method - * @param procedure - * @return - */ - private String deriveProcedureNameFrom(Method method, Procedure procedure) { - - if (StringUtils.hasText(procedure.value())) { - return procedure.value(); - } - - String procedureName = procedure.procedureName(); - return StringUtils.hasText(procedureName) ? procedureName : method.getName(); - } - - /** - * @param method - * @param namedStoredProc - * @param procedure - * @return - */ - private StoredProcedureAttributes newProcedureAttributesFrom(Method method, - NamedStoredProcedureQuery namedStoredProc, Procedure procedure) { - - List outputParameterNames = new ArrayList<>(); - List> outputParameterTypes = new ArrayList<>(); - - if (!procedure.outputParameterName().isEmpty()) { - // we give the output parameter definition from the @Procedure annotation precedence - outputParameterNames.add(procedure.outputParameterName()); - } else { - - // try to discover the output parameter - List outputParameters = extractOutputParametersFrom(namedStoredProc); - - for (StoredProcedureParameter outputParameter : outputParameters) { - outputParameterNames.add(outputParameter.name()); - outputParameterTypes.add(outputParameter.type()); - } - } - - if (outputParameterTypes.isEmpty()) { - outputParameterTypes.add(method.getReturnType()); - } - - return new StoredProcedureAttributes(namedStoredProc.name(), outputParameterNames, outputParameterTypes, true); - } - - private List extractOutputParametersFrom(NamedStoredProcedureQuery namedStoredProc) { - - List outputParameters = new ArrayList(); - - for (StoredProcedureParameter param : namedStoredProc.parameters()) { - - switch (param.mode()) { - case OUT: - case INOUT: - case REF_CURSOR: - outputParameters.add(param); - break; - case IN: - default: - continue; - } - } - - return outputParameters; - } - - /** - * @param method must not be {@literal null}. - * @param entityMetadata must not be {@literal null}. - * @param procedure must not be {@literal null}. - * @return - */ - @Nullable - private NamedStoredProcedureQuery tryFindAnnotatedNamedStoredProcedureQuery(Method method, - JpaEntityMetadata entityMetadata, Procedure procedure) { - - Assert.notNull(method, "Method must not be null!"); - Assert.notNull(entityMetadata, "EntityMetadata must not be null!"); - Assert.notNull(procedure, "Procedure must not be null!"); - - Class entityType = entityMetadata.getJavaType(); - - List queries = collectNamedStoredProcedureQueriesFrom(entityType); - - if (queries.isEmpty()) { - return null; - } - - String namedProcedureName = derivedNamedProcedureNameFrom(method, entityMetadata, procedure); - - for (NamedStoredProcedureQuery query : queries) { - - if (query.name().equals(namedProcedureName)) { - return query; - } - } - - return null; - } - - /** - * @param method - * @param entityMetadata - * @param procedure - * @return - */ - private String derivedNamedProcedureNameFrom(Method method, JpaEntityMetadata entityMetadata, Procedure procedure) { - return StringUtils.hasText(procedure.name()) ? procedure.name() : entityMetadata.getEntityName() + "." - + method.getName(); - } - - /** - * @param entityType - * @return - */ - private List collectNamedStoredProcedureQueriesFrom(Class entityType) { - - List queries = new ArrayList(); - - NamedStoredProcedureQueries namedQueriesAnnotation = AnnotatedElementUtils.findMergedAnnotation(entityType, - NamedStoredProcedureQueries.class); - if (namedQueriesAnnotation != null) { - queries.addAll(Arrays.asList(namedQueriesAnnotation.value())); - } - - NamedStoredProcedureQuery namedQueryAnnotation = AnnotatedElementUtils.findMergedAnnotation(entityType, - NamedStoredProcedureQuery.class); - if (namedQueryAnnotation != null) { - queries.add(namedQueryAnnotation); - } - - return queries; - } + INSTANCE; + + /** + * Creates a new {@link StoredProcedureAttributes} from the given {@link Method} and {@link JpaEntityMetadata}. + * + * @param method must not be {@literal null} + * @param entityMetadata must not be {@literal null} + * @return + */ + public StoredProcedureAttributes createFrom(Method method, JpaEntityMetadata entityMetadata) { + + Assert.notNull(method, "Method must not be null!"); + Assert.notNull(entityMetadata, "EntityMetadata must not be null!"); + + Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class); + Assert.notNull(procedure, "Method must have an @Procedure annotation!"); + + NamedStoredProcedureQuery namedStoredProc = tryFindAnnotatedNamedStoredProcedureQuery(method, entityMetadata, + procedure); + + if (namedStoredProc != null) { + return newProcedureAttributesFrom(method, namedStoredProc, procedure); + } + + String procedureName = deriveProcedureNameFrom(method, procedure); + if (StringUtils.isEmpty(procedureName)) { + throw new IllegalArgumentException("Could not determine name of procedure for @Procedure annotated method: " + + method); + } + + return new StoredProcedureAttributes(procedureName, createOutputProcedureParameterFrom(method, procedure)); + } + + /** + * Tries to derive the procedure name from the given {@link Procedure}, falls back to the name of the given + * {@link Method}. + * + * @param method + * @param procedure + * @return + */ + private String deriveProcedureNameFrom(Method method, Procedure procedure) { + + if (StringUtils.hasText(procedure.value())) { + return procedure.value(); + } + + String procedureName = procedure.procedureName(); + return StringUtils.hasText(procedureName) ? procedureName : method.getName(); + } + + /** + * @param method + * @param namedStoredProc + * @param procedure + * @return + */ + private StoredProcedureAttributes newProcedureAttributesFrom(Method method, + NamedStoredProcedureQuery namedStoredProc, Procedure procedure) { + + List outputParameters = new ArrayList<>(); + + if (!procedure.outputParameterName().isEmpty()) { + // we give the output parameter definition from the @Procedure annotation precedence + outputParameters.add(createOutputProcedureParameterFrom(method, procedure)); + } else { + + // try to discover the output parameter + List namedProcedureOutputParameters = extractOutputParametersFrom(namedStoredProc); + + for (StoredProcedureParameter outputParameter : namedProcedureOutputParameters) { + outputParameters.add(new ProcedureParameter( + outputParameter.name(), outputParameter.mode(), outputParameter.type())); + } + } + + return new StoredProcedureAttributes(namedStoredProc.name(), outputParameters, 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) { + + List outputParameters = new ArrayList<>(); + + for (StoredProcedureParameter param : namedStoredProc.parameters()) { + + switch (param.mode()) { + case OUT: + case INOUT: + case REF_CURSOR: + outputParameters.add(param); + break; + case IN: + } + } + + return outputParameters; + } + + /** + * @param method must not be {@literal null}. + * @param entityMetadata must not be {@literal null}. + * @param procedure must not be {@literal null}. + * @return + */ + @Nullable + private NamedStoredProcedureQuery tryFindAnnotatedNamedStoredProcedureQuery(Method method, + JpaEntityMetadata entityMetadata, Procedure procedure) { + + Assert.notNull(method, "Method must not be null!"); + Assert.notNull(entityMetadata, "EntityMetadata must not be null!"); + Assert.notNull(procedure, "Procedure must not be null!"); + + Class entityType = entityMetadata.getJavaType(); + + List queries = collectNamedStoredProcedureQueriesFrom(entityType); + + if (queries.isEmpty()) { + return null; + } + + String namedProcedureName = derivedNamedProcedureNameFrom(method, entityMetadata, procedure); + + for (NamedStoredProcedureQuery query : queries) { + + if (query.name().equals(namedProcedureName)) { + return query; + } + } + + return null; + } + + /** + * @param method + * @param entityMetadata + * @param procedure + * @return + */ + private String derivedNamedProcedureNameFrom(Method method, JpaEntityMetadata entityMetadata, Procedure procedure) { + return StringUtils.hasText(procedure.name()) ? procedure.name() : entityMetadata.getEntityName() + "." + + method.getName(); + } + + /** + * @param entityType + * @return + */ + private List collectNamedStoredProcedureQueriesFrom(Class entityType) { + + List queries = new ArrayList<>(); + + NamedStoredProcedureQueries namedQueriesAnnotation = AnnotatedElementUtils.findMergedAnnotation(entityType, + NamedStoredProcedureQueries.class); + if (namedQueriesAnnotation != null) { + queries.addAll(Arrays.asList(namedQueriesAnnotation.value())); + } + + NamedStoredProcedureQuery namedQueryAnnotation = AnnotatedElementUtils.findMergedAnnotation(entityType, + NamedStoredProcedureQuery.class); + if (namedQueryAnnotation != null) { + queries.add(namedQueryAnnotation); + } + + return queries; + } } 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..988bbddcba 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -15,7 +15,6 @@ */ package org.springframework.data.jpa.repository.query; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -33,114 +32,101 @@ * @author Mark Paluch * @author Jeff Sheets * @author Jens Schauder + * @author Gabriel Basilio * @since 1.6 */ class StoredProcedureAttributes { - // A synthetic output parameter name to be used in case of derived stored procedures and named parameters - static final String SYNTHETIC_OUTPUT_PARAMETER_NAME = "out"; - - private final boolean namedStoredProcedure; - private final String procedureName; - private final List outputParameterNames; - private final List> outputParameterTypes; - - /** - * 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); - } - - /** - * 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) { - - 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"); - - this.procedureName = procedureName; - this.outputParameterNames = namedStoredProcedure - ? outputParameterNames - : completeOutputParameterNames(outputParameterNames); - this.outputParameterTypes = outputParameterTypes; - this.namedStoredProcedure = namedStoredProcedure; - } - - private List completeOutputParameterNames(List outputParameterNames) { - - return IntStream.range(0, outputParameterNames.size()) // - .mapToObj(i -> completeOutputParameterName(i, outputParameterNames.get(i))) // - .collect(Collectors.toList()); - } - - private String completeOutputParameterName(int i, String paramName) { - - return StringUtils.hasText(paramName) // - ? paramName // - : createSyntheticParameterName(i); - } - - private String createSyntheticParameterName(int i) { - return SYNTHETIC_OUTPUT_PARAMETER_NAME + (i == 0 ? "" : i); - } - - /** - * Returns the name of the stored procedure. - * - * @return - */ - public String getProcedureName() { - return procedureName; - } - - /** - * Returns the names of the output parameters. - * - * @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; - } - - /** - * 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)))); - } + // A synthetic output parameter name to be used in case of derived stored procedures and named parameters + static final String SYNTHETIC_OUTPUT_PARAMETER_NAME = "out"; + + private final boolean namedStoredProcedure; + private final String procedureName; + private final List outputProcedureParameters; + + /** + * Creates a new {@link StoredProcedureAttributes}. + * + * @param procedureName must not be {@literal null}. + */ + StoredProcedureAttributes(String procedureName, ProcedureParameter parameter) { + this(procedureName, Collections.singletonList(parameter), false); + } + + /** + * Creates a new {@link StoredProcedureAttributes}. + * + * @param procedureName must not be {@literal null}. + * @param namedStoredProcedure flag signaling if the stored procedure has a name. + */ + StoredProcedureAttributes(String procedureName, List outputProcedureParameters, boolean namedStoredProcedure) { + + Assert.notNull(procedureName, "ProcedureName must not be null!"); + 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.namedStoredProcedure = namedStoredProcedure; + + if (namedStoredProcedure) { + this.outputProcedureParameters = outputProcedureParameters; + } else { + this.outputProcedureParameters = getParametersWithCompletedNames(outputProcedureParameters); + } + } + + 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 index) { + return new ProcedureParameter( + completeOutputParameterName(index, parameter.getName()), + parameter.getMode(), parameter.getType()); + } + + private String completeOutputParameterName(int index, String paramName) { + + return StringUtils.hasText(paramName) // + ? paramName // + : createSyntheticParameterName(index); + } + + private String createSyntheticParameterName(int i) { + return SYNTHETIC_OUTPUT_PARAMETER_NAME + (i == 0 ? "" : i); + } + + /** + * @return Returns the name of the stored procedure. + */ + public String getProcedureName() { + return procedureName; + } + + /** + * @return Returns whether the stored procedure is a named one. + */ + public boolean isNamedStoredProcedure() { + return namedStoredProcedure; + } + + /** + * @return Returns the stored procedure output parameter list + */ + public List getOutputProcedureParameters() { + return outputProcedureParameters; + } + + /** + * @return Returns whether the stored procedure will produce a result. + */ + public boolean hasReturnValue() { + if (getOutputProcedureParameters().isEmpty()) + return false; + + Class firstOutputType = getOutputProcedureParameters().get(0).getType(); + return !(void.class.equals(firstOutputType) || Void.class.equals(firstOutputType)); + } } 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..87d42e1cfb 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 @@ -18,7 +18,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.IntStream; import javax.persistence.EntityManager; import javax.persistence.NamedStoredProcedureQuery; @@ -28,9 +27,7 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.ResultProcessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,191 +43,187 @@ * @author Mark Paluch * @author Jeff Sheets * @author JyotirmoyVS + * @author Gabriel Basilio * @since 1.6 */ class StoredProcedureJpaQuery extends AbstractJpaQuery { - private final StoredProcedureAttributes procedureAttributes; - private final boolean useNamedParameters; - private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); - - /** - * Creates a new {@link StoredProcedureJpaQuery}. - * - * @param method must not be {@literal null} - * @param em must not be {@literal null} - */ - StoredProcedureJpaQuery(JpaQueryMethod method, EntityManager em) { - - super(method, em); - this.procedureAttributes = method.getProcedureAttributes(); - this.useNamedParameters = useNamedParameters(method); - - } - - /** - * Determine whether to used named parameters for the given query method. - * - * @param method must not be {@literal null}. - */ - private static boolean useNamedParameters(QueryMethod method) { - - for (Parameter parameter : method.getParameters()) { - if (parameter.isNamedParameter()) { - return true; - } - } - - return false; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery(JpaParametersParameterAccessor) - */ - @Override - protected StoredProcedureQuery createQuery(JpaParametersParameterAccessor accessor) { - return applyHints(doCreateQuery(accessor), getQueryMethod()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(JpaParametersParameterAccessor) - */ - @Override - protected StoredProcedureQuery doCreateQuery(JpaParametersParameterAccessor accessor) { - - StoredProcedureQuery storedProcedure = createStoredProcedure(); - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("singleton", storedProcedure); - - return parameterBinder.get().bind(storedProcedure, metadata, accessor); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(JpaParametersParameterAccessor) - */ - @Override - protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor accessor) { - throw new UnsupportedOperationException("StoredProcedureQuery does not support count queries!"); - } - - /** - * Extracts the output value from the given {@link StoredProcedureQuery}. - * - * @param storedProcedureQuery must not be {@literal null}. - *

- * Result is either a single value, or a Map> of output parameter names to nullable - * values - */ - @Nullable - Object extractOutputValue(StoredProcedureQuery storedProcedureQuery) { - - Assert.notNull(storedProcedureQuery, "StoredProcedureQuery must not be null!"); - - if (!procedureAttributes.hasReturnValue()) { - return null; - } - - Map outputValues = new HashMap<>(); - List parameterNames = procedureAttributes.getOutputParameterNames(); - - for (int i = 0; i < parameterNames.size(); i++) { - - String name = parameterNames.get(i); - outputValues.put(name, extractOutputParameter(storedProcedureQuery, i)); - } - - return outputValues.size() == 1 ? outputValues.values().iterator().next() : outputValues; - } - - private Object extractOutputParameter(StoredProcedureQuery storedProcedureQuery, Integer index) { - - String outputParameterName = procedureAttributes.getOutputParameterNames().get(index); - JpaParameters parameters = getQueryMethod().getParameters(); - - return extractOutputParameterValue(storedProcedureQuery, outputParameterName, index, - parameters.getNumberOfParameters()); - } - - /** - * 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 - */ - private Object extractOutputParameterValue(StoredProcedureQuery storedProcedureQuery, String name, Integer index, - int offset) { - - return useNamedParameters && StringUtils.hasText(name) ? // - storedProcedureQuery.getOutputParameterValue(name) - : storedProcedureQuery.getOutputParameterValue(offset + index + 1); - } - - /** - * Creates a new JPA 2.1 {@link StoredProcedureQuery} from this {@link StoredProcedureJpaQuery}. - */ - private StoredProcedureQuery createStoredProcedure() { - - return procedureAttributes.isNamedStoredProcedure() ? newNamedStoredProcedureQuery() - : newAdhocStoredProcedureQuery(); - } - - /** - * Creates a new named {@link StoredProcedureQuery} defined via an {@link NamedStoredProcedureQuery} on an entity. - */ - private StoredProcedureQuery newNamedStoredProcedureQuery() { - return getEntityManager().createNamedStoredProcedureQuery(procedureAttributes.getProcedureName()); - } - - /** - * Creates a new ad-hoc {@link StoredProcedureQuery} from the given {@link StoredProcedureAttributes}. - */ - private StoredProcedureQuery newAdhocStoredProcedureQuery() { - - JpaParameters params = getQueryMethod().getParameters(); - String procedureName = procedureAttributes.getProcedureName(); - - StoredProcedureQuery procedureQuery = getEntityManager().createStoredProcedureQuery(procedureName); - - for (JpaParameter param : params) { - - if (!param.isBindable()) { - continue; - } - - if (useNamedParameters) { - procedureQuery.registerStoredProcedureParameter( - param.getName() - .orElseThrow(() -> new IllegalArgumentException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED)), - param.getType(), ParameterMode.IN); - } else { - procedureQuery.registerStoredProcedureParameter(param.getIndex() + 1, param.getType(), ParameterMode.IN); - } - } - - if (procedureAttributes.hasReturnValue()) { - - ParameterMode mode = ParameterMode.OUT; - - IntStream.range(0, procedureAttributes.getOutputParameterTypes().size()).forEach(i -> { - Class outputParameterType = procedureAttributes.getOutputParameterTypes().get(i); - - if (useNamedParameters) { - - String outputParameterName = procedureAttributes.getOutputParameterNames().get(i); - procedureQuery.registerStoredProcedureParameter(outputParameterName, outputParameterType, mode); + private final StoredProcedureAttributes procedureAttributes; + private final boolean useNamedParameters; + private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); + + /** + * Creates a new {@link StoredProcedureJpaQuery}. + * + * @param method must not be {@literal null} + * @param em must not be {@literal null} + */ + StoredProcedureJpaQuery(JpaQueryMethod method, EntityManager em) { + + super(method, em); + this.procedureAttributes = method.getProcedureAttributes(); + this.useNamedParameters = useNamedParameters(method); + + } + + /** + * Determine whether to used named parameters for the given query method. + * + * @param method must not be {@literal null}. + */ + private static boolean useNamedParameters(QueryMethod method) { + + for (Parameter parameter : method.getParameters()) { + if (parameter.isNamedParameter()) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery(JpaParametersParameterAccessor) + */ + @Override + protected StoredProcedureQuery createQuery(JpaParametersParameterAccessor accessor) { + return applyHints(doCreateQuery(accessor), getQueryMethod()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(JpaParametersParameterAccessor) + */ + @Override + protected StoredProcedureQuery doCreateQuery(JpaParametersParameterAccessor accessor) { + + StoredProcedureQuery storedProcedure = createStoredProcedure(); + QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("singleton", storedProcedure); + + return parameterBinder.get().bind(storedProcedure, metadata, accessor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(JpaParametersParameterAccessor) + */ + @Override + protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor accessor) { + throw new UnsupportedOperationException("StoredProcedureQuery does not support count queries!"); + } + + /** + * Extracts the output value from the given {@link StoredProcedureQuery}. + * + * @param storedProcedureQuery must not be {@literal null}. + *

+ * Result is either a single value, or a Map> of output parameter names to nullable + * values + */ + @Nullable + Object extractOutputParametersValues(StoredProcedureQuery storedProcedureQuery) { + + Assert.notNull(storedProcedureQuery, "StoredProcedureQuery must not be null!"); + + if (!procedureAttributes.hasReturnValue()) { + return null; + } + + List outputParameters = procedureAttributes.getOutputProcedureParameters(); + + if (outputParameters.size() == 1) { + return extractOutputParameterValue(outputParameters.get(0), 0, storedProcedureQuery); + } + + Map outputValues = new HashMap<>(); + + for (int i = 0; i < outputParameters.size(); i++) { + ProcedureParameter outputParameter = outputParameters.get(i); + outputValues.put(outputParameter.getName(), extractOutputParameterValue(outputParameter, i, storedProcedureQuery)); + } + + return outputValues; + } + + /** + * @return The value of an output parameter either by name or by index. + */ + private Object extractOutputParameterValue(ProcedureParameter outputParameter, Integer index, StoredProcedureQuery storedProcedureQuery) { + + JpaParameters methodParameters = getQueryMethod().getParameters(); + + return useNamedParameters && StringUtils.hasText(outputParameter.getName()) ? + storedProcedureQuery.getOutputParameterValue(outputParameter.getName()) + : storedProcedureQuery.getOutputParameterValue(methodParameters.getNumberOfParameters() + index + 1); + } + + /** + * Creates a new JPA 2.1 {@link StoredProcedureQuery} from this {@link StoredProcedureJpaQuery}. + */ + private StoredProcedureQuery createStoredProcedure() { + + return procedureAttributes.isNamedStoredProcedure() ? newNamedStoredProcedureQuery() + : newAdHocSingleResultProcedureQuery(); + } + + /** + * Creates a new named {@link StoredProcedureQuery} defined via an {@link NamedStoredProcedureQuery} on an entity. + */ + private StoredProcedureQuery newNamedStoredProcedureQuery() { + return getEntityManager().createNamedStoredProcedureQuery(procedureAttributes.getProcedureName()); + } + + /** + * Creates a new ad-hoc {@link StoredProcedureQuery} from the given {@link StoredProcedureAttributes}. + */ + private StoredProcedureQuery newAdHocSingleResultProcedureQuery() { + + JpaParameters params = getQueryMethod().getParameters(); + StoredProcedureQuery procedureQuery = createAdhocSingleResultProcedureQuery(); + + for (JpaParameter param : params) { + + if (!param.isBindable()) { + continue; + } + + if (useNamedParameters) { + procedureQuery.registerStoredProcedureParameter( + param.getName() + .orElseThrow(() -> new IllegalArgumentException(ParameterBinder.PARAMETER_NEEDS_TO_BE_NAMED)), + param.getType(), ParameterMode.IN); + } else { + procedureQuery.registerStoredProcedureParameter(param.getIndex() + 1, param.getType(), ParameterMode.IN); + } + } + + if (procedureAttributes.hasReturnValue()) { + + ProcedureParameter procedureOutput = procedureAttributes.getOutputProcedureParameters().get(0); + + if (useNamedParameters) { + procedureQuery.registerStoredProcedureParameter(procedureOutput.getName(), procedureOutput.getType(), procedureOutput.getMode()); + } else { + // Output parameter should be after the input parameters + int outputParameterIndex = params.getNumberOfParameters() + 1; + procedureQuery.registerStoredProcedureParameter(outputParameterIndex, procedureOutput.getType(), + procedureOutput.getMode()); + } + } + + return procedureQuery; + } + + private StoredProcedureQuery createAdhocSingleResultProcedureQuery() { + String procedureName = procedureAttributes.getProcedureName(); + + if (getQueryMethod().isQueryForEntity()) { - } else { - procedureQuery.registerStoredProcedureParameter(params.getNumberOfParameters() + i + 1, outputParameterType, - mode); - } - }); - } + return getEntityManager().createStoredProcedureQuery(procedureName, + getQueryMethod().getEntityInformation().getJavaType()); + } - return procedureQuery; - } + return getEntityManager().createStoredProcedureQuery(procedureName); + } } 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..501f76b68a 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 @@ -33,10 +33,7 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.query.AbstractJpaQuery; -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.*; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -92,7 +89,7 @@ public JpaRepositoryFactory(EntityManager entityManager) { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); addRepositoryProxyPostProcessor((factory, repositoryInformation) -> { - if (hasMethodReturningStream(repositoryInformation.getRepositoryInterface())) { + if (isTransactionNeeded(repositoryInformation.getRepositoryInterface())) { factory.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); } }); @@ -241,12 +238,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; } } diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/Dummy.java b/src/test/java/org/springframework/data/jpa/domain/sample/Dummy.java index 5b82c15537..b672fc8d24 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/Dummy.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/Dummy.java @@ -46,12 +46,12 @@ type = Integer.class) }) // , @NamedStoredProcedureQuery(name = "Dummy.procedureWith1InputAnd1OutputParameterWithResultSet", - procedureName = "procedure_in1_out0_return_rs_no_update", parameters = { + procedureName = "procedure_in1_out0_return_ref_cursor_no_update", parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, type = String.class), @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) }) // , @NamedStoredProcedureQuery(name = "Dummy.procedureWith1InputAnd1OutputParameterWithResultSetWithUpdate", - procedureName = "procedure_in1_out0_return_rs_with_update", parameters = { + procedureName = "procedure_in1_out0_return_ref_cursor_with_update", parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, type = String.class), @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) }) // , 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..7f1d48a2ee 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; @@ -80,9 +81,9 @@ public void shouldExecuteAdHocProcedureWith1InputAndNoOutputParameter() { @Test // DATAJPA-652 @Ignore(NOT_SUPPORTED) - public void shouldExecuteAdHocProcedureWith1InputAnd1OutputParameterWithResultSet() { + public void shouldExecuteAdHocProcedureWith1InputAnd1OutputParameterWithRefCursor() { - List dummies = repository.adHocProcedureWith1InputAnd1OutputParameterWithResultSet("FOO"); + List dummies = repository.adHocProcedureWith1InputAnd1OutputParameterWithRefCursor("FOO"); assertThat(dummies).isNotNull(); assertThat(dummies.size()).isEqualTo(3); @@ -90,9 +91,9 @@ public void shouldExecuteAdHocProcedureWith1InputAnd1OutputParameterWithResultSe @Test // DATAJPA-652 @Ignore(NOT_SUPPORTED) - public void shouldExecuteAdHocProcedureWith1InputAnd1OutputParameterWithResultSetWithUpdate() { + public void shouldExecuteAdHocProcedureWith1InputAnd1OutputParameterWithRefCursorWithUpdate() { - List dummies = repository.adHocProcedureWith1InputAnd1OutputParameterWithResultSetWithUpdate("FOO"); + List dummies = repository.adHocProcedureWith1InputAnd1OutputParameterWithRefCursorWithUpdate("FOO"); assertThat(dummies).isNotNull(); assertThat(dummies.size()).isEqualTo(3); 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..231254dcaa 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -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,235 +46,382 @@ * @author Diego Diez * @author Jeff Sheets * @author Jens Schauder + * @author Gabriel Basilio * @since 1.6 */ @RunWith(MockitoJUnitRunner.class) public class StoredProcedureAttributeSourceUnitTests { - StoredProcedureAttributeSource creator; - @Mock - JpaEntityMetadata entityMetadata; + StoredProcedureAttributeSource creator; + @Mock + JpaEntityMetadata entityMetadata; + + @Before + public void setup() { + + creator = StoredProcedureAttributeSource.INSTANCE; - @Before - public void setup() { + when(entityMetadata.getJavaType()).thenReturn(User.class); + when(entityMetadata.getEntityName()).thenReturn("User"); + } - creator = StoredProcedureAttributeSource.INSTANCE; + @Test // DATAJPA-455 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithImplicitProcedureName() { - when(entityMetadata.getJavaType()).thenReturn(User.class); - when(entityMetadata.getEntityName()).thenReturn("User"); - } + StoredProcedureAttributes attr = creator.createFrom(method("plus1inout", Integer.class), entityMetadata); + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithImplicitProcedureName() { + @Test // DATAJPA-455 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictName() { - StoredProcedureAttributes attr = creator.createFrom(method("plus1inout", Integer.class), entityMetadata); + StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), + entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictName() { + @Test // DATAJPA-455 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameValue() { - StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), - entityMetadata); + StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), + entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameValue() { + @Test // DATAJPA-455 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameAlias() { - StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), - entityMetadata); + StoredProcedureAttributes attr = creator + .createFrom(method("explicitPlus1inoutViaProcedureNameAlias", Integer.class), entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameAlias() { + @Test // DATAJPA-1297 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameAliasAndOutputParameterName() { - StoredProcedureAttributes attr = creator - .createFrom(method("explicitPlus1inoutViaProcedureNameAlias", Integer.class), entityMetadata); + StoredProcedureAttributes attr = creator.createFrom( + method("explicitPlus1inoutViaProcedureNameAliasAndOutputParameterName", Integer.class), entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); + } - @Test // DATAJPA-1297 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameAliasAndOutputParameterName() { + @Test // DATAJPA-455 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedure() { - StoredProcedureAttributes attr = creator.createFrom( - method("explicitPlus1inoutViaProcedureNameAliasAndOutputParameterName", Integer.class), entityMetadata); + StoredProcedureAttributes attr = creator + .createFrom(method("entityAnnotatedCustomNamedProcedurePlus1IO", Integer.class), entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); + } - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedure() { + @Test // DATAJPA-707 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedureAndOutputParamName() { - StoredProcedureAttributes attr = creator - .createFrom(method("entityAnnotatedCustomNamedProcedurePlus1IO", Integer.class), entityMetadata); + StoredProcedureAttributes attr = creator + .createFrom(method("entityAnnotatedCustomNamedProcedureOutputParamNamePlus1IO", Integer.class), entityMetadata); - assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); - } - - @Test // DATAJPA-707 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedureAndOutputParamName() { - - StoredProcedureAttributes attr = creator - .createFrom(method("entityAnnotatedCustomNamedProcedureOutputParamNamePlus1IO", Integer.class), entityMetadata); - - assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("override"); - } - - @Test // DATAJPA-707 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedureAnd2OutParams() { - - StoredProcedureAttributes attr = creator - .createFrom(method("entityAnnotatedCustomNamedProcedurePlus1IO2", Integer.class), entityMetadata); - - 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"); - } - - @Test // DATAJPA-455 - public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithImplicitlyNamedProcedure() { - - StoredProcedureAttributes attr = creator.createFrom(method("plus1", Integer.class), entityMetadata); - - assertThat(attr.getProcedureName()).isEqualTo("User.plus1"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); - } - - @Test // DATAJPA-871 - public void aliasedStoredProcedure() { - - StoredProcedureAttributes attr = creator - .createFrom(method("plus1inoutWithComposedAnnotationOverridingProcedureName", Integer.class), entityMetadata); - - assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); - } - - @Test // DATAJPA-871 - public void aliasedStoredProcedure2() { - - StoredProcedureAttributes attr = creator - .createFrom(method("plus1inoutWithComposedAnnotationOverridingName", Integer.class), entityMetadata); - - assertThat(attr.getProcedureName()).isEqualTo("User.plus1"); - assertThat(attr.getOutputParameterTypes().get(0)).isEqualTo(Integer.class); - assertThat(attr.getOutputParameterNames().get(0)).isEqualTo("res"); - } - - private static Method method(String name, Class... paramTypes) { - return ReflectionUtils.findMethod(DummyRepository.class, name, paramTypes); - } - - /** - * @author Thomas Darimont - */ - @SuppressWarnings("unused") - interface DummyRepository { - - /** - * Explicitly mapped to a procedure with name "plus1inout" in database. - */ - @Procedure("plus1inout") - // DATAJPA-455 - Integer explicitlyNamedPlus1inout(Integer arg); - - /** - * Explicitly mapped to a procedure with name "plus1inout" in database via alias. - */ - @Procedure(procedureName = "plus1inout") - // DATAJPA-455 - Integer explicitPlus1inoutViaProcedureNameAlias(Integer arg); - - /** - * Explicitly mapped to a procedure with name "plus1inout" in database via alias and explicitly named ouput - * parameter. - */ - @Procedure(procedureName = "plus1inout", outputParameterName = "res") - // DATAJPA-1297 - Integer explicitPlus1inoutViaProcedureNameAliasAndOutputParameterName(Integer arg); - - /** - * Implicitly mapped to a procedure with name "plus1inout" in database via alias. - */ - @Procedure - // DATAJPA-455 - Integer plus1inout(Integer arg); - - /** - * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. - */ - @Procedure(name = "User.plus1IO") - // DATAJPA-455 - Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); - - /** - * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. - * With a outputParameterName - */ - @Procedure(name = "User.plus1IO", outputParameterName = "override") - // DATAJPA-707 - Integer entityAnnotatedCustomNamedProcedureOutputParamNamePlus1IO(@Param("arg") Integer arg); - - /** - * Explicitly mapped to named stored procedure "User.plus1IO2" in {@link EntityManager}. - */ - @Procedure(name = "User.plus1IO2") - // DATAJPA-707 - Map entityAnnotatedCustomNamedProcedurePlus1IO2(@Param("arg") Integer arg); - - /** - * Implicitly mapped to named stored procedure "User.plus1" in {@link EntityManager}. - */ - @Procedure - // DATAJPA-455 - Integer plus1(@Param("arg") Integer arg); - - @ComposedProcedureUsingAliasFor(explicitProcedureName = "plus1inout") - Integer plus1inoutWithComposedAnnotationOverridingProcedureName(Integer arg); - - @ComposedProcedureUsingAliasFor(emProcedureName = "User.plus1") - Integer plus1inoutWithComposedAnnotationOverridingName(Integer arg); - } - - @SuppressWarnings("unused") - @Procedure - @Retention(RetentionPolicy.RUNTIME) - @interface ComposedProcedureUsingAliasFor { - - @AliasFor(annotation = Procedure.class, attribute = "value") - String dbProcedureName() default ""; - - @AliasFor(annotation = Procedure.class, attribute = "procedureName") - String explicitProcedureName() default ""; - - @AliasFor(annotation = Procedure.class, attribute = "name") - String emProcedureName() default ""; - - @AliasFor(annotation = Procedure.class, attribute = "outputParameterName") - String outParamName() default ""; - } + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("User.plus1IO"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("override"); + } + + @Test // DATAJPA-707 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedureAnd2OutParams() { + + 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(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 + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithImplicitlyNamedProcedure() { + + StoredProcedureAttributes attr = creator.createFrom(method("plus1", Integer.class), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("User.plus1"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); + } + + @Test // DATAJPA-871 + public void aliasedStoredProcedure() { + + StoredProcedureAttributes attr = creator + .createFrom(method("plus1inoutWithComposedAnnotationOverridingProcedureName", Integer.class), entityMetadata); + + ProcedureParameter outputParameter = attr.getOutputProcedureParameters().get(0); + assertThat(attr.getProcedureName()).isEqualTo("plus1inout"); + assertThat(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo(StoredProcedureAttributes.SYNTHETIC_OUTPUT_PARAMETER_NAME); + } + + @Test // DATAJPA-871 + 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(outputParameter.getMode()).isEqualTo(ParameterMode.OUT); + assertThat(outputParameter.getType()).isEqualTo(Integer.class); + assertThat(outputParameter.getName()).isEqualTo("res"); + } + + @Test // DATAJPA-652 + public void testSingleEntityFromResultSetAndNoInput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("singleEntityFromResultSetAndNoInput"), 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-652 + 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-652 + public void testEntityListFrom1RowResultSetWithNoInput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFrom1RowResultSetWithNoInput"), 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-652 + public void testEntityListFrom1RowResultSetWithInput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFrom1RowResultSetWithInput", 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-652 + public void testGenericObjectListFrom1RowResultSetWithInput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("genericObjectListFrom1RowResultSetWithInput", 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-652 + public void testEntityListFrom1RowResultSetWithInputAndNamedOutput() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFrom1RowResultSetWithInputAndNamedOutput", 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-652 + public void testEntityListFrom1RowResultSetWithInputAndNamedOutputAndCursor() { + + StoredProcedureAttributes attr = creator + .createFrom(method("entityListFrom1RowResultSetWithInputAndNamedOutputAndCursor", 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) { + return ReflectionUtils.findMethod(DummyRepository.class, name, paramTypes); + } + + /** + * @author Thomas Darimont + */ + @SuppressWarnings("unused") + interface DummyRepository { + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database. + */ + @Procedure("plus1inout") + // DATAJPA-455 + Integer explicitlyNamedPlus1inout(Integer arg); + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database via alias. + */ + @Procedure(procedureName = "plus1inout") + // DATAJPA-455 + Integer explicitPlus1inoutViaProcedureNameAlias(Integer arg); + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database via alias and explicitly named ouput + * parameter. + */ + @Procedure(procedureName = "plus1inout", outputParameterName = "res") + // DATAJPA-1297 + Integer explicitPlus1inoutViaProcedureNameAliasAndOutputParameterName(Integer arg); + + /** + * Implicitly mapped to a procedure with name "plus1inout" in database via alias. + */ + @Procedure + // DATAJPA-455 + Integer plus1inout(Integer arg); + + /** + * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. + */ + @Procedure(name = "User.plus1IO") + // DATAJPA-455 + Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); + + /** + * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. + * With a outputParameterName + */ + @Procedure(name = "User.plus1IO", outputParameterName = "override") + // DATAJPA-707 + Integer entityAnnotatedCustomNamedProcedureOutputParamNamePlus1IO(@Param("arg") Integer arg); + + /** + * Explicitly mapped to named stored procedure "User.plus1IO2" in {@link EntityManager}. + */ + @Procedure(name = "User.plus1IO2") + // DATAJPA-707 + Map entityAnnotatedCustomNamedProcedurePlus1IO2(@Param("arg") Integer arg); + + /** + * Implicitly mapped to named stored procedure "User.plus1" in {@link EntityManager}. + */ + @Procedure + // DATAJPA-455 + Integer plus1(@Param("arg") Integer arg); + + @ComposedProcedureUsingAliasFor(explicitProcedureName = "plus1inout") + Integer plus1inoutWithComposedAnnotationOverridingProcedureName(Integer arg); + + @ComposedProcedureUsingAliasFor(emProcedureName = "User.plus1") + Integer plus1inoutWithComposedAnnotationOverridingName(Integer arg); + + @Procedure("0_input_1_row_resultset") + // DATAJPA-652 + Dummy singleEntityFromResultSetAndNoInput(); + + @Procedure("1_input_1_row_resultset") + // DATAJPA-652 + Dummy singleEntityFrom1RowResultSetWithInput(Integer arg); + + @Procedure("0_input_1_resultset") + // DATAJPA-652 + List entityListFrom1RowResultSetWithNoInput(); + + @Procedure("1_input_1_resultset") + // DATAJPA-652 + List entityListFrom1RowResultSetWithInput(Integer arg); + + @Procedure(value = "1_input_1_resultset", outputParameterName = "dummies") + // DATAJPA-652 + List entityListFrom1RowResultSetWithInputAndNamedOutput(Integer arg); + + @Procedure(value = "1_input_1_resultset", outputParameterName = "dummies", refCursor = true) + // DATAJPA-652 + List entityListFrom1RowResultSetWithInputAndNamedOutputAndCursor(Integer arg); + } + + @SuppressWarnings("unused") + @Procedure + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedProcedureUsingAliasFor { + + @AliasFor(annotation = Procedure.class, attribute = "value") + String dbProcedureName() default ""; + + @AliasFor(annotation = Procedure.class, attribute = "procedureName") + String explicitProcedureName() default ""; + + @AliasFor(annotation = Procedure.class, attribute = "name") + String emProcedureName() default ""; + + @AliasFor(annotation = Procedure.class, attribute = "outputParameterName") + String outParamName() default ""; + + @AliasFor(annotation = Procedure.class, attribute = "refCursor") + boolean refCursor() default false; + } } 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); } } diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/DummyRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/DummyRepository.java index 47bb91c71c..104308b8c5 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/DummyRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/DummyRepository.java @@ -34,11 +34,11 @@ public interface DummyRepository extends CrudRepository { @Procedure("procedure_in0_out1") Integer adHocProcedureWithNoInputAnd1OutputParameter(); - @Procedure("procedure_in1_out0_return_rs_no_update") - List adHocProcedureWith1InputAnd1OutputParameterWithResultSet(String in); + @Procedure("procedure_in1_out0_return_ref_cursor_no_update") + List adHocProcedureWith1InputAnd1OutputParameterWithRefCursor(String in); - @Procedure("procedure_in1_out0_return_rs_with_update") - List adHocProcedureWith1InputAnd1OutputParameterWithResultSetWithUpdate(String in); + @Procedure("procedure_in1_out0_return_ref_cursor_with_update") + List adHocProcedureWith1InputAnd1OutputParameterWithRefCursorWithUpdate(String in); @Procedure("procedure_in1_out0_no_return_with_update") void adHocProcedureWith1InputAndNoOutputParameterWithUpdate(String in); diff --git a/src/test/resources/scripts/schema-stored-procedures.sql b/src/test/resources/scripts/schema-stored-procedures.sql index 859133de01..fa0cc2a803 100644 --- a/src/test/resources/scripts/schema-stored-procedures.sql +++ b/src/test/resources/scripts/schema-stored-procedures.sql @@ -27,9 +27,9 @@ DROP procedure IF EXISTS procedure_in1_out0 /; DROP procedure IF EXISTS procedure_in0_out1 /; -DROP procedure IF EXISTS procedure_in1_out0_return_rs_no_update +DROP procedure IF EXISTS procedure_in1_out0_return_ref_cursor_no_update /; -DROP procedure IF EXISTS procedure_in1_out0_return_rs_with_update +DROP procedure IF EXISTS procedure_in1_out0_return_ref_cursor_with_update /; DROP procedure IF EXISTS procedure_in1_out0_no_return_with_update /; @@ -65,7 +65,7 @@ set res = 42; END /; -CREATE procedure procedure_in1_out0_return_rs_no_update (IN arg varchar(32)) +CREATE procedure procedure_in1_out0_return_ref_cursor_no_update (IN arg varchar(32)) READS SQL DATA DYNAMIC RESULT SETS 1 BEGIN ATOMIC DECLARE result CURSOR WITH RETURN FOR SELECT * FROM public.dummy FOR READ ONLY; @@ -73,7 +73,7 @@ open result; END /; -CREATE procedure procedure_in1_out0_return_rs_with_update (IN arg varchar(32)) +CREATE procedure procedure_in1_out0_return_ref_cursor_with_update (IN arg varchar(32)) MODIFIES SQL DATA DYNAMIC RESULT SETS 1 BEGIN ATOMIC DECLARE result CURSOR WITH RETURN FOR SELECT * FROM public.dummy FOR READ ONLY;