Skip to content

Commit 554bd3d

Browse files
GabrielBBgregturn
authored andcommitted
Add REF_CURSOR support for stored procedures.
When registered procedure output parameters, if the method annotated with @procedure has a collection return type or an entity return type, then use ResultSet. Otherwise, handle either the array (Object[]) or the primitive types. * Throw a proper exception when using an @Procedure-annotated method and no transaction is active. * Introduce refCursor as a boolean flag to @procedure denoting when to use REF_CURSORs for OUT parameters.. * Extract a ProcedureParameter command object to ease usage of a parameter's name, type and mode. (NOTE: javax.persistence already has "StoredProcedureParameter".) NOTE: Integration testing of stored procedures is very limited by lack of HSQL's support for REF CURSORS. See ##2256 to track future work for potential testing against MySQL, Oracle, Postgres, or SQL Server. See: #1959, #409. Related: #2014.
1 parent 2e30a35 commit 554bd3d

10 files changed

+364
-132
lines changed

src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
* @author Christoph Strobl
5454
* @author Nicolas Cirigliano
5555
* @author Jens Schauder
56+
* @author Gabriel Basilio
5657
*/
5758
public abstract class JpaQueryExecution {
5859

@@ -301,6 +302,8 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso
301302
*/
302303
static class ProcedureExecution extends JpaQueryExecution {
303304

305+
private static final String NO_SURROUNDING_TRANSACTION = "You're trying to execute a @Procedure method without a surrounding transaction that keeps the connection open so that the ResultSet can actually be consumed. Make sure the consumer code uses @Transactional or any other way of declaring a (read-only) transaction.";
306+
304307
/*
305308
* (non-Javadoc)
306309
* @see org.springframework.data.jpa.repository.query.JpaQueryExecution#doExecute(org.springframework.data.jpa.repository.query.AbstractJpaQuery, java.lang.Object[])
@@ -312,7 +315,24 @@ protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAcce
312315

313316
StoredProcedureJpaQuery storedProcedureJpaQuery = (StoredProcedureJpaQuery) jpaQuery;
314317
StoredProcedureQuery storedProcedure = storedProcedureJpaQuery.createQuery(accessor);
315-
storedProcedure.execute();
318+
319+
boolean returnsResultSet = storedProcedure.execute();
320+
321+
if (returnsResultSet) {
322+
if (!SurroundingTransactionDetectorMethodInterceptor.INSTANCE.isSurroundingTransactionActive())
323+
throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION);
324+
325+
List<?> result = storedProcedure.getResultList();
326+
327+
if (!storedProcedureJpaQuery.getQueryMethod().isCollectionQuery()) {
328+
if (result.isEmpty())
329+
return null;
330+
if (result.size() == 1)
331+
return result.get(0);
332+
}
333+
334+
return result;
335+
}
316336

317337
return storedProcedureJpaQuery.extractOutputValue(storedProcedure);
318338
}

src/main/java/org/springframework/data/jpa/repository/query/Procedure.java

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* @author Thomas Darimont
2727
* @author Oliver Gierke
2828
* @author Christoph Strobl
29+
* @author Gabriel Basilio
2930
* @since 1.6
3031
*/
3132
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@@ -51,4 +52,9 @@
5152
* The name of the outputParameter, defaults to {@code ""}.
5253
*/
5354
String outputParameterName() default "";
55+
56+
/**
57+
* Whether the procedure returns a Ref Cursor from the database {@code false}.
58+
*/
59+
boolean refCursor() default false;
5460
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.jpa.repository.query;
18+
19+
import org.springframework.lang.Nullable;
20+
21+
import javax.persistence.ParameterMode;
22+
23+
/**
24+
* This class represents a Stored Procedure Parameter
25+
* and an instance of the annotation {@link javax.persistence.StoredProcedureParameter}.
26+
*
27+
* @author Gabriel Basilio
28+
*/
29+
public class ProcedureParameter {
30+
31+
private final String name;
32+
private final ParameterMode mode;
33+
private final Class<?> type;
34+
35+
public ProcedureParameter(@Nullable String name, ParameterMode mode, Class<?> type) {
36+
this.name = name;
37+
this.mode = mode;
38+
this.type = type;
39+
}
40+
41+
public String getName() {
42+
return name;
43+
}
44+
45+
public ParameterMode getMode() {
46+
return mode;
47+
}
48+
49+
public Class<?> getType() {
50+
return type;
51+
}
52+
}

src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java

+15-12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import javax.persistence.NamedStoredProcedureQueries;
2424
import javax.persistence.NamedStoredProcedureQuery;
25+
import javax.persistence.ParameterMode;
2526
import javax.persistence.StoredProcedureParameter;
2627

2728
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -38,6 +39,7 @@
3839
* @author Mark Paluch
3940
* @author Diego Diez
4041
* @author Jeff Sheets
42+
* @author Gabriel Basilio
4143
* @since 1.6
4244
*/
4345
enum StoredProcedureAttributeSource {
@@ -72,7 +74,7 @@ public StoredProcedureAttributes createFrom(Method method, JpaEntityMetadata<?>
7274
+ method);
7375
}
7476

75-
return new StoredProcedureAttributes(procedureName, procedure.outputParameterName(), method.getReturnType());
77+
return new StoredProcedureAttributes(procedureName, createOutputProcedureParameterFrom(method, procedure));
7678
}
7779

7880
/**
@@ -102,28 +104,29 @@ private String deriveProcedureNameFrom(Method method, Procedure procedure) {
102104
private StoredProcedureAttributes newProcedureAttributesFrom(Method method,
103105
NamedStoredProcedureQuery namedStoredProc, Procedure procedure) {
104106

105-
List<String> outputParameterNames = new ArrayList<>();
106-
List<Class<?>> outputParameterTypes = new ArrayList<>();
107+
List<ProcedureParameter> outputParameters = new ArrayList<>();
107108

108109
if (!procedure.outputParameterName().isEmpty()) {
109110
// we give the output parameter definition from the @Procedure annotation precedence
110-
outputParameterNames.add(procedure.outputParameterName());
111+
outputParameters.add(createOutputProcedureParameterFrom(method, procedure));
111112
} else {
112113

113114
// try to discover the output parameter
114-
List<StoredProcedureParameter> outputParameters = extractOutputParametersFrom(namedStoredProc);
115+
List<StoredProcedureParameter> namedProcedureOutputParameters = extractOutputParametersFrom(namedStoredProc);
115116

116-
for (StoredProcedureParameter outputParameter : outputParameters) {
117-
outputParameterNames.add(outputParameter.name());
118-
outputParameterTypes.add(outputParameter.type());
117+
for (StoredProcedureParameter outputParameter : namedProcedureOutputParameters) {
118+
outputParameters.add(new ProcedureParameter(
119+
outputParameter.name(), outputParameter.mode(), outputParameter.type()));
119120
}
120121
}
121122

122-
if (outputParameterTypes.isEmpty()) {
123-
outputParameterTypes.add(method.getReturnType());
124-
}
123+
return new StoredProcedureAttributes(namedStoredProc.name(), outputParameters, true);
124+
}
125125

126-
return new StoredProcedureAttributes(namedStoredProc.name(), outputParameterNames, outputParameterTypes, true);
126+
private ProcedureParameter createOutputProcedureParameterFrom(Method method, Procedure procedure) {
127+
return new ProcedureParameter(procedure.outputParameterName(),
128+
procedure.refCursor() ? ParameterMode.REF_CURSOR : ParameterMode.OUT,
129+
method.getReturnType());
127130
}
128131

129132
private List<StoredProcedureParameter> extractOutputParametersFrom(NamedStoredProcedureQuery namedStoredProc) {

src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java

+32-39
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @author Mark Paluch
3434
* @author Jeff Sheets
3535
* @author Jens Schauder
36+
* @author Gabriel Basilio
3637
* @since 1.6
3738
*/
3839
class StoredProcedureAttributes {
@@ -42,52 +43,51 @@ class StoredProcedureAttributes {
4243

4344
private final boolean namedStoredProcedure;
4445
private final String procedureName;
45-
private final List<String> outputParameterNames;
46-
private final List<Class<?>> outputParameterTypes;
46+
private final List<ProcedureParameter> outputProcedureParameters;
4747

4848
/**
4949
* Creates a new {@link StoredProcedureAttributes}.
5050
*
5151
* @param procedureName must not be {@literal null}.
52-
* @param outputParameterName may be {@literal null}.
53-
* @param outputParameterType must not be {@literal null}.
5452
*/
55-
StoredProcedureAttributes(String procedureName, @Nullable String outputParameterName,
56-
Class<?> outputParameterType) {
57-
this(procedureName, Collections.singletonList(outputParameterName), Collections.singletonList(outputParameterType), false);
53+
StoredProcedureAttributes(String procedureName, ProcedureParameter parameter) {
54+
this(procedureName, Collections.singletonList(parameter), false);
5855
}
5956

6057
/**
6158
* Creates a new {@link StoredProcedureAttributes}.
6259
*
6360
* @param procedureName must not be {@literal null}.
64-
* @param outputParameterNames may be empty, but not {@literal null}.
65-
* @param outputParameterTypes must not be empty, and cannot be a single element of {@literal null}.
6661
* @param namedStoredProcedure flag signaling if the stored procedure has a name.
6762
*/
68-
StoredProcedureAttributes(String procedureName, List<String> outputParameterNames,
69-
List<Class<?>> outputParameterTypes, boolean namedStoredProcedure) {
63+
StoredProcedureAttributes(String procedureName, List<ProcedureParameter> outputProcedureParameters, boolean namedStoredProcedure) {
7064

7165
Assert.notNull(procedureName, "ProcedureName must not be null!");
72-
Assert.notNull(outputParameterNames, "OutputParameterNames must not be null!");
73-
Assert.notEmpty(outputParameterTypes, "OutputParameterTypes must not be empty!");
74-
Assert.isTrue(outputParameterTypes.size() != 1 || outputParameterTypes.get(0) != null, "OutputParameterTypes must not have size 1 with a null value");
66+
Assert.notNull(outputProcedureParameters, "OutputProcedureParameters must not be null!");
67+
Assert.isTrue(outputProcedureParameters.size() != 1 || outputProcedureParameters.get(0) != null, "ProcedureParameters must not have size 1 with a null value");
7568

7669
this.procedureName = procedureName;
77-
this.outputParameterNames = namedStoredProcedure
78-
? outputParameterNames
79-
: completeOutputParameterNames(outputParameterNames);
80-
this.outputParameterTypes = outputParameterTypes;
8170
this.namedStoredProcedure = namedStoredProcedure;
82-
}
8371

84-
private List<String> completeOutputParameterNames(List<String> outputParameterNames) {
72+
if (namedStoredProcedure) {
73+
this.outputProcedureParameters = outputProcedureParameters;
74+
} else {
75+
this.outputProcedureParameters = getParametersWithCompletedNames(outputProcedureParameters);
76+
}
77+
}
8578

86-
return IntStream.range(0, outputParameterNames.size()) //
87-
.mapToObj(i -> completeOutputParameterName(i, outputParameterNames.get(i))) //
79+
private List<ProcedureParameter> getParametersWithCompletedNames(List<ProcedureParameter> procedureParameters) {
80+
return IntStream.range(0, procedureParameters.size())
81+
.mapToObj(i -> getParameterWithCompletedName(procedureParameters.get(i), i))
8882
.collect(Collectors.toList());
8983
}
9084

85+
private ProcedureParameter getParameterWithCompletedName(ProcedureParameter parameter, int i) {
86+
return new ProcedureParameter(
87+
completeOutputParameterName(i, parameter.getName()),
88+
parameter.getMode(), parameter.getType());
89+
}
90+
9191
private String completeOutputParameterName(int i, String paramName) {
9292

9393
return StringUtils.hasText(paramName) //
@@ -113,26 +113,15 @@ public String getProcedureName() {
113113
*
114114
* @return
115115
*/
116-
public List<String> getOutputParameterNames() {
117-
return outputParameterNames;
118-
}
119-
120-
/**
121-
* Returns the types of the output parameters.
122-
*
123-
* @return
124-
*/
125-
public List<Class<?>> getOutputParameterTypes() {
126-
return outputParameterTypes;
116+
public boolean isNamedStoredProcedure() {
117+
return namedStoredProcedure;
127118
}
128119

129120
/**
130-
* Returns whether the stored procedure is a named one.
131-
*
132-
* @return
121+
* @return Returns the stored procedure output parameter list
133122
*/
134-
public boolean isNamedStoredProcedure() {
135-
return namedStoredProcedure;
123+
public List<ProcedureParameter> getOutputProcedureParameters() {
124+
return outputProcedureParameters;
136125
}
137126

138127
/**
@@ -141,6 +130,10 @@ public boolean isNamedStoredProcedure() {
141130
* @return
142131
*/
143132
public boolean hasReturnValue() {
144-
return !(outputParameterTypes.size() == 1 && (void.class.equals(outputParameterTypes.get(0)) || Void.class.equals(outputParameterTypes.get(0))));
133+
if (getOutputProcedureParameters().isEmpty())
134+
return false;
135+
136+
Class<?> outputType = getOutputProcedureParameters().get(0).getType();
137+
return !(void.class.equals(outputType) || Void.class.equals(outputType));
145138
}
146139
}

0 commit comments

Comments
 (0)