Skip to content

Commit 1dfe3fa

Browse files
DiegoKrupitzagregturn
authored andcommitted
Make JSqlParserQueryEnhancer work with updating statements.
This implementation of QueryEnhancer was originally designed for SELECT statements. This commit now handles DELETE and UPDATE operations by side-stepping any sorting or other changes. Keep in mind that "enhancing" non selects does not have any effect on them (and the current default implementation `QueryUtils` does not care either aka it often just returns the same query, null or empty string). Closes #2555
1 parent be2fbaa commit 1dfe3fa

File tree

4 files changed

+101
-1
lines changed

4 files changed

+101
-1
lines changed

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

+64-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
import net.sf.jsqlparser.expression.Function;
2525
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
2626
import net.sf.jsqlparser.schema.Column;
27+
import net.sf.jsqlparser.statement.Statement;
28+
import net.sf.jsqlparser.statement.delete.Delete;
2729
import net.sf.jsqlparser.statement.select.OrderByElement;
2830
import net.sf.jsqlparser.statement.select.PlainSelect;
2931
import net.sf.jsqlparser.statement.select.Select;
3032
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
3133
import net.sf.jsqlparser.statement.select.SelectItem;
34+
import net.sf.jsqlparser.statement.update.Update;
3235

3336
import java.util.ArrayList;
3437
import java.util.Collections;
@@ -54,20 +57,49 @@
5457
public class JSqlParserQueryEnhancer implements QueryEnhancer {
5558

5659
private final DeclaredQuery query;
60+
private final ParsedType parsedType;
5761

5862
/**
5963
* @param query the query we want to enhance. Must not be {@literal null}.
6064
*/
6165
public JSqlParserQueryEnhancer(DeclaredQuery query) {
6266
this.query = query;
67+
this.parsedType = detectParsedType();
68+
}
69+
70+
/**
71+
* Detects what type of query is provided.
72+
*
73+
* @return the parsed type
74+
*/
75+
private ParsedType detectParsedType() {
76+
try {
77+
Statement statement = CCJSqlParserUtil.parse(this.query.getQueryString());
78+
79+
if (statement instanceof Update) {
80+
return ParsedType.UPDATE;
81+
} else if (statement instanceof Delete) {
82+
return ParsedType.DELETE;
83+
} else if (statement instanceof Select) {
84+
return ParsedType.SELECT;
85+
} else {
86+
return ParsedType.SELECT;
87+
}
88+
89+
} catch (JSQLParserException e) {
90+
throw new IllegalArgumentException("The query you provided is not a valid SQL Query!", e);
91+
}
6392
}
6493

6594
@Override
6695
public String applySorting(Sort sort, @Nullable String alias) {
67-
6896
String queryString = query.getQueryString();
6997
Assert.hasText(queryString, "Query must not be null or empty!");
7098

99+
if (this.parsedType != ParsedType.SELECT) {
100+
return queryString;
101+
}
102+
71103
if (sort.isUnsorted()) {
72104
return queryString;
73105
}
@@ -120,6 +152,10 @@ private Set<String> getSelectionAliases(PlainSelect selectBody) {
120152
*/
121153
Set<String> getSelectionAliases() {
122154

155+
if (this.parsedType != ParsedType.SELECT) {
156+
return new HashSet<>();
157+
}
158+
123159
Select selectStatement = parseSelectStatement(this.query.getQueryString());
124160
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
125161
return this.getSelectionAliases(selectBody);
@@ -132,6 +168,9 @@ Set<String> getSelectionAliases() {
132168
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
133169
*/
134170
private Set<String> getJoinAliases(String query) {
171+
if (this.parsedType != ParsedType.SELECT) {
172+
return new HashSet<>();
173+
}
135174
return getJoinAliases((PlainSelect) parseSelectStatement(query).getSelectBody());
136175
}
137176

@@ -211,6 +250,10 @@ public String detectAlias() {
211250
@Nullable
212251
private String detectAlias(String query) {
213252

253+
if (this.parsedType != ParsedType.SELECT) {
254+
return null;
255+
}
256+
214257
Select selectStatement = parseSelectStatement(query);
215258
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
216259
return detectAlias(selectBody);
@@ -233,6 +276,10 @@ private static String detectAlias(PlainSelect selectBody) {
233276
@Override
234277
public String createCountQueryFor(@Nullable String countProjection) {
235278

279+
if (this.parsedType != ParsedType.SELECT) {
280+
return this.query.getQueryString();
281+
}
282+
236283
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty!");
237284

238285
Select selectStatement = parseSelectStatement(this.query.getQueryString());
@@ -278,6 +325,10 @@ public String createCountQueryFor(@Nullable String countProjection) {
278325
@Override
279326
public String getProjection() {
280327

328+
if (this.parsedType != ParsedType.SELECT) {
329+
return "";
330+
}
331+
281332
Assert.hasText(query.getQueryString(), "Query must not be null or empty!");
282333

283334
Select selectStatement = parseSelectStatement(query.getQueryString());
@@ -327,3 +378,15 @@ public DeclaredQuery getQuery() {
327378
return this.query;
328379
}
329380
}
381+
382+
/**
383+
* An enum to represent the top level parsed statement of the provided query.
384+
* <ul>
385+
* <li>{@code ParsedType.DELETE}: means the top level statement is {@link Delete}</li>
386+
* <li>{@code ParsedType.UPDATE}: means the top level statement is {@link Update}</li>
387+
* <li>{@code ParsedType.SELECT}: means the top level statement is {@link Select}</li>
388+
* </ul>
389+
*/
390+
enum ParsedType {
391+
DELETE, UPDATE, SELECT;
392+
}

src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

+13
Original file line numberDiff line numberDiff line change
@@ -2678,6 +2678,19 @@ void existsWithSpec() {
26782678
assertThat(repository.exists(hundredYearsOld)).isTrue();
26792679
}
26802680

2681+
@Test // GH-2555
2682+
void modifyingUpdateNativeQueryWorksWithJSQLParser() {
2683+
flushTestUsers();
2684+
2685+
Optional<User> byIdUser = repository.findById(firstUser.getId());
2686+
assertThat(byIdUser).isPresent().map(User::isActive).get().isEqualTo(true);
2687+
2688+
repository.setActiveToFalseWithModifyingNative(byIdUser.get().getId());
2689+
2690+
Optional<User> afterUpdate = repository.findById(firstUser.getId());
2691+
assertThat(afterUpdate).isPresent().map(User::isActive).get().isEqualTo(false);
2692+
}
2693+
26812694
@Test // GH-2045, GH-425
26822695
public void correctlyBuildSortClauseWhenSortingByFunctionAliasAndFunctionContainsPositionalParameters() {
26832696
repository.findAllAndSortByFunctionResultPositionalParameter("prefix", "suffix", Sort.by("idWithPrefixAndSuffix"));

src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,25 @@ void countQueryUsesCorrectVariable() {
731731
assertThat(countQueryFor).isEqualTo("SELECT count(test) FROM (SELECT * FROM test) AS test");
732732
}
733733

734+
@Test // GH-2555
735+
void modifyingQueriesAreDetectedCorrectly() {
736+
String modifyingQuery = "update userinfo user set user.is_in_treatment = false where user.id = :userId";
737+
738+
String aliasNotConsideringQueryType = QueryUtils.detectAlias(modifyingQuery);
739+
String projectionNotConsideringQueryType = QueryUtils.getProjection(modifyingQuery);
740+
boolean constructorExpressionNotConsideringQueryType = QueryUtils.hasConstructorExpression(modifyingQuery);
741+
String countQueryForNotConsiderQueryType = QueryUtils.createCountQueryFor(modifyingQuery);
742+
743+
StringQuery modiQuery = new StringQuery(modifyingQuery, true);
744+
745+
assertThat(modiQuery.getAlias()).isEqualToIgnoringCase(aliasNotConsideringQueryType);
746+
assertThat(modiQuery.getProjection()).isEqualToIgnoringCase(projectionNotConsideringQueryType);
747+
assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType);
748+
749+
assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery);
750+
assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery);
751+
}
752+
734753
public static Stream<Arguments> detectsJoinAliasesCorrectlySource() {
735754

736755
return Stream.of( //

src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java

+5
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,11 @@ List<String> findAllAndSortByFunctionResultPositionalParameter(
637637
List<String> findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter1") String namedParameter1,
638638
@Param("namedParameter2") String namedParameter2, Sort sort);
639639

640+
// GH-2555
641+
@Modifying(clearAutomatically = true)
642+
@Query(value = "update SD_User u set u.active = false where u.id = :userId", nativeQuery = true)
643+
void setActiveToFalseWithModifyingNative(@Param("userId") int userId);
644+
640645
interface RolesAndFirstname {
641646

642647
String getFirstname();

0 commit comments

Comments
 (0)