Skip to content

Commit 3e1c8a2

Browse files
mp911dechristophstrobl
authored andcommitted
Refactor EQL, HQL & JPQL query rendering.
This commit turns existing query transformers into introspectors using eager parsing and detection of query parts. Query transformation has also been split up into dedicated Count and Sort Query parts. To reduce duplicate code across the existing parses we introduced a reusable functional configuration. For better testing support a query assertion has been introduced to query parser tests and we adjusted invalid formatting. See: #3309 Closes: #3326
1 parent f607dd2 commit 3e1c8a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5700
-5537
lines changed

spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java

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

1818
import java.util.List;
1919

20+
import org.springframework.data.domain.Sort;
2021
import org.springframework.data.jpa.model.IPersonProjection;
2122
import org.springframework.data.jpa.model.Person;
2223
import org.springframework.data.repository.ListCrudRepository;
@@ -33,6 +34,9 @@ public interface PersonRepository extends ListCrudRepository<Person, Integer> {
3334
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
3435
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname);
3536

37+
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
38+
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort);
39+
3640
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
3741
List<Person> findAllWithNativeQueryByFirstname(String firstname);
3842
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2024 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;
17+
18+
import org.springframework.data.domain.Sort;
19+
20+
/**
21+
* @author Mark Paluch
22+
*/
23+
public class Profiler {
24+
25+
public static void main(String[] args) throws InterruptedException {
26+
27+
RepositoryFinderTests tests = new RepositoryFinderTests();
28+
RepositoryFinderTests.BenchmarkParameters params = new RepositoryFinderTests.BenchmarkParameters();
29+
params.doSetup();
30+
31+
System.out.println("Ready. Waiting 10sec");
32+
Thread.sleep(10000);
33+
34+
System.out.println("Go!");
35+
36+
while (true) {
37+
params.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
38+
}
39+
40+
}
41+
}

spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/RepositoryFinderTests.java

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import org.openjdk.jmh.annotations.TearDown;
4141
import org.openjdk.jmh.annotations.Timeout;
4242
import org.openjdk.jmh.annotations.Warmup;
43+
44+
import org.springframework.data.domain.Sort;
4345
import org.springframework.data.jpa.model.IPersonProjection;
4446
import org.springframework.data.jpa.model.Person;
4547
import org.springframework.data.jpa.model.Profile;
@@ -151,11 +153,17 @@ public List<IPersonProjection> derivedFinderMethodWithInterfaceProjection(Benchm
151153
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname("first");
152154
}
153155

156+
154157
@Benchmark
155158
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
156159
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first");
157160
}
158161

