Skip to content

Commit 7045f4c

Browse files
committed
Remove JSqlParserQueryUtils in favour of JSqlParserQueryEnhancer.
Since all the methods within `JSqlParserQueryUtils` were only used by `JSqlParserQueryEnhancer` it makes more sense to just implement it there. Additionally we introduced the `JSqlParserUtils` class which is there for operations that a commonly used when working with JSqlParser (for example generating a count function object). Related tickets spring-projects#2409
1 parent 2096671 commit 7045f4c

File tree

7 files changed

+498
-1006
lines changed

7 files changed

+498
-1006
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import org.springframework.data.domain.Sort;
1919

20+
import java.util.Set;
21+
2022
/**
2123
* The implementation of {@link QueryEnhancer} using {@link QueryUtils}.
2224
*
@@ -60,6 +62,11 @@ public String getProjection() {
6062
return QueryUtils.getProjection(this.query.getQueryString());
6163
}
6264

65+
@Override
66+
public Set<String> getJoinAliases() {
67+
return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
68+
}
69+
6370
@Override
6471
public DeclaredQuery getQuery() {
6572
return this.query;

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

+273-7
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,38 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import net.sf.jsqlparser.JSQLParserException;
19+
import net.sf.jsqlparser.expression.Alias;
20+
import net.sf.jsqlparser.expression.Expression;
21+
import net.sf.jsqlparser.expression.Function;
22+
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
23+
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
24+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
25+
import net.sf.jsqlparser.schema.Column;
26+
import net.sf.jsqlparser.schema.Table;
27+
import net.sf.jsqlparser.statement.select.*;
28+
import net.sf.jsqlparser.util.SelectUtils;
1829
import org.springframework.data.domain.Sort;
30+
import org.springframework.data.util.Streamable;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.CollectionUtils;
33+
import org.springframework.util.StringUtils;
34+
35+
import java.util.*;
36+
import java.util.stream.Collectors;
37+
38+
import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*;
39+
import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression;
1940

2041
/**
21-
* The implementation of {@link QueryEnhancer} using {@link JSqlParserQueryUtils}.
42+
* The implementation of {@link QueryEnhancer} using JSqlParser.
2243
*
2344
* @author Diego Krupitza
2445
*/
2546
public class JSqlParserQueryEnhancer implements QueryEnhancer {
2647

48+
private static final String DEFAULT_TABLE_ALIAS = "x";
49+
2750
private final DeclaredQuery query;
2851

2952
/**
@@ -35,32 +58,275 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) {
3558

3659
@Override
3760
public String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable<String> idAttributes) {
38-
return JSqlParserQueryUtils.getExistsQueryString(entityName, countQueryPlaceHolder, idAttributes);
61+
final Table tableNameWithAlias = getTableWithAlias(entityName, DEFAULT_TABLE_ALIAS);
62+
Function jSqlCount = getJSqlCount(Collections.singletonList(countQueryPlaceHolder), false);
63+
64+
Select select = SelectUtils.buildSelectFromTableAndSelectItems(tableNameWithAlias,
65+
new SelectExpressionItem(jSqlCount));
66+
67+
PlainSelect selectBody = (PlainSelect) select.getSelectBody();
68+
69+
List<Expression> equalityExpressions = Streamable.of(idAttributes).stream() //
70+
.map(field -> {
71+
Expression tableNameField = new Column().withTable(tableNameWithAlias).withColumnName(field);
72+
Expression inputField = new Column(":".concat(field));
73+
return new EqualsTo(tableNameField, inputField);
74+
}).collect(Collectors.toList());
75+
76+
if (equalityExpressions.size() > 1) {
77+
AndExpression rootOfWhereClause = concatenateWithAndExpression(equalityExpressions);
78+
selectBody.setWhere(rootOfWhereClause);
79+
} else if (equalityExpressions.size() == 1) {
80+
selectBody.setWhere(equalityExpressions.get(0));
81+
}
82+
83+
return selectBody.toString();
3984
}
4085

4186
@Override
4287
public String getQueryString(String template, String entityName) {
43-
return JSqlParserQueryUtils.getQueryString(template, entityName);
88+
Assert.hasText(entityName, "Entity name must not be null or empty!");
89+
return String.format(template, entityName);
4490
}
4591

4692
@Override
4793
public String applySorting(Sort sort, String alias) {
48-
return JSqlParserQueryUtils.applySorting(this.query.getQueryString(), sort, alias);
94+
String queryString = query.getQueryString();
95+
Assert.hasText(queryString, "Query must not be null or empty!");
96+
97+
if (sort.isUnsorted()) {
98+
return queryString;
99+
}
100+
101+
Select selectStatement = parseSelectStatement(queryString);
102+
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
103+
104+
final Set<String> joinAliases = getJoinAliases(selectBody);
105+
106+
final Set<String> selectionAliases = getSelectionAliases(selectBody);
107+
108+
List<OrderByElement> orderByElements = sort.stream() //
109+
.map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) //
110+
.collect(Collectors.toList());
111+
112+
if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) {
113+
selectBody.setOrderByElements(new ArrayList<>());
114+
}
115+
116+
selectBody.getOrderByElements().addAll(orderByElements);
117+
118+
return selectBody.toString();
119+
120+
}
121+
122+
/**
123+
* Returns the aliases used inside the selection part in the query.
124+
*
125+
* @param selectBody a {@link PlainSelect} containing a query. Must not be {@literal null}.
126+
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
127+
*/
128+
private Set<String> getSelectionAliases(PlainSelect selectBody) {
129+
130+
if (CollectionUtils.isEmpty(selectBody.getSelectItems())) {
131+
return new HashSet<>();
132+
}
133+
134+
return selectBody.getSelectItems().stream() //
135+
.filter(SelectExpressionItem.class::isInstance) //
136+
.map(item -> ((SelectExpressionItem) item).getAlias()) //
137+
.filter(Objects::nonNull) //
138+
.map(Alias::getName) //
139+
.collect(Collectors.toSet());
140+
}
141+
142+
/**
143+
* Returns the aliases used for {@code join}s.
144+
*
145+
* @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
146+
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
147+
*/
148+
private Set<String> getJoinAliases(String query) {
149+
return getJoinAliases((PlainSelect) parseSelectStatement(query).getSelectBody());
150+
}
151+
152+
/**
153+
* Returns the aliases used for {@code join}s.
154+
*
155+
* @param selectBody the selection body to extract the aliases of joins from. Must not be {@literal null}.
156+
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
157+
*/
158+
private Set<String> getJoinAliases(PlainSelect selectBody) {
159+
160+
if (CollectionUtils.isEmpty(selectBody.getJoins())) {
161+
return new HashSet<>();
162+
}
163+
164+
return selectBody.getJoins().stream() //
165+
.map(join -> join.getRightItem().getAlias()) //
166+
.filter(Objects::nonNull) //
167+
.map(Alias::getName) //
168+
.collect(Collectors.toSet());
169+
}
170+
171+
/**
172+
* Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the
173+
* referenced property refers to a join alias, i.e. starts with {@code $alias.}.
174+
*
175+
* @param joinAliases the join aliases of the original query. Must not be {@literal null}.
176+
* @param alias the alias for the root entity. May be {@literal null}.
177+
* @param order the order object to build the clause for. Must not be {@literal null}.
178+
* @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}.
179+
*/
180+
private OrderByElement getOrderClause(final Set<String> joinAliases, final Set<String> selectionAliases,
181+
final String alias, final Sort.Order order) {
182+
183+
final OrderByElement orderByElement = new OrderByElement();
184+
orderByElement.setAsc(order.getDirection().isAscending());
185+
orderByElement.setAscDescPresent(true);
186+
187+
final String property = order.getProperty();
188+
189+
checkSortExpression(order);
190+
191+
if (selectionAliases.contains(property)) {
192+
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property);
193+
194+
orderByElement.setExpression(orderExpression);
195+
return orderByElement;
196+
}
197+
198+
boolean qualifyReference = joinAliases //
199+
.parallelStream() //
200+
.map(joinAlias -> joinAlias.concat(".")) //
201+
.noneMatch(property::startsWith);
202+
203+
boolean functionIndicator = property.contains("(");
204+
205+
String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias)
206+
? String.format("%s.%s", alias, property)
207+
: property;
208+
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference);
209+
orderByElement.setExpression(orderExpression);
210+
return orderByElement;
49211
}
50212

