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