162+
@Benchmark
163+
public List<Person> stringBasedQueryDynamicSort(BenchmarkParameters parameters) {
164+
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
165+
}
166+
159167
@Benchmark
160168
public List<Person> stringBasedNativeQuery(BenchmarkParameters parameters) {
161169
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname("first");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2024 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 jmh.mbr.junit5.Microbenchmark;
19+
20+
import org.openjdk.jmh.annotations.Benchmark;
21+
import org.openjdk.jmh.annotations.Fork;
22+
import org.openjdk.jmh.annotations.Level;
23+
import org.openjdk.jmh.annotations.Measurement;
24+
import org.openjdk.jmh.annotations.Scope;
25+
import org.openjdk.jmh.annotations.Setup;
26+
import org.openjdk.jmh.annotations.State;
27+
import org.openjdk.jmh.annotations.Timeout;
28+
import org.openjdk.jmh.annotations.Warmup;
29+
30+
import org.springframework.data.domain.Sort;
31+
32+
/**
33+
* @author Mark Paluch
34+
*/
35+
@Microbenchmark
36+
@Fork(1)
37+
@Warmup(time = 2, iterations = 3)
38+
@Measurement(time = 2)
39+
@Timeout(time = 2)
40+
public class HqlParserTests {
41+
42+
@State(Scope.Benchmark)
43+
public static class BenchmarkParameters {
44+
45+
DeclaredQuery query;
46+
Sort sort = Sort.by("foo");
47+
QueryEnhancer enhancer;
48+
49+
@Setup(Level.Iteration)
50+
public void doSetup() {
51+
52+
String s = """
53+
SELECT e FROM Employee e JOIN e.projects p
54+
WHERE TREAT(p AS LargeProject).budget > 1000
55+
OR TREAT(p AS SmallProject).name LIKE 'Persist%'
56+
OR p.description LIKE "cost overrun"
57+
""";
58+
59+
query = DeclaredQuery.of(s, false);
60+
enhancer = QueryEnhancerFactory.forQuery(query);
61+
}
62+
}
63+
64+
@Benchmark
65+
public Object measure(BenchmarkParameters parameters) {
66+
return parameters.enhancer.applySorting(parameters.sort);
67+
}
68+
69+
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ static DeclaredQuery of(@Nullable String query, boolean nativeQuery) {
4040
return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery);
4141
}
4242

43+
static boolean hasNamedParameter(String query) {
44+
45+
if (ObjectUtils.isEmpty(query)) {
46+
return false;
47+
}
48+
49+
return StringQuery.hasNamedParameter(query);
50+
}
51+
4352
/**
4453
* @return whether the underlying query has at least one named parameter.
4554
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2023-2024 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 static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
19+
20+
import java.util.List;
21+
22+
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query into a
27+
* {@code COUNT(…)} query.
28+
*
29+
* @author Greg Turnquist
30+
* @author Mark Paluch
31+
* @since 3.4
32+
*/
33+
@SuppressWarnings("ConstantValue")
34+
class EqlCountQueryTransformer extends EqlQueryRenderer {
35+
36+
private final @Nullable String countProjection;
37+
private final @Nullable String primaryFromAlias;
38+
39+
EqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
40+
this.countProjection = countProjection;
41+
this.primaryFromAlias = primaryFromAlias;
42+
}
43+
44+
@Override
45+
public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementContext ctx) {
46+
47+
QueryRendererBuilder builder = QueryRenderer.builder();
48+
49+
builder.appendExpression(visit(ctx.select_clause()));
50+
builder.appendExpression(visit(ctx.from_clause()));
51+
52+
if (ctx.where_clause() != null) {
53+
builder.appendExpression(visit(ctx.where_clause()));
54+
}
55+
if (ctx.groupby_clause() != null) {
56+
builder.appendExpression(visit(ctx.groupby_clause()));
57+
}
58+
if (ctx.having_clause() != null) {
59+
builder.appendExpression(visit(ctx.having_clause()));
60+
}
61+
62+
return builder;
63+
}
64+
65+
@Override
66+
public QueryRendererBuilder visitSelect_clause(EqlParser.Select_clauseContext ctx) {
67+
68+
QueryRendererBuilder builder = QueryRenderer.builder();
69+
70+
builder.append(JpaQueryParsingToken.expression(ctx.SELECT()));
71+
builder.append(TOKEN_COUNT_FUNC);
72+
73+
if (countProjection != null) {
74+
builder.append(JpaQueryParsingToken.token(countProjection));
75+
}
76+
77+
QueryRendererBuilder nested = QueryRenderer.builder();
78+
79+
if (ctx.DISTINCT() != null) {
80+
nested.append(JpaQueryParsingToken.expression(ctx.DISTINCT()));
81+
}
82+
83+
if (countProjection == null) {
84+
85+
if (ctx.DISTINCT() != null) {
86+
87+
QueryRendererBuilder selectionListbuilder = QueryRendererBuilder.concat(ctx.select_item(), this::visit,
88+
TOKEN_COMMA);
89+
90+
List<JpaQueryParsingToken> countSelection = QueryTransformers
91+
.filterCountSelection(selectionListbuilder.build().stream().toList());
92+
93+
if (countSelection.stream().anyMatch(eqlToken -> eqlToken.getToken().contains("new"))) {
94+
// constructor
95+
nested.append(new JpaQueryParsingToken(primaryFromAlias));
96+
} else {
97+
// keep all the select items to distinct against
98+
nested.append(countSelection);
99+
}
100+
} else {
101+
nested.append(new JpaQueryParsingToken(primaryFromAlias));
102+
}
103+
}
104+
105+
builder.appendInline(nested);
106+
builder.append(TOKEN_CLOSE_PAREN);
107+
108+
return builder;
109+
}
110+
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2024 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 static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
19+
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* {@link ParsedQueryIntrospector} for EQL queries.
28+
*
29+
* @author Mark Paluch
30+
*/
31+
@SuppressWarnings("UnreachableCode")
32+
class EqlQueryIntrospector extends EqlBaseVisitor<Void> implements ParsedQueryIntrospector {
33+
34+
private final EqlQueryRenderer renderer = new EqlQueryRenderer();
35+
36+
private @Nullable String primaryFromAlias = null;
37+
private @Nullable List<JpaQueryParsingToken> projection;
38+
private boolean projectionProcessed;
39+
private boolean hasConstructorExpression = false;
40+
41+
@Override
42+
public String getAlias() {
43+
return primaryFromAlias;
44+
}
45+
46+
@Override
47+
public List<JpaQueryParsingToken> getProjection() {
48+
return projection == null ? Collections.emptyList() : projection;
49+
}
50+
51+
@Override
52+
public boolean hasConstructorExpression() {
53+
return hasConstructorExpression;
54+
}
55+
56+
@Override
57+
public Void visitSelect_clause(EqlParser.Select_clauseContext ctx) {
58+
59+
List<EqlParser.Select_itemContext> selections = ctx.select_item();
60+
List<JpaQueryParsingToken> selectItemTokens = new ArrayList<>(selections.size() * 2);
61+
62+
for (EqlParser.Select_itemContext selection : selections) {
63+
64+
if (!selectItemTokens.isEmpty()) {
65+
selectItemTokens.add(TOKEN_COMMA);
66+
}
67+
68+
selectItemTokens.add(JpaQueryParsingToken.token(renderer.visitSelect_item(selection).build().render()));
69+
}
70+
71+
if (!projectionProcessed) {
72+
projection = selectItemTokens;
73+
projectionProcessed = true;
74+
}
75+
76+
return super.visitSelect_clause(ctx);
77+
}
78+
79+
@Override
80+
public Void visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) {
81+
82+
if (primaryFromAlias == null) {
83+
primaryFromAlias = ctx.identification_variable() != null ? ctx.identification_variable().getText()
84+
: ctx.entity_name().getText();
85+
}
86+
87+
return super.visitRange_variable_declaration(ctx);
88+
}
89+
90+
@Override
91+
public Void visitConstructor_expression(EqlParser.Constructor_expressionContext ctx) {
92+
93+
hasConstructorExpression = true;
94+
95+
return super.visitConstructor_expression(ctx);
96+
}
97+
98+
}

0 commit comments

Comments
 (0)