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.
This commit is contained in:
Thomas Darimont
2014-04-07 22:29:42 +02:00
committed by Oliver Gierke
parent e32e724fc8
commit 09535813c2
18 changed files with 1003 additions and 8 deletions

View File

@@ -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 extends Query> T applyHints(T query, JpaQueryMethod method) {
protected <T extends Query> T applyHints(T query, JpaQueryMethod method) {
for (QueryHint hint : method.getHints()) {
applyQueryHint(query, hint);

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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<Class<?>> 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;
}
}

View File

@@ -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 "";
}

View File

@@ -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 {

View File

@@ -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<NamedStoredProcedureQuery> 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<NamedStoredProcedureQuery> collectNamedStoredProcedureQueriesFrom(Class<?> entityType) {
List<NamedStoredProcedureQuery> queries = new ArrayList<NamedStoredProcedureQuery>();
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;
}
}

View File

@@ -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));
}
}

View File

@@ -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<Long> 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;
}
}

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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<User> 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);
}
}

View File

@@ -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<User, Integer>, 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);
}

View File

@@ -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;
}
}

View File

@@ -24,6 +24,8 @@
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<jdbc:embedded-database id="dataSource" type="HSQL" />
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script execution="INIT" separator="/;" location="classpath:scripts/schema-stored-procedures.sql"/>
</jdbc:embedded-database>
</beans>

View File

@@ -1,17 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
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">
<!-- EclipseLink vendor adaptor with workaround platform class for HSQL usage -->
<bean id="vendorAdaptor" class="org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter" parent="abstractVendorAdaptor">
<!-- EclipseLink vendor adaptor with workaround platform class for HSQL
usage -->
<bean id="vendorAdaptor" class="org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter"
parent="abstractVendorAdaptor">
<property name="database" value="HSQL" />
</bean>
<util:properties id="jpaProperties">
<prop key="openjpa.Log">none</prop>
</util:properties>
<!-- Needed to override dataSource definition from infrastructure.xml to
make OpenJPA tests work. Open JPA doesn't work with hsqldb 2.x and runs with
1.x instead which doesn't support stored procedures which leads to errors
at runtime when the scripts/schema-stored-procedure.sql is executed, therefore we omit the script here. -->
<jdbc:embedded-database id="dataSource" type="HSQL" />
</beans>

View File

@@ -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
/;