1
16 package org.springframework.data.jpa.repository.query;
17
18 import java.util.List;
19
20 import javax.persistence.EntityManager;
21 import javax.persistence.Query;
22 import javax.persistence.TypedQuery;
23 import javax.persistence.criteria.CriteriaBuilder;
24 import javax.persistence.criteria.CriteriaQuery;
25
26 import org.springframework.data.domain.Sort;
27 import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
28 import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
29 import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
30 import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
31 import org.springframework.data.repository.query.ResultProcessor;
32 import org.springframework.data.repository.query.ReturnedType;
33 import org.springframework.data.repository.query.parser.Part;
34 import org.springframework.data.repository.query.parser.Part.Type;
35 import org.springframework.data.repository.query.parser.PartTree;
36 import org.springframework.data.util.Streamable;
37 import org.springframework.lang.Nullable;
38
39
49 public class PartTreeJpaQuery extends AbstractJpaQuery {
50
51 private final PartTree tree;
52 private final JpaParameters parameters;
53
54 private final QueryPreparer query;
55 private final QueryPreparer countQuery;
56 private final EntityManager em;
57 private final EscapeCharacter escape;
58
59
65 PartTreeJpaQuery(JpaQueryMethod method, EntityManager em) {
66 this(method, em, EscapeCharacter.DEFAULT);
67 }
68
69
76 PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, EscapeCharacter escape) {
77
78 super(method, em);
79
80 this.em = em;
81 this.escape = escape;
82 Class<?> domainClass = method.getEntityInformation().getJavaType();
83 this.parameters = method.getParameters();
84
85 boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically();
86
87 try {
88
89 this.tree = new PartTree(method.getName(), domainClass);
90 validate(tree, parameters, method.toString());
91 this.countQuery = new CountQueryPreparer(recreationRequired);
92 this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(recreationRequired);
93
94 } catch (Exception o_O) {
95 throw new IllegalArgumentException(
96 String.format("Failed to create query for method %s! %s", method, o_O.getMessage()), o_O);
97 }
98 }
99
100
104 @Override
105 public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
106 return query.createQuery(accessor);
107 }
108
109
113 @Override
114 @SuppressWarnings("unchecked")
115 public TypedQuery<Long> doCreateCountQuery(JpaParametersParameterAccessor accessor) {
116 return (TypedQuery<Long>) countQuery.createQuery(accessor);
117 }
118
119
123 @Override
124 protected JpaQueryExecution getExecution() {
125
126 if (this.tree.isDelete()) {
127 return new DeleteExecution(em);
128 } else if (this.tree.isExistsProjection()) {
129 return new ExistsExecution();
130 }
131
132 return super.getExecution();
133 }
134
135 private static void validate(PartTree tree, JpaParameters parameters, String methodName) {
136
137 int argCount = 0;
138
139 Iterable<Part> parts = () -> tree.stream().flatMap(Streamable::stream).iterator();
140
141 for (Part part : parts) {
142
143 int numberOfArguments = part.getNumberOfArguments();
144
145 for (int i = 0; i < numberOfArguments; i++) {
146
147 throwExceptionOnArgumentMismatch(methodName, part, parameters, argCount);
148
149 argCount++;
150 }
151 }
152 }
153
154 private static void throwExceptionOnArgumentMismatch(String methodName, Part part, JpaParameters parameters,
155 int index) {
156
157 Type type = part.getType();
158 String property = part.getProperty().toDotPath();
159
160 if (!parameters.getBindableParameters().hasParameterAt(index)) {
161 throw new IllegalStateException(String.format(
162 "Method %s expects at least %d arguments but only found %d. This leaves an operator of type %s for property %s unbound.",
163 methodName, index + 1, index, type.name(), property));
164 }
165
166 JpaParameter parameter = parameters.getBindableParameter(index);
167
168 if (expectsCollection(type) && !parameterIsCollectionLike(parameter)) {
169 throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "Collection", parameter));
170 } else if (!expectsCollection(type) && !parameterIsScalarLike(parameter)) {
171 throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "scalar", parameter));
172 }
173 }
174
175 private static String wrongParameterTypeMessage(String methodName, String property, Type operatorType,
176 String expectedArgumentType, JpaParameter parameter) {
177
178 return String.format("Operator %s on %s requires a %s argument, found %s in method %s.", operatorType.name(),
179 property, expectedArgumentType, parameter.getType(), methodName);
180 }
181
182 private static boolean parameterIsCollectionLike(JpaParameter parameter) {
183 return Iterable.class.isAssignableFrom(parameter.getType()) || parameter.getType().isArray();
184 }
185
186
189 private static boolean parameterIsScalarLike(JpaParameter parameter) {
190 return !Iterable.class.isAssignableFrom(parameter.getType());
191 }
192
193 private static boolean expectsCollection(Type type) {
194 return type == Type.IN || type == Type.NOT_IN;
195 }
196
197
203 private class QueryPreparer {
204
205 private final @Nullable CriteriaQuery<?> cachedCriteriaQuery;
206 private final @Nullable ParameterBinder cachedParameterBinder;
207 private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
208
209 QueryPreparer(boolean recreateQueries) {
210
211 JpaQueryCreator creator = createCreator(null);
212
213 if (recreateQueries) {
214 this.cachedCriteriaQuery = null;
215 this.cachedParameterBinder = null;
216 } else {
217 this.cachedCriteriaQuery = creator.createQuery();
218 this.cachedParameterBinder = getBinder(creator.getParameterExpressions());
219 }
220 }
221
222
225 public Query createQuery(JpaParametersParameterAccessor accessor) {
226
227 CriteriaQuery<?> criteriaQuery = cachedCriteriaQuery;
228 ParameterBinder parameterBinder = cachedParameterBinder;
229
230 if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) {
231 JpaQueryCreator creator = createCreator(accessor);
232 criteriaQuery = creator.createQuery(getDynamicSort(accessor));
233 List<ParameterMetadata<?>> expressions = creator.getParameterExpressions();
234 parameterBinder = getBinder(expressions);
235 }
236
237 if (parameterBinder == null) {
238 throw new IllegalStateException("ParameterBinder is null!");
239 }
240
241 TypedQuery<?> query = createQuery(criteriaQuery);
242
243 return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache));
244 }
245
246
250 @SuppressWarnings("ConstantConditions")
251 private Query restrictMaxResultsIfNecessary(Query query) {
252
253 if (tree.isLimiting()) {
254
255 if (query.getMaxResults() != Integer.MAX_VALUE) {
256
262 if (query.getMaxResults() > tree.getMaxResults() && query.getFirstResult() > 0) {
263 query.setFirstResult(query.getFirstResult() - (query.getMaxResults() - tree.getMaxResults()));
264 }
265 }
266
267 query.setMaxResults(tree.getMaxResults());
268 }
269
270 if (tree.isExistsProjection()) {
271 query.setMaxResults(1);
272 }
273
274 return query;
275 }
276
277
284 private TypedQuery<?> createQuery(CriteriaQuery<?> criteriaQuery) {
285
286 if (this.cachedCriteriaQuery != null) {
287 synchronized (this.cachedCriteriaQuery) {
288 return getEntityManager().createQuery(criteriaQuery);
289 }
290 }
291
292 return getEntityManager().createQuery(criteriaQuery);
293 }
294
295 protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
296
297 EntityManager entityManager = getEntityManager();
298
299 CriteriaBuilder builder = entityManager.getCriteriaBuilder();
300 ResultProcessor processor = getQueryMethod().getResultProcessor();
301
302 ParameterMetadataProvider provider;
303 ReturnedType returnedType;
304
305 if (accessor != null) {
306 provider = new ParameterMetadataProvider(builder, accessor, escape);
307 returnedType = processor.withDynamicProjection(accessor).getReturnedType();
308 } else {
309 provider = new ParameterMetadataProvider(builder, parameters, escape);
310 returnedType = processor.getReturnedType();
311 }
312
313 return new JpaQueryCreator(tree, returnedType, builder, provider);
314 }
315
316
319 protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
320 QueryParameterSetter.QueryMetadataCache metadataCache) {
321
322 QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("query", query);
323
324 return binder.bindAndPrepare(query, metadata, accessor);
325 }
326
327 private ParameterBinder getBinder(List<ParameterMetadata<?>> expressions) {
328 return ParameterBinderFactory.createCriteriaBinder(parameters, expressions);
329 }
330
331 private Sort getDynamicSort(JpaParametersParameterAccessor accessor) {
332
333 return parameters.potentiallySortsDynamically()
334 ? accessor.getSort()
335 : Sort.unsorted();
336 }
337 }
338
339
345 private class CountQueryPreparer extends QueryPreparer {
346
347 CountQueryPreparer(boolean recreateQueries) {
348 super(recreateQueries);
349 }
350
351 @Override
352 protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
353
354 EntityManager entityManager = getEntityManager();
355 CriteriaBuilder builder = entityManager.getCriteriaBuilder();
356
357 ParameterMetadataProvider provider;
358
359 if (accessor != null) {
360 provider = new ParameterMetadataProvider(builder, accessor, escape);
361 } else {
362 provider = new ParameterMetadataProvider(builder, parameters, escape);
363 }
364
365 return new JpaCountQueryCreator(tree, getQueryMethod().getResultProcessor().getReturnedType(), builder, provider);
366 }
367
368
371 @Override
372 protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
373 QueryParameterSetter.QueryMetadataCache metadataCache) {
374
375 QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("countquery", query);
376
377 return binder.bind(query, metadata, accessor);
378 }
379 }
380 }
381