From 09535813c2e23f3a13c0c270e1d571b574a831f6 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 7 Apr 2014 22:29:42 +0200 Subject: [PATCH] DATAJPA-455 - Add support for stored procedure backed repository methods. Added support for JPA 2.1 stored procedures mapping for repository methods. Introduced @Procedure annotation for declaring stored procedure metadata. Repository methods backed by stored procedures are represented as a StoredProcedureJpaQuery that is constructed by JpaQueryFactory#fromProcedureAnnotation. Enhanced JpaQueryLookupStrategy to support @Procedure. Introduced StoredProcedureAttribute to capture the derived configuration for a stored procedure query. The stored procedure needed for the tests is created via the schema-stored-procedures.sql script that is picked up by the customized DataSource definition in infrastructure.xml. Added new test class UserRepositoryStoredProcedureTests to be able to exclude those tests for OpenJPA. OpenJPA tests don't work with hsqldb 2.x and use hsqldb 1.x instead which doesn't support stored procedures. Original pull request: #80. --- .../repository/query/AbstractJpaQuery.java | 7 +- .../repository/query/JpaQueryExecution.java | 26 +++ .../jpa/repository/query/JpaQueryFactory.java | 16 ++ .../query/JpaQueryLookupStrategy.java | 6 + .../jpa/repository/query/JpaQueryMethod.java | 27 +++ .../data/jpa/repository/query/Procedure.java | 53 +++++ .../jpa/repository/query/SimpleJpaQuery.java | 4 + .../query/StoredProcedureAttributeSource.java | 197 ++++++++++++++++++ .../query/StoredProcedureAttributes.java | 100 +++++++++ .../query/StoredProcedureJpaQuery.java | 153 ++++++++++++++ .../data/jpa/domain/sample/User.java | 12 ++ .../UserRepositoryStoredProcedureTests.java | 121 +++++++++++ ...oredProcedureAttributeSourceUnitTests.java | 190 +++++++++++++++++ .../jpa/repository/sample/UserRepository.java | 34 +++ .../jpa/support/EntityManagerTestUtils.java | 35 ++++ src/test/resources/infrastructure.xml | 4 +- src/test/resources/openjpa.xml | 18 +- .../scripts/schema-stored-procedures.sql | 8 + 18 files changed, 1003 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/Procedure.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/UserRepositoryStoredProcedureTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java create mode 100644 src/test/java/org/springframework/data/jpa/support/EntityManagerTestUtils.java create mode 100644 src/test/resources/scripts/schema-stored-procedures.sql diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 77872ab3f..2cb0c30fa 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -25,6 +25,7 @@ import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.PagedExecution; +import org.springframework.data.jpa.repository.query.JpaQueryExecution.ProcedureExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.SlicedExecution; import org.springframework.data.repository.query.RepositoryQuery; @@ -98,7 +99,9 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { protected JpaQueryExecution getExecution() { - if (method.isCollectionQuery()) { + if (method.isProcedureQuery()) { + return new ProcedureExecution(); + } else if (method.isCollectionQuery()) { return new CollectionExecution(); } else if (method.isSliceQuery()) { return new SlicedExecution(method.getParameters()); @@ -117,7 +120,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { * @param query * @return */ - private T applyHints(T query, JpaQueryMethod method) { + protected T applyHints(T query, JpaQueryMethod method) { for (QueryHint hint : method.getHints()) { applyQueryHint(query, hint); 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 cc625b1e9..faba30c61 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 @@ -21,6 +21,7 @@ import java.util.List; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.Query; +import javax.persistence.StoredProcedureQuery; import javax.persistence.TypedQuery; import org.springframework.data.domain.PageImpl; @@ -238,4 +239,29 @@ public abstract class JpaQueryExecution { return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); } } + + /** + * {@link Execution} executing a stored procedure. + * + * @author Thomas Darimont + * @since 1.6 + */ + static class ProcedureExecution extends JpaQueryExecution { + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[]) + */ + @Override + protected Object doExecute(AbstractJpaQuery jpaQuery, Object[] values) { + + Assert.isInstanceOf(StoredProcedureJpaQuery.class, jpaQuery); + + StoredProcedureJpaQuery storedProcedureJpaQuery = (StoredProcedureJpaQuery) jpaQuery; + StoredProcedureQuery storedProcedure = storedProcedureJpaQuery.createQuery(values); + storedProcedure.execute(); + + return storedProcedureJpaQuery.extractOutputValue(storedProcedure); + } + } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index 7f0f81ee2..c42931a47 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -65,4 +65,20 @@ enum JpaQueryFactory { return method.isNativeQuery() ? new NativeJpaQuery(method, em, queryString) : // new SimpleJpaQuery(method, em, queryString); } + + /** + * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @return + */ + public StoredProcedureJpaQuery fromProcedureAnnotation(JpaQueryMethod method, EntityManager em) { + + if (!method.isProcedureQuery()) { + return null; + } + + return new StoredProcedureJpaQuery(method, em); + } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 546586b0b..071f61b4d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -118,6 +118,12 @@ public final class JpaQueryLookupStrategy { return query; } + query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em); + + if (null != query) { + return query; + } + String name = method.getNamedQueryName(); if (namedQueries.hasQuery(name)) { return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name)); 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 158a0e10c..24b489352 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 @@ -53,6 +53,7 @@ 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 { @@ -68,6 +69,8 @@ public class JpaQueryMethod extends QueryMethod { private final QueryExtractor extractor; private final Method method; + private StoredProcedureAttributes storedProcedureAttributes; + /** * Creates a {@link JpaQueryMethod}. * @@ -314,4 +317,28 @@ public class JpaQueryMethod extends QueryMethod { public boolean isCollectionQuery() { return super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(method.getReturnType()); } + + /** + * Return {@literal true} if the method contains a {@link Procedure} annotation. + * + * @return + */ + public boolean isProcedureQuery() { + return method.getAnnotation(Procedure.class) != null; + } + + /** + * 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 new file mode 100644 index 000000000..3cd8efe36 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/Procedure.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014 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 + * + * http://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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to declare JPA 2.1 stored procedure mappings directly on repository methods. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Procedure { + + /** + * The name of the procedure in the database, defaults to {@code ""}. Short form for {@link #procedureName()}. + */ + String value() default ""; + + /** + * The name of the procedure in the database, defaults to {@code ""}. + */ + String procedureName() default ""; + + /** + * The name of the procedure in the EntityManager - defaults to {@code ""}. + */ + String name() default ""; + + /** + * The name of the outputParameter, defaults to {@code ""}. + */ + String outputParameterName() default ""; +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index 6a00e2890..c3e62aba8 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -67,6 +67,10 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { */ private final void validateQuery(String query, String errorMessage) { + if (getQueryMethod().isProcedureQuery()) { + return; + } + EntityManager validatingEm = null; try { 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 new file mode 100644 index 000000000..a678b6676 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java @@ -0,0 +1,197 @@ +/* + * Copyright 2014 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 + * + * http://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 java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.persistence.NamedStoredProcedureQueries; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.StoredProcedureParameter; + +import org.springframework.data.jpa.repository.support.JpaEntityMetadata; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A factory class for {@link StoredProcedureAttributes}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @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 = method.getAnnotation(Procedure.class); + Assert.notNull(procedure, "Method must have an @Procedure annotation!"); + + NamedStoredProcedureQuery namedStoredProc = tryFindAnnotatedNamedStoredProcedureQuery(method, entityMetadata, + procedure); + + if (namedStoredProc != null) { + return newProcedureAttributesFrom(method, namedStoredProc); + } + + 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, null, method.getReturnType(), false); + } + + /** + * 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 + * @return + */ + private StoredProcedureAttributes newProcedureAttributesFrom(Method method, NamedStoredProcedureQuery namedStoredProc) { + + String outputParameterName = null; + Class outputParameterType = null; + + int outputParameterCount = 0; + + for (StoredProcedureParameter param : namedStoredProc.parameters()) { + switch (param.mode()) { + case OUT: + case INOUT: + + if (outputParameterCount > 0) { + throw new IllegalStateException( + String.format( + "Could not create ProcedureAttributes from %s. We currently support only one output parameter!", + method)); + } + + outputParameterName = param.name(); + outputParameterType = param.type(); + + outputParameterCount++; + break; + case IN: + default: + continue; + } + } + + if (outputParameterType == null) { + outputParameterType = method.getReturnType(); + } + + return new StoredProcedureAttributes(namedStoredProc.name(), outputParameterName, outputParameterType, true); + } + + /** + * @param method must not be {@literal null}. + * @param entityMetadata must not be {@literal null}. + * @param procedure must not be {@literal null}. + * @return + */ + 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 = entityType.getAnnotation(NamedStoredProcedureQueries.class); + if (namedQueriesAnnotation != null) { + queries.addAll(Arrays.asList(namedQueriesAnnotation.value())); + } + + NamedStoredProcedureQuery namedQueryAnnotation = entityType.getAnnotation(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 new file mode 100644 index 000000000..0da8dbf57 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java @@ -0,0 +1,100 @@ +/* + * Copyright 2014 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 + * + * http://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 javax.persistence.StoredProcedureQuery; + +import org.springframework.util.Assert; + +/** + * Stored procedure configuration for JPA 2.1 {@link StoredProcedureQuery}s. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ +class StoredProcedureAttributes { + + private final boolean namedStoredProcedure; + private final String procedureName; + private final String outputParameterName; + private final Class outputParameterType; + + /** + * Creates a new {@link StoredProcedureAttributes}. + * + * @param procedureName must not be {@literal null} + * @param outputParameterName may be {@literal null} + * @param outputParameterIndex must not be {@literal null} + * @param outputParameterType + */ + public StoredProcedureAttributes(String procedureName, String outputParameterName, Class outputParameterType, + boolean namedStoredProcedure) { + + Assert.notNull(procedureName, "ProcedureName must not be null!"); + Assert.notNull(outputParameterType, "OutputParameterType must not be null!"); + + this.procedureName = procedureName; + this.outputParameterName = outputParameterName; + this.outputParameterType = outputParameterType; + this.namedStoredProcedure = namedStoredProcedure; + } + + /** + * Returns the name of the stored procedure. + * + * @return + */ + public String getProcedureName() { + return procedureName; + } + + /** + * Returns the name of the output parameter. + * + * @return + */ + public String getOutputParameterName() { + return outputParameterName; + } + + /** + * Returns the type of the output parameter. + * + * @return + */ + public Class getOutputParameterType() { + return outputParameterType; + } + + /** + * 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 !(void.class.equals(outputParameterType) || Void.class.equals(outputParameterType)); + } +} 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 new file mode 100644 index 000000000..a3402424c --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -0,0 +1,153 @@ +/* + * Copyright 2014 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 + * + * http://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 javax.persistence.EntityManager; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureQuery; +import javax.persistence.TypedQuery; + +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link AbstractJpaQuery} implementation that inspects a {@link JpaQueryMethod} for the existence of an + * {@link Procedure} annotation and creates a JPA 2.1 {@link StoredProcedureQuery} from it. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ +class StoredProcedureJpaQuery extends AbstractJpaQuery { + + private final StoredProcedureAttributes procedureAttributes; + + /** + * Creates a new {@link StoredProcedureJpaQuery}. + * + * @param method must not be {@literal null} + * @param em must not be {@literal null} + */ + public StoredProcedureJpaQuery(JpaQueryMethod method, EntityManager em) { + + super(method, em); + this.procedureAttributes = method.getProcedureAttributes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#createQuery(java.lang.Object[]) + */ + @Override + protected StoredProcedureQuery createQuery(Object[] values) { + return applyHints(doCreateQuery(values), getQueryMethod()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(java.lang.Object[]) + */ + @Override + protected StoredProcedureQuery doCreateQuery(Object[] values) { + + StoredProcedureQuery proc = createStoredProcedure(); + + return createBinder(values).bind(proc); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(java.lang.Object[]) + */ + @Override + protected TypedQuery doCreateCountQuery(Object[] values) { + return null; + } + + /** + * Extracts the output value from the given {@link StoredProcedureQuery}. + * + * @param storedProcedureQuery must not be {@literal null}. + * @return + */ + Object extractOutputValue(StoredProcedureQuery storedProcedureQuery) { + + Assert.notNull(storedProcedureQuery, "StoredProcedureQuery must not be null!"); + + if (!procedureAttributes.hasReturnValue()) { + return null; + } + + if (StringUtils.hasText(procedureAttributes.getOutputParameterName())) { + return storedProcedureQuery.getOutputParameterValue(procedureAttributes.getOutputParameterName()); + } + + return storedProcedureQuery.getOutputParameterValue(getQueryMethod().getParameters().getNumberOfParameters() + 1); + } + + /** + * Creates a new JPA 2.1 {@link StoredProcedureQuery} from this {@link StoredProcedureJpaQuery}. + * + * @return + */ + private StoredProcedureQuery createStoredProcedure() { + return procedureAttributes.isNamedStoredProcedure() ? newNamedStoredProcedureQuery() + : newAdhocStoredProcedureQuery(); + } + + /** + * Creates a new named {@link StoredProcedureQuery} defined via an {@link NamedStoredProcedureQuery} on an entity. + * + * @return + */ + private StoredProcedureQuery newNamedStoredProcedureQuery() { + return getEntityManager().createNamedStoredProcedureQuery(procedureAttributes.getProcedureName()); + } + + /** + * Creates a new ad-hoc {@link StoredProcedureQuery} from the given {@link StoredProcedureAttributes}. + * + * @return + */ + private StoredProcedureQuery newAdhocStoredProcedureQuery() { + + StoredProcedureQuery procedureQuery = getEntityManager().createStoredProcedureQuery( + procedureAttributes.getProcedureName()); + + JpaParameters params = getQueryMethod().getParameters(); + for (JpaParameter param : params) { + + if (!param.isBindable()) { + continue; + } + + if (param.isNamedParameter()) { + procedureQuery.registerStoredProcedureParameter(param.getName(), param.getType(), ParameterMode.IN); + } else { + procedureQuery.registerStoredProcedureParameter(param.getIndex() + 1, param.getType(), ParameterMode.IN); + } + } + + if (procedureAttributes.hasReturnValue()) { + procedureQuery.registerStoredProcedureParameter(params.getNumberOfParameters() + 1, + procedureAttributes.getOutputParameterType(), ParameterMode.OUT); + } + + return procedureQuery; + } +} diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/src/test/java/org/springframework/data/jpa/domain/sample/User.java index f0e74b056..a2b60d16a 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -34,6 +34,10 @@ import javax.persistence.NamedAttributeNode; import javax.persistence.NamedEntityGraph; import javax.persistence.NamedEntityGraphs; import javax.persistence.NamedQuery; +import javax.persistence.NamedStoredProcedureQueries; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; import javax.persistence.Temporal; import javax.persistence.TemporalType; @@ -50,6 +54,14 @@ import javax.persistence.TemporalType; @NamedEntityGraph(name = "User.detail", attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode("manager"), @NamedAttributeNode("colleagues") }) }) @NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1") +@NamedStoredProcedureQueries({ // +@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) }) // +}) +@NamedStoredProcedureQuery(name = "User.plus1IO", procedureName = "plus1inout", parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) }) public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryStoredProcedureTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryStoredProcedureTests.java new file mode 100644 index 000000000..6415df777 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryStoredProcedureTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2014 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 + * + * http://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; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.springframework.data.jpa.support.EntityManagerTestUtils.*; + +import javax.persistence.EntityManager; +import javax.persistence.ParameterMode; +import javax.persistence.PersistenceContext; +import javax.persistence.StoredProcedureQuery; + +import org.junit.Assume; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for JPA 2.1 stored procedure support. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +public class UserRepositoryStoredProcedureTests { + + @Autowired UserRepository repository; + @PersistenceContext EntityManager em; + + /** + * @see DATAJPA-455 + */ + @Test + public void callProcedureWithInAndOutParameters() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + assertThat(repository.plus1inout(1), is(2)); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void callProcedureExplicitNameWithInAndOutParameters() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + assertThat(repository.explicitlyNamedPlus1inout(1), is(2)); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void entityAnnotatedCustomNamedProcedurePlus1IO() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + assertThat(repository.entityAnnotatedCustomNamedProcedurePlus1IO(1), is(2)); + } + + /** + * @see DATAJPA-455 + */ + @Test + @Ignore + public void plainJpa21() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + StoredProcedureQuery proc = em.createStoredProcedureQuery("plus1inout"); + proc.registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN); + proc.registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT); + + proc.setParameter(1, 1); + proc.execute(); + + assertThat(proc.getOutputParameterValue(2), is((Object) 2)); + } + + /** + * @see DATAJPA-455 + */ + @Test + @Ignore + public void plainJpa21_entityAnnotatedCustomNamedProcedurePlus1IO() { + + Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + StoredProcedureQuery proc = em.createNamedStoredProcedureQuery("User.plus1IO"); + + proc.setParameter("arg", 1); + proc.execute(); + + assertThat(proc.getOutputParameterValue("res"), is((Object) 2)); + } +} 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 new file mode 100644 index 000000000..b4cced5ac --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2014 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 + * + * http://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 static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.object.IsCompatibleType.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import javax.persistence.EntityManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.support.JpaEntityMetadata; +import org.springframework.data.repository.query.Param; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for {@link StoredProcedureAttributeSource}. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @since 1.6 + */ +@RunWith(MockitoJUnitRunner.class) +public class StoredProcedureAttributeSourceUnitTests { + + StoredProcedureAttributeSource creator; + @Mock JpaEntityMetadata entityMetadata; + + @Before + public void setup() { + + creator = StoredProcedureAttributeSource.INSTANCE; + + when(entityMetadata.getJavaType()).thenReturn(User.class); + when(entityMetadata.getEntityName()).thenReturn("User"); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithImplicitProcedureName() { + + StoredProcedureAttributes attr = creator.createFrom(method("plus1inout", Integer.class), entityMetadata); + + assertThat(attr.getProcedureName(), is("plus1inout")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is(nullValue())); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictName() { + + StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), + entityMetadata); + + assertThat(attr.getProcedureName(), is("plus1inout")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is(nullValue())); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameValue() { + + StoredProcedureAttributes attr = creator.createFrom(method("explicitlyNamedPlus1inout", Integer.class), + entityMetadata); + + assertThat(attr.getProcedureName(), is("plus1inout")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is(nullValue())); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodWithExplictProcedureNameAlias() { + + StoredProcedureAttributes attr = creator.createFrom( + method("explicitPlus1inoutViaProcedureNameAlias", Integer.class), entityMetadata); + + assertThat(attr.getProcedureName(), is("plus1inout")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is(nullValue())); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithExplicitlyNamedProcedure() { + + StoredProcedureAttributes attr = creator.createFrom( + method("entityAnnotatedCustomNamedProcedurePlus1IO", Integer.class), entityMetadata); + + assertThat(attr.getProcedureName(), is("User.plus1IO")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is("res")); + } + + /** + * @see DATAJPA-455 + */ + @Test + public void shouldCreateStoredProcedureAttributesFromProcedureMethodBackedWithImplicitlyNamedProcedure() { + + StoredProcedureAttributes attr = creator.createFrom(method("plus1", Integer.class), entityMetadata); + + assertThat(attr.getProcedureName(), is("User.plus1")); + assertThat(attr.getOutputParameterType(), is(typeCompatibleWith(Integer.class))); + assertThat(attr.getOutputParameterName(), is("res")); + } + + private static Method method(String name, Class... paramTypes) { + return ReflectionUtils.findMethod(DummyRepository.class, name, paramTypes); + } + + /** + * @author Thomas Darimont + */ + static interface DummyRepository { + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database. + * + * @see DATAJPA-455 + */ + @Procedure("plus1inout") + Integer explicitlyNamedPlus1inout(Integer arg); + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database via alias. + * + * @see DATAJPA-455 + */ + @Procedure(procedureName = "plus1inout") + Integer explicitPlus1inoutViaProcedureNameAlias(Integer arg); + + /** + * Implicitly mapped to a procedure with name "plus1inout" in database via alias. + * + * @see DATAJPA-455 + */ + @Procedure + Integer plus1inout(Integer arg); + + /** + * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. + * + * @see DATAJPA-455 + */ + @Procedure(name = "User.plus1IO") + Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); + + /** + * Implicitly mapped to named stored procedure "User.plus1" in {@link EntityManager}. + * + * @see DATAJPA-455 + */ + @Procedure + Integer plus1(@Param("arg") Integer arg); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 904021569..0b04c3321 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.List; import java.util.Set; +import javax.persistence.EntityManager; import javax.persistence.QueryHint; import org.springframework.data.domain.Page; @@ -33,6 +34,7 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; @@ -334,4 +336,36 @@ public interface UserRepository extends JpaRepository, JpaSpecifi */ // @Query(value = "select u.binaryData from User u where u.id = :id") // byte[] findBinaryDataByIdJpaQl(@Param("id") Integer id); + + /** + * Explicitly mapped to a procedure with name "plus1inout" in database. + * + * @see DATAJPA-455 + */ + @Procedure("plus1inout") + Integer explicitlyNamedPlus1inout(Integer arg); + + /** + * Implicitly mapped to a procedure with name "plus1inout" in database via alias. + * + * @see DATAJPA-455 + */ + @Procedure(procedureName = "plus1inout") + Integer plus1inout(Integer arg); + + /** + * Explicitly mapped to named stored procedure "User.plus1IO" in {@link EntityManager}. + * + * @see DATAJPA-455 + */ + @Procedure(name = "User.plus1IO") + Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg); + + /** + * Implicitly mapped to named stored procedure "User.plus1" in {@link EntityManager}. + * + * @see DATAJPA-455 + */ + @Procedure + Integer plus1(@Param("arg") Integer arg); } diff --git a/src/test/java/org/springframework/data/jpa/support/EntityManagerTestUtils.java b/src/test/java/org/springframework/data/jpa/support/EntityManagerTestUtils.java new file mode 100644 index 000000000..f1797888f --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/support/EntityManagerTestUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 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 + * + * http://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.support; + +import javax.persistence.EntityManager; + +import org.springframework.util.ReflectionUtils; + +/** + * Utility class with {@link EntityManager} related helper methods. + * + * @author Thomas Darimont + */ +public abstract class EntityManagerTestUtils { + + private EntityManagerTestUtils() {} + + public static boolean currentEntityManagerIsAJpa21EntityManager(EntityManager em) { + return ReflectionUtils.findMethod(((org.springframework.orm.jpa.EntityManagerProxy) em).getTargetEntityManager() + .getClass(), "getEntityGraph", String.class) != null; + } +} diff --git a/src/test/resources/infrastructure.xml b/src/test/resources/infrastructure.xml index 370015f25..db96fca62 100644 --- a/src/test/resources/infrastructure.xml +++ b/src/test/resources/infrastructure.xml @@ -24,6 +24,8 @@ - + + + diff --git a/src/test/resources/openjpa.xml b/src/test/resources/openjpa.xml index b84c743ad..4ceca067d 100644 --- a/src/test/resources/openjpa.xml +++ b/src/test/resources/openjpa.xml @@ -1,17 +1,25 @@ + http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> - - + + - + none + + diff --git a/src/test/resources/scripts/schema-stored-procedures.sql b/src/test/resources/scripts/schema-stored-procedures.sql new file mode 100644 index 000000000..dee6b566c --- /dev/null +++ b/src/test/resources/scripts/schema-stored-procedures.sql @@ -0,0 +1,8 @@ +/; +DROP procedure IF EXISTS plus1inout +/; +CREATE procedure plus1inout (IN arg int, OUT res int) +BEGIN ATOMIC + set res = arg + 1; +END +/; \ No newline at end of file