Skip to content

Commit 2a8230e

Browse files
committed
QueryUtils uses JSqlParser if possible.
`QueryUtils` can now decide at runtime if a given query can be processed using the JSqlParser or the (default) custom parser implementation, that can parse more than just standard SQL queries. JSqlParser is selected if the query qualifies as a native query (which means it does not use any special features such as SpEL and is marked with `nativeQuery=true`). Related tickets spring-projects#2409
1 parent 1f702d6 commit 2a8230e

13 files changed

+381
-56
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2008-2021 the original author or authors.
2+
* Copyright 2008-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
8686
@Override
8787
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
8888

89-
String sortedQueryString = DefaultQueryUtils.applySorting(query.getQueryString(), accessor.getSort(),
90-
query.getAlias());
89+
String sortedQueryString = QueryUtils.applySorting(query, accessor.getSort(), query.getAlias());
9190
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
9291

9392
Query query = createJpaQuery(sortedQueryString, processor.getReturnedType());

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

+9
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,13 @@ default boolean usesPaging() {
102102
* @since 2.0.6
103103
*/
104104
boolean usesJdbcStyleParameters();
105+
106+
/**
107+
* Return whether the query is a native query of not.
108+
*
109+
* @return <code>true</code> if native query otherwise <code>false</code>
110+
*/
111+
default boolean isNativeQuery() {
112+
return false;
113+
}
105114
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class ExpressionBasedStringQuery extends StringQuery {
6060
*/
6161
public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, SpelExpressionParser parser,
6262
boolean nativeQuery) {
63-
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery);
63+
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query));
6464
}
6565

6666
/**

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate
201201
query = query.select((Root) root);
202202
}
203203

204-
CriteriaQuery<? extends Object> select = query.orderBy(DefaultQueryUtils.toOrders(sort, root, builder));
204+
CriteriaQuery<? extends Object> select = query.orderBy(QueryUtils.toOrders(sort, root, builder));
205205
return predicate == null ? select : select.where(predicate);
206206
}
207207

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ interface QueryParameterSetter {
4949
void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
5050

5151
/** Noop implementation */
52-
QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
52+
QueryParameterSetter NOOP = (query, values, errorHandling) -> {
53+
};
5354