51213
@Override
52214
public String detectAlias() {
53-
return JSqlParserQueryUtils.detectAlias(this.query.getQueryString());
215+
return detectAlias(this.query.getQueryString());
216+
}
217+
218+
/**
219+
* Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
220+
* strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
221+
*
222+
* @param query must not be {@literal null}.
223+
* @return Might return {@literal null}.
224+
*/
225+
private String detectAlias(String query) {
226+
Select selectStatement = parseSelectStatement(query);
227+
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
228+
return detectAlias(selectBody);
229+
}
230+
231+
/**
232+
* Resolves the alias for the entity to be retrieved from the given {@link PlainSelect}. Note that you only provide
233+
* valid Query strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
234+
*
235+
* @param selectBody must not be {@literal null}.
236+
* @return Might return {@literal null}.
237+
*/
238+
private static String detectAlias(PlainSelect selectBody) {
239+
Alias alias = selectBody.getFromItem().getAlias();
240+
return alias == null ? null : alias.getName();
54241
}
55242

56243
@Override
57244
public String createCountQueryFor(String countProjection) {
58-
return JSqlParserQueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection);
245+
246+
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty!");
247+
248+
Select selectStatement = parseSelectStatement(this.query.getQueryString());
249+
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
250+
251+
// remove order by
252+
selectBody.setOrderByElements(null);
253+
254+
if (StringUtils.hasText(countProjection)) {
255+
Function jSqlCount = getJSqlCount(Collections.singletonList(countProjection), false);
256+
selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
257+
return selectBody.toString();
258+
}
259+
260+
boolean distinct = selectBody.getDistinct() != null;
261+
selectBody.setDistinct(null); // reset possible distinct
262+
263+
String tableAlias = detectAlias(selectBody);
264+
265+
// is never null
266+
List<SelectItem> selectItems = selectBody.getSelectItems();
267+
268+
if (onlyASingleColumnProjection(selectItems)) {
269+
SelectExpressionItem singleProjection = (SelectExpressionItem) selectItems.get(0);
270+
271+
Column column = (Column) singleProjection.getExpression();
272+
String countProp = column.getFullyQualifiedName();
273+
274+
Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
275+
selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
276+
return selectBody.toString();
277+
}
278+
279+
String countProp = tableAlias == null ? "*" : tableAlias;
280+
281+
Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct);
282+
selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount)));
283+
284+
return selectBody.toString();
285+
59286
}
60287

