1
16 package org.springframework.data.jpa.repository.query;
17
18 import java.lang.annotation.Annotation;
19 import java.lang.reflect.Method;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.Set;
26
27 import javax.persistence.LockModeType;
28 import javax.persistence.QueryHint;
29
30 import org.springframework.core.annotation.AnnotatedElementUtils;
31 import org.springframework.core.annotation.AnnotationUtils;
32 import org.springframework.data.jpa.provider.QueryExtractor;
33 import org.springframework.data.jpa.repository.EntityGraph;
34 import org.springframework.data.jpa.repository.Lock;
35 import org.springframework.data.jpa.repository.Modifying;
36 import org.springframework.data.jpa.repository.Query;
37 import org.springframework.data.jpa.repository.QueryHints;
38 import org.springframework.data.projection.ProjectionFactory;
39 import org.springframework.data.repository.core.RepositoryMetadata;
40 import org.springframework.data.repository.query.Parameter;
41 import org.springframework.data.repository.query.Parameters;
42 import org.springframework.data.repository.query.QueryMethod;
43 import org.springframework.data.util.Lazy;
44 import org.springframework.lang.Nullable;
45 import org.springframework.util.Assert;
46 import org.springframework.util.StringUtils;
47
48
59 public class JpaQueryMethod extends QueryMethod {
60
61
66 private static final Set<Class<?>> NATIVE_ARRAY_TYPES;
67 private static final StoredProcedureAttributeSource storedProcedureAttributeSource = StoredProcedureAttributeSource.INSTANCE;
68
69 static {
70
71 Set<Class<?>> types = new HashSet<>();
72 types.add(byte[].class);
73 types.add(Byte[].class);
74 types.add(char[].class);
75 types.add(Character[].class);
76
77 NATIVE_ARRAY_TYPES = Collections.unmodifiableSet(types);
78 }
79
80 private final QueryExtractor extractor;
81 private final Method method;
82
83 private @Nullable StoredProcedureAttributes storedProcedureAttributes;
84 private final Lazy<LockModeType> lockModeType;
85 private final Lazy<QueryHints> queryHints;
86 private final Lazy<JpaEntityGraph> jpaEntityGraph;
87 private final Lazy<Modifying> modifying;
88 private final Lazy<Boolean> isNativeQuery;
89 private final Lazy<Boolean> isCollectionQuery;
90 private final Lazy<Boolean> isProcedureQuery;
91 private final Lazy<JpaEntityMetadata<?>> entityMetadata;
92
93
101 protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
102 QueryExtractor extractor) {
103
104 super(method, metadata, factory);
105
106 Assert.notNull(method, "Method must not be null!");
107 Assert.notNull(extractor, "Query extractor must not be null!");
108
109 this.method = method;
110 this.extractor = extractor;
111 this.lockModeType = Lazy
112 .of(() -> (LockModeType) Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class))
113 .map(AnnotationUtils::getValue)
114 .orElse(null));
115
116 this.queryHints = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class));
117 this.modifying = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, Modifying.class));
118 this.jpaEntityGraph = Lazy.of(() -> {
119
120 EntityGraph entityGraph = AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class);
121
122 if (entityGraph == null) {
123 return null;
124 }
125
126 return new JpaEntityGraph(entityGraph, getNamedQueryName());
127 });
128 this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class));
129 this.isCollectionQuery = Lazy
130 .of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(method.getReturnType()));
131 this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null);
132 this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
133
134 Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
135 String.format("Modifying method must not contain %s!", Parameters.TYPES));
136 assertParameterNamesInAnnotatedQuery();
137 }
138
139 private void assertParameterNamesInAnnotatedQuery() {
140
141 String annotatedQuery = getAnnotatedQuery();
142
143 if (!DeclaredQuery.of(annotatedQuery).hasNamedParameter()) {
144 return;
145 }
146
147 for (Parameter parameter : getParameters()) {
148
149 if (!parameter.isNamedParameter()) {
150 continue;
151 }
152
153 if (StringUtils.isEmpty(annotatedQuery)
154 || !annotatedQuery.contains(String.format(":%s", parameter.getName().get()))
155 && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) {
156 throw new IllegalStateException(
157 String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'!",
158 method, parameter.getName(), annotatedQuery));
159 }
160 }
161 }
162
163
167 @Override
168 @SuppressWarnings({ "rawtypes", "unchecked" })
169 public JpaEntityMetadata<?> getEntityInformation() {
170 return this.entityMetadata.get();
171 }
172
173
178 @Override
179 public boolean isModifyingQuery() {
180 return modifying.getNullable() != null;
181 }
182
183
188 List<QueryHint> getHints() {
189
190 QueryHints hints = this.queryHints.getNullable();
191 if (hints != null) {
192 return Arrays.asList(hints.value());
193 }
194
195 return Collections.emptyList();
196 }
197
198
203 @Nullable
204 LockModeType getLockModeType() {
205 return lockModeType.getNullable();
206 }
207
208
214 @Nullable
215 JpaEntityGraph getEntityGraph() {
216 return jpaEntityGraph.getNullable();
217 }
218
219
225 boolean applyHintsToCountQuery() {
226
227 QueryHints hints = this.queryHints.getNullable();
228 return hints != null ? hints.forCounting() : false;
229 }
230
231
236 QueryExtractor getQueryExtractor() {
237 return extractor;
238 }
239
240
245 Class<?> getReturnType() {
246 return method.getReturnType();
247 }
248
249
255 @Nullable
256 String getAnnotatedQuery() {
257
258 String query = getAnnotationValue("value", String.class);
259 return StringUtils.hasText(query) ? query : null;
260 }
261
262
270 String getRequiredAnnotatedQuery() throws IllegalStateException {
271
272 String query = getAnnotatedQuery();
273
274 if (query != null) {
275 return query;
276 }
277
278 throw new IllegalStateException(String.format("No annotated query found for query method %s!", getName()));
279 }
280
281
287 @Nullable
288 String getCountQuery() {
289
290 String countQuery = getAnnotationValue("countQuery", String.class);
291 return StringUtils.hasText(countQuery) ? countQuery : null;
292 }
293
294
301 @Nullable
302 String getCountQueryProjection() {
303
304 String countProjection = getAnnotationValue("countProjection", String.class);
305 return StringUtils.hasText(countProjection) ? countProjection : null;
306 }
307
308
313 boolean isNativeQuery() {
314 return this.isNativeQuery.get();
315 }
316
317
321 @Override
322 public String getNamedQueryName() {
323
324 String annotatedName = getAnnotationValue("name", String.class);
325 return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName();
326 }
327
328
333 String getNamedCountQueryName() {
334
335 String annotatedName = getAnnotationValue("countName", String.class);
336 return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count";
337 }
338
339
344 boolean getFlushAutomatically() {
345 return getMergedOrDefaultAnnotationValue("flushAutomatically", Modifying.class, Boolean.class);
346 }
347
348
353 boolean getClearAutomatically() {
354 return getMergedOrDefaultAnnotationValue("clearAutomatically", Modifying.class, Boolean.class);
355 }
356
357
365 private <T> T getAnnotationValue(String attribute, Class<T> type) {
366 return getMergedOrDefaultAnnotationValue(attribute, Query.class, type);
367 }
368
369 @SuppressWarnings({ "rawtypes", "unchecked" })
370 private <T> T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class<T> targetType) {
371
372 Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType);
373 if (annotation == null) {
374 return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute));
375 }
376
377 return targetType.cast(AnnotationUtils.getValue(annotation, attribute));
378 }
379
380
384 @Override
385 protected JpaParameters createParameters(Method method) {
386 return new JpaParameters(method);
387 }
388
389
393 @Override
394 public JpaParameters getParameters() {
395 return (JpaParameters) super.getParameters();
396 }
397
398
402 @Override
403 public boolean isCollectionQuery() {
404 return this.isCollectionQuery.get();
405 }
406
407
412 public boolean isProcedureQuery() {
413 return this.isProcedureQuery.get();
414 }
415
416
422 StoredProcedureAttributes getProcedureAttributes() {
423
424 if (storedProcedureAttributes == null) {
425 this.storedProcedureAttributes = storedProcedureAttributeSource.createFrom(method, getEntityInformation());
426 }
427
428 return storedProcedureAttributes;
429 }
430 }
431