5455
/**
5556
* {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.
@@ -211,7 +212,7 @@ class QueryMetadata {
211212

212213
QueryMetadata(Query query) {
213214

214-
this.namedParameters = DefaultQueryUtils.hasNamedParameter(query);
215+
this.namedParameters = QueryUtils.hasNamedParameter(query);
215216
this.parameters = query.getParameters();
216217

217218
// DATAJPA-1172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/*
2+
* Copyright 2022 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+
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.criteria.CriteriaBuilder;
23+
import javax.persistence.criteria.From;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
27+
import org.springframework.data.domain.Sort;
28+
import org.springframework.lang.Nullable;
29+
30+
/**
31+
* Simple utility class to create JPA queries.
32+
*
33+
* @author Diego Krupitza
34+
*/
35+
public abstract class QueryUtils {
36+
37+
private static final Log LOG = LogFactory.getLog(QueryUtils.class);
38+
39+
private static final boolean JSQLPARSER_IN_CLASSPATH = isJSqlParserInClassPath();
40+
41+
private QueryUtils() {
42+
}
43+
44+
/**
45+
* Returns the query string to execute an exists query for the given id attributes.
46+
*
47+
* @param entityName the name of the entity to create the query for, must not be {@literal null}.
48+
* @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
49+
* @param idAttributes the id attributes for the entity, must not be {@literal null}.
50+
*/
51+
public static String getExistsQueryString(String entityName, String countQueryPlaceHolder,
52+
Iterable<String> idAttributes) {
53+
if (JSQLPARSER_IN_CLASSPATH) {
54+
return JSqlParserQueryUtils.getExistsQueryString(entityName, countQueryPlaceHolder, idAttributes);
55+
} else {
56+
return DefaultQueryUtils.getExistsQueryString(entityName, countQueryPlaceHolder, idAttributes);
57+
}
58+
}
59+
60+
/**
61+
* Returns the query string for the given class name.
62+
*
63+
* @param template must not be {@literal null}.
64+
* @param entityName must not be {@literal null}.
65+
* @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
66+
*/
67+
public static String getQueryString(String template, String entityName) {
68+
if (JSQLPARSER_IN_CLASSPATH) {
69+
return JSqlParserQueryUtils.getQueryString(template, entityName);
70+
} else {
71+
return DefaultQueryUtils.getQueryString(template, entityName);
72+
}
73+
}
74+
75+
/**
76+
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
77+
*
78+
* @param query the query string to which sorting is applied
79+
* @param sort the sort specification to apply.
80+
* @return the modified query string.
81+
* @deprecated in favor of {@link QueryUtils#applySorting(DeclaredQuery, Sort)}
82+
*/
83+
@Deprecated
84+
public static String applySorting(String query, Sort sort) {
85+
return applySorting(query, sort, detectAlias(query));
86+
}
87+
88+
/**
89+
* Adds {@literal order by} clause to the JPQL query.
90+
*
91+
* @param query the query string to which sorting is applied. Must not be {@literal null} or empty.
92+
* @param sort the sort specification to apply.
93+
* @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
94+
* @return the modified query string.
95+
* @deprecated in favour of {@link QueryUtils#applySorting(DeclaredQuery, Sort, String)}
96+
*/
97+
@Deprecated
98+
public static String applySorting(String query, Sort sort, @Nullable String alias) {
99+
return DefaultQueryUtils.applySorting(query, sort, alias);
100+
}
101+
102+
/**
103+
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
104+
*
105+
* @param query the query string to which sorting is applied
106+
* @param sort the sort specification to apply.
107+
* @return the modified query string.
108+
*/
109+
public static String applySorting(DeclaredQuery query, Sort sort) {
110+
return applySorting(query, sort, detectAlias(query));
111+
}
112+
113+
/**
114+
* Adds {@literal order by} clause to the JPQL query.
115+
*
116+
* @param query the query string to which sorting is applied. Must not be {@literal null} or empty.
117+
* @param sort the sort specification to apply.
118+
* @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
119+
* @return the modified query string.
120+
*/
121+
public static String applySorting(DeclaredQuery query, Sort sort, @Nullable String alias) {
122+
if (qualifiesForJSqlParserUsage(query)) {
123+
return JSqlParserQueryUtils.applySorting(query.getQueryString(), sort, alias);
124+
} else {
125+
return DefaultQueryUtils.applySorting(query.getQueryString(), sort, alias);
126+
}
127+
}
128+
129+
/**
130+
* Resolves the alias for the entity to be retrieved from the given JPA query.
131+
*
132+
* @param query must not be {@literal null}.
133+
* @return Might return {@literal null}.
134+
* @deprecated in favor of {@link QueryUtils#detectAlias(DeclaredQuery)}
135+
*/
136+
@Nullable
137+
@Deprecated
138+
public static String detectAlias(String query) {
139+
return DeclaredQuery.of(query, false).getAlias();
140+
}
141+
142+
/**
143+
* Resolves the alias for the entity to be retrieved from the given JPA query.
144+
*
145+
* @param query must not be {@literal null}.
146+
* @return Might return {@literal null}.
147+
*/
148+
@Nullable
149+
public static String detectAlias(DeclaredQuery query) {
150+
if (qualifiesForJSqlParserUsage(query)) {
151+
return JSqlParserQueryUtils.detectAlias(query.getQueryString());
152+
} else {
153+
return DefaultQueryUtils.detectAlias(query.getQueryString());
154+
}
155+
}
156+
157+
/**
158+
* Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
159+
* entities to the query.
160+
*
161+
* @param <T> type of the entities.
162+
* @param queryString must not be {@literal null}.
163+
* @param entities must not be {@literal null}.
164+
* @param entityManager must not be {@literal null}.
165+
* @return Guaranteed to be not {@literal null}.
166+
*/
167+
public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {
168+
return DefaultQueryUtils.applyAndBind(queryString, entities, entityManager);
169+
}
170+
171+
/**
172+
* Creates a count projected query from the given original query.
173+
*
174+
* @param originalQuery must not be {@literal null} or empty.
175+
* @return Guaranteed to be not {@literal null}.
176+
* @deprecated in favor of {@link QueryUtils#createCountQueryFor(DeclaredQuery)}
177+
*/
178+
@Deprecated
179+
public static String createCountQueryFor(String originalQuery) {
180+
return createCountQueryFor(originalQuery, null);
181+
}
182+
183+
/**
184+
* Creates a count projected query from the given original query.
185+
*
186+
* @param originalQuery must not be {@literal null}.
187+
* @param countProjection may be {@literal null}.
188+
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
189+
* @since 1.6
190+
* @deprecated in favor of {@link QueryUtils#createCountQueryFor(DeclaredQuery, String)}
191+
*/
192+
@Deprecated
193+
public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) {
194+
return DefaultQueryUtils.createCountQueryFor(originalQuery, countProjection);
195+
}
196+
197+
/**
198+
* Creates a count projected query from the given original query.
199+
*
200+
* @param originalQuery must not be {@literal null} or empty.
201+
* @return Guaranteed to be not {@literal null}.
202+
*/
203+
public static String createCountQueryFor(DeclaredQuery originalQuery) {
204+
return createCountQueryFor(originalQuery, null);
205+
}
206+
207+
/**
208+
* Creates a count projected query from the given original query.
209+
*
210+
* @param originalQuery must not be {@literal null}.
211+
* @param countProjection may be {@literal null}.
212+
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
213+
*/
214+
public static String createCountQueryFor(DeclaredQuery originalQuery, @Nullable String countProjection) {
215+
if (qualifiesForJSqlParserUsage(originalQuery)) {
216+
return JSqlParserQueryUtils.createCountQueryFor(originalQuery.getQueryString(), countProjection);
217+
} else {
218+
return DefaultQueryUtils.createCountQueryFor(originalQuery.getQueryString(), countProjection);
219+
}
220+
}
221+
222+
/**
223+
* Returns whether the given {@link Query} contains named parameters.
224+
*
225+
* @param query Must not be {@literal null}.
226+
* @return whether the given {@link Query} contains named parameters.
227+
*/
228+
public static boolean hasNamedParameter(Query query) {
229+
return DefaultQueryUtils.hasNamedParameter(query);
230+
}
231+
232+
/**
233+
* Turns the given {@link Sort} into {@link javax.persistence.criteria.Order}s.
234+
*
235+
* @param sort the {@link Sort} instance to be transformed into JPA {@link javax.persistence.criteria.Order}s.
236+
* @param from must not be {@literal null}.
237+
* @param cb must not be {@literal null}.
238+
* @return a {@link List} of {@link javax.persistence.criteria.Order}s.
239+
*/
240+
public static List<javax.persistence.criteria.Order> toOrders(Sort sort, From<?, ?> from, CriteriaBuilder cb) {
241+
return DefaultQueryUtils.toOrders(sort, from, cb);
242+
}
243+
244+
/**
245+
* Returns whether the given JPQL query contains a constructor expression.
246+
*
247+
* @param query must not be {@literal null} or empty.
248+
* @return whether the given JPQL query contains a constructor expression.
249+
*/
250+
public static boolean hasConstructorExpression(String query) {
251+
return DefaultQueryUtils.hasConstructorExpression(query);
252+
}
253+
254+
/**
255+
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
256+
*
257+
* @param query must not be {@literal null} or empty.
258+
* @return the projection part of the query.
259+
* @deprecated in favor of {@link QueryUtils#getProjection(DeclaredQuery)}
260+
*/
261+
@Deprecated
262+
public static String getProjection(String query) {
263+
return DefaultQueryUtils.getProjection(query);
264+
}
265+
266+
/**
267+
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
268+
*
269+
* @param query must not be {@literal null}.
270+
* @return the projection part of the query.
271+
*/
272+
public static String getProjection(DeclaredQuery query) {
273+
if (qualifiesForJSqlParserUsage(query)) {
274+
return JSqlParserQueryUtils.getProjection(query.getQueryString());
275+
} else {
276+
return DefaultQueryUtils.getProjection(query.getQueryString());
277+
}
278+
}
279+
280+
/**
281+
* Checks if a given query can be process with the JSqlParser under the condition that the parser is in the classpath.
282+
*
283+
* @param query the query we want to check
284+
* @return <code>true</code> if JSqlParser is in the classpath and the query is classified as a native query otherwise
285+
* <code>false</code>
286+
*/
287+
private static boolean qualifiesForJSqlParserUsage(DeclaredQuery query) {
288+
return JSQLPARSER_IN_CLASSPATH && query.isNativeQuery();
289+
}
290+
291+
/**
292+
* Checks whether JSqlParser is in classpath or not.
293+
*
294+
* @return <code>true</code> when in classpath otherwise <code>false</code>
295+
*/
296+
private static boolean isJSqlParserInClassPath() {
297+
try {
298+
Class.forName("net.sf.jsqlparser.parser.JSqlParser", false, QueryUtils.class.getClassLoader());
299+
LOG.info("JSqlParser is in classpath. If applicable JSqlParser will be used.");
300+
return true;
301+
} catch (ClassNotFoundException e) {
302+
return false;
303+
}
304+
}
305+
306+
}

0 commit comments

Comments
 (0)