61288
@Override
62289
public String getProjection() {
63-
return JSqlParserQueryUtils.getProjection(this.query.getQueryString());
290+
Assert.hasText(query.getQueryString(), "Query must not be null or empty!");
291+
292+
Select selectStatement = parseSelectStatement(query.getQueryString());
293+
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
294+
295+
return selectBody.getSelectItems() //
296+
.stream() //
297+
.map(Object::toString) //
298+
.collect(Collectors.joining(", ")).trim();
299+
}
300+
301+
@Override
302+
public Set<String> getJoinAliases() {
303+
return this.getJoinAliases(this.query.getQueryString());
304+
}
305+
306+
/**
307+
* Parses a query string with JSqlParser.
308+
*
309+
* @param query the query to parse
310+
* @return the parsed query
311+
*/
312+
private static Select parseSelectStatement(String query) {
313+
try {
314+
return (Select) CCJSqlParserUtil.parse(query);
315+
} catch (JSQLParserException e) {
316+
throw new IllegalArgumentException("The query you provided is not a valid SQL Query!", e);
317+
}
318+
}
319+
320+
/**
321+
* Checks whether a given projection only contains a single column definition (aka without functions, etc)
322+
*
323+
* @param projection the projection to analyse
324+
* @return <code>true</code> when the projection only contains a single column definition otherwise <code>false</code>
325+
*/
326+
private boolean onlyASingleColumnProjection(List<SelectItem> projection) {
327+
// this is unfortunately the only way to check without any hacky & hard string regex magic
328+
return projection.size() == 1 && projection.get(0) instanceof SelectExpressionItem
329+
&& (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column;
64330
}
65331

66332
@Override

0 commit comments

Comments
 (0)