Refactor native query handling and JsqlParser usage.

This commit introduce eager alias and projection detection and aims to cache expensive calls.
It also revises JPQL parsers and enhancers into single-class hierarchy and removes strange parameter verification (as it was wrong anyway).

See: #3309
Closes: #3311
This commit is contained in:
Mark Paluch
2024-05-31 13:41:12 +02:00
committed by Christoph Strobl
parent 3e1c8a28a9
commit 2dc94dde8c
29 changed files with 734 additions and 819 deletions

View File

@@ -85,6 +85,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -27,16 +27,16 @@ import org.springframework.data.repository.ListCrudRepository;
*/
public interface PersonRepository extends ListCrudRepository<Person, Integer> {
List<Person> findAllByFirstname(String firstname);
List<Person> findAllByFirstname(String firstname);
List<IPersonProjection> findAllAndProjectToInterfaceByFirstname(String firstname);
List<IPersonProjection> findAllAndProjectToInterfaceByFirstname(String firstname);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort);
@Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
List<Person> findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort);
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List<Person> findAllWithNativeQueryByFirstname(String firstname);
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List<Person> findAllWithNativeQueryByFirstname(String firstname);
}

View File

@@ -15,26 +15,37 @@
*/
package org.springframework.data.jpa.repository;
import java.lang.reflect.Method;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.StringQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* @author Mark Paluch
*/
public class Profiler {
public static void main(String[] args) throws InterruptedException {
public static void main(String[] args) throws Exception {
RepositoryFinderTests tests = new RepositoryFinderTests();
RepositoryFinderTests.BenchmarkParameters params = new RepositoryFinderTests.BenchmarkParameters();
params.doSetup();
DefaultRepositoryMetadata art = new DefaultRepositoryMetadata(PersonRepository.class);
Method method = PersonRepository.class.getMethod("findAllWithAnnotatedQueryByFirstname", String.class, Sort.class);
ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
System.out.println("Ready. Waiting 10sec");
Thread.sleep(10000);
System.out.println("Go!");
while (true) {
params.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
JpaQueryMethod queryMethod = new JpaQueryMethod(method, art, projectionFactory, PersistenceProvider.HIBERNATE);
StringQuery stringQuery = new StringQuery(queryMethod.getRequiredAnnotatedQuery(), false);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import jmh.mbr.junit5.Microbenchmark;
import java.io.IOException;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
/**
* @author Mark Paluch
*/
@Microbenchmark
@Fork(1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2)
@Timeout(time = 2)
public class JSqlParserQueryEnhancerTests {
@State(Scope.Benchmark)
public static class BenchmarkParameters {
JSqlParserQueryEnhancer enhancer;
Sort sort = Sort.by("foo");
private byte[] serialized;
@Setup(Level.Iteration)
public void doSetup() throws IOException {
String s = """
select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE
except
select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE
union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE""";
enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true));
}
}
@Benchmark
public Object applySortWithParsing(BenchmarkParameters p) {
return p.enhancer.applySorting(p.sort);
}
}

View File

@@ -40,15 +40,6 @@ interface DeclaredQuery {
return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery);
}
static boolean hasNamedParameter(String query) {
if (ObjectUtils.isEmpty(query)) {
return false;
}
return StringQuery.hasNamedParameter(query);
}
/**
* @return whether the underlying query has at least one named parameter.
*/

View File

@@ -29,9 +29,22 @@ import org.springframework.lang.Nullable;
public class DefaultQueryEnhancer implements QueryEnhancer {
private final DeclaredQuery query;
private final boolean hasConstructorExpression;
private final String alias;
private final String projection;
private final Set<String> joinAliases;
public DefaultQueryEnhancer(DeclaredQuery query) {
this.query = query;
this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString());
this.alias = QueryUtils.detectAlias(query.getQueryString());
this.projection = QueryUtils.getProjection(this.query.getQueryString());
this.joinAliases = QueryUtils.getOuterJoinAliases(this.query.getQueryString());
}
@Override
public String applySorting(Sort sort) {
return QueryUtils.applySorting(this.query.getQueryString(), sort, this.alias);
}
@Override
@@ -39,24 +52,29 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
@Override
public String detectAlias() {
return QueryUtils.detectAlias(this.query.getQueryString());
}
@Override
public String createCountQueryFor(@Nullable String countProjection) {
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery());
}
@Override
public boolean hasConstructorExpression() {
return this.hasConstructorExpression;
}
@Override
public String detectAlias() {
return this.alias;
}
@Override
public String getProjection() {
return QueryUtils.getProjection(this.query.getQueryString());
return this.projection;
}
@Override
public Set<String> getJoinAliases() {
return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
return this.joinAliases;
}
@Override

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
/**
* Implements the {@code EQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated
* {@link EqlParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.2
*/
class EqlQueryParser extends JpaQueryParser {
private EqlQueryParser(String query) {
super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(),
EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new);
}
/**
* Parse a EQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new EqlQueryParser(query);
}
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
/**
* Implements the {@code HQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated {@link HqlParser}
* and {@link HqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
class HqlQueryParser extends JpaQueryParser {
private HqlQueryParser(String query) {
super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(),
HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new);
}
/**
* Parse a HQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new HqlQueryParser(query);
}
}

View File

@@ -294,6 +294,10 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryRendererBuilder> {
@Override
public QueryRendererBuilder visitQueryOrder(HqlParser.QueryOrderContext ctx) {
if (ctx.limitClause() == null && ctx.offsetClause() == null && ctx.fetchClause() == null) {
return visit(ctx.orderByClause());
}
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.orderByClause()));
@@ -406,14 +410,16 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryRendererBuilder> {
@Override
public QueryRendererBuilder visitJoinPath(HqlParser.JoinPathContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
HqlParser.VariableContext variable = ctx.variable();
builder.appendExpression(visit(ctx.path()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
if (variable == null) {
return visit(ctx.path());
}
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.path()));
builder.appendExpression(visit(variable));
return builder;
}
@@ -461,14 +467,16 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryRendererBuilder> {
@Override
public QueryRendererBuilder visitTargetEntity(HqlParser.TargetEntityContext ctx) {
QueryRendererBuilder builder = QueryRenderer.builder();
HqlParser.VariableContext variable = ctx.variable();
builder.appendExpression(visit(ctx.entityName()));
if (ctx.variable() != null) {
builder.appendExpression(visit(ctx.variable()));
if (variable == null) {
return visit(ctx.entityName());
}
QueryRendererBuilder builder = QueryRenderer.builder();
builder.appendExpression(visit(ctx.entityName()));
builder.appendExpression(visit(variable));
return builder;
}

View File

@@ -28,6 +28,7 @@ import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.merge.Merge;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
@@ -36,18 +37,21 @@ import net.sf.jsqlparser.statement.select.SetOperationList;
import net.sf.jsqlparser.statement.select.Values;
import net.sf.jsqlparser.statement.update.Update;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.StringJoiner;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;
import org.springframework.util.StringUtils;
/**
@@ -65,6 +69,12 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
private final DeclaredQuery query;
private final Statement statement;
private final ParsedType parsedType;
private final boolean hasConstructorExpression;
private final @Nullable String primaryAlias;
private final String projection;
private final Set<String> joinAliases;
private final Set<String> selectAliases;
private final byte[] serialized;
/**
* @param query the query we want to enhance. Must not be {@literal null}.
@@ -72,13 +82,150 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
public JSqlParserQueryEnhancer(DeclaredQuery query) {
this.query = query;
try {
this.statement = CCJSqlParserUtil.parse(this.query.getQueryString());
} catch (JSQLParserException e) {
throw new IllegalArgumentException("The query is not a valid SQL Query", e);
}
this.statement = parseStatement(query.getQueryString(), Statement.class);
this.parsedType = detectParsedType(statement);
this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString());
this.primaryAlias = detectAlias(this.parsedType, this.statement);
this.projection = detectProjection(this.statement);
this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(this.statement));
this.joinAliases = Collections.unmodifiableSet(getJoinAliases(this.statement));
this.serialized = SerializationUtils.serialize(this.statement);
}
/**
* Parses a query string with JSqlParser.
*
* @param query the query to parse
* @return the parsed query
*/
private static <T extends Statement> T parseStatement(String query, Class<T> classOfT) {
try {
return classOfT.cast(CCJSqlParserUtil.parse(query));
} catch (JSQLParserException e) {
throw new IllegalArgumentException("The query you provided is not a valid SQL Query", e);
}
}
/**
* Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
* strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
*
* @return Might return {@literal null}.
*/
@Nullable
private static String detectAlias(ParsedType parsedType, Statement statement) {
if (ParsedType.MERGE.equals(parsedType)) {
Merge mergeStatement = (Merge) statement;
Alias alias = mergeStatement.getUsingAlias();
return alias == null ? null : alias.getName();
}
if (ParsedType.SELECT.equals(parsedType)) {
Select selectStatement = (Select) statement;
/*
* For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide
* alias since:
* ValuesStatement has no alias
* SetOperation can have multiple alias for each operation item
*/
if (!(selectStatement instanceof PlainSelect selectBody)) {
return null;
}
if (selectBody.getFromItem() == null) {
return null;
}
Alias alias = selectBody.getFromItem().getAlias();
return alias == null ? null : alias.getName();
}
return null;
}
/**
* Returns the aliases used inside the selection part in the query.
*
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
*/
private static Set<String> getSelectionAliases(Statement statement) {
if (!(statement instanceof PlainSelect select) || CollectionUtils.isEmpty(select.getSelectItems())) {
return Collections.emptySet();
}
Set<String> set = new HashSet<>(select.getSelectItems().size());
for (SelectItem<?> selectItem : select.getSelectItems()) {
Alias alias = selectItem.getAlias();
if (alias != null) {
set.add(alias.getName());
}
}
return set;
}
/**
* Returns the aliases used for {@code join}s.
*
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
*/
private static Set<String> getJoinAliases(Statement statement) {
if (!(statement instanceof PlainSelect selectBody) || CollectionUtils.isEmpty(selectBody.getJoins())) {
return Collections.emptySet();
}
Set<String> set = new HashSet<>(selectBody.getJoins().size());
for (Join join : selectBody.getJoins()) {
Alias alias = join.getRightItem().getAlias();
if (alias != null) {
set.add(alias.getName());
}
}
return set;
}
private static String detectProjection(Statement statement) {
if (!(statement instanceof Select select)) {
return "";
}
if (select instanceof Values) {
return "";
}
Select selectBody = select;
if (select instanceof SetOperationList setOperationList) {
// using the first one since for setoperations the projection has to be the same
selectBody = setOperationList.getSelects().get(0);
if (!(selectBody instanceof PlainSelect)) {
return "";
}
}
StringJoiner joiner = new StringJoiner(", ");
for (SelectItem<?> selectItem : ((PlainSelect) selectBody).getSelectItems()) {
joiner.add(selectItem.toString());
}
return joiner.toString().trim();
}
/**
@@ -103,259 +250,77 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
}
}
@Override
public boolean hasConstructorExpression() {
return hasConstructorExpression;
}
@Override
public String detectAlias() {
return this.primaryAlias;
}
@Override
public String getProjection() {
return this.projection;
}
@Override
public Set<String> getJoinAliases() {
return joinAliases;
}
public Set<String> getSelectionAliases() {
return selectAliases;
}
@Override
public DeclaredQuery getQuery() {
return this.query;
}
@Override
public String applySorting(Sort sort) {
return applySorting(sort, detectAlias());
}
@Override
public String applySorting(Sort sort, @Nullable String alias) {
String queryString = query.getQueryString();
Assert.hasText(queryString, "Query must not be null or empty");
if (this.parsedType != ParsedType.SELECT) {
if (this.parsedType != ParsedType.SELECT || sort.isUnsorted()) {
return queryString;
}
if (sort.isUnsorted()) {
return queryString;
}
return applySorting((Select) deserialize(this.serialized), sort, alias);
}
Select selectStatement = parseSelectStatement(queryString);
private String applySorting(Select selectStatement, Sort sort, @Nullable String alias) {
if (selectStatement instanceof SetOperationList setOperationList) {
return applySortingToSetOperationList(setOperationList, sort);
}
if (!(selectStatement instanceof PlainSelect selectBody)) {
return queryString;
return selectStatement.toString();
}
Set<String> joinAliases = getJoinAliases(selectBody);
Set<String> selectionAliases = getSelectionAliases(selectBody);
List<OrderByElement> orderByElements = sort.stream() //
.map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) //
.toList();
List<OrderByElement> orderByElements = new ArrayList<>(16);
for (Sort.Order order : sort) {
orderByElements.add(getOrderClause(joinAliases, selectAliases, alias, order));
}
if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) {
selectBody.setOrderByElements(new ArrayList<>());
selectBody.setOrderByElements(orderByElements);
} else {
selectBody.getOrderByElements().addAll(orderByElements);
}
selectBody.getOrderByElements().addAll(orderByElements);
return selectStatement.toString();
}
/**
* Returns the {@link SetOperationList} as a string query with {@link Sort}s applied in the right order.
*
* @param setOperationListStatement
* @param sort
* @return
*/
private String applySortingToSetOperationList(SetOperationList setOperationListStatement, Sort sort) {
// special case: ValuesStatements are detected as nested OperationListStatements
if (setOperationListStatement.getSelects().stream().anyMatch(Values.class::isInstance)) {
return setOperationListStatement.toString();
}
// if (CollectionUtils.isEmpty(setOperationListStatement.getOrderByElements())) {
if (setOperationListStatement.getOrderByElements() == null) {
setOperationListStatement.setOrderByElements(new ArrayList<>());
}
List<OrderByElement> orderByElements = sort.stream() //
.map(order -> getOrderClause(Collections.emptySet(), Collections.emptySet(), null, order)) //
.toList();
setOperationListStatement.getOrderByElements().addAll(orderByElements);
return setOperationListStatement.toString();
}
/**
* Returns the aliases used inside the selection part in the query.
*
* @param selectBody a {@link PlainSelect} containing a query. Must not be {@literal null}.
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
*/
private Set<String> getSelectionAliases(PlainSelect selectBody) {
if (CollectionUtils.isEmpty(selectBody.getSelectItems())) {
return new HashSet<>();
}
return selectBody.getSelectItems().stream() //
.filter(SelectItem.class::isInstance) //
.map(item -> ((SelectItem) item).getAlias()) //
.filter(Objects::nonNull) //
.map(Alias::getName) //
.collect(Collectors.toSet());
}
/**
* Returns the aliases used inside the selection part in the query.
*
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
*/
Set<String> getSelectionAliases() {
if (this.parsedType != ParsedType.SELECT) {
return new HashSet<>();
}
return this.getSelectionAliases((PlainSelect) statement);
}
/**
* Returns the aliases used for {@code join}s.
*
* @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
*/
private Set<String> getJoinAliases(String query) {
if (this.parsedType != ParsedType.SELECT) {
return new HashSet<>();
}
Select selectStatement = (Select) statement;
if (selectStatement instanceof PlainSelect selectBody) {
return getJoinAliases(selectBody);
}
return new HashSet<>();
}
/**
* Returns the aliases used for {@code join}s.
*
* @param selectBody the selection body to extract the aliases of joins from. Must not be {@literal null}.
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
*/
private Set<String> getJoinAliases(PlainSelect selectBody) {
if (CollectionUtils.isEmpty(selectBody.getJoins())) {
return new HashSet<>();
}
return selectBody.getJoins().stream() //
.map(join -> join.getRightItem().getAlias()) //
.filter(Objects::nonNull) //
.map(Alias::getName) //
.collect(Collectors.toSet());
}
/**
* Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the
* referenced property refers to a join alias, i.e. starts with {@code $alias.}.
*
* @param joinAliases the join aliases of the original query. Must not be {@literal null}.
* @param alias the alias for the root entity. May be {@literal null}.
* @param order the order object to build the clause for. Must not be {@literal null}.
* @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}.
*/
private OrderByElement getOrderClause(final Set<String> joinAliases, final Set<String> selectionAliases,
@Nullable final String alias, final Sort.Order order) {
final OrderByElement orderByElement = new OrderByElement();
orderByElement.setAsc(order.getDirection().isAscending());
orderByElement.setAscDescPresent(true);
final String property = order.getProperty();
checkSortExpression(order);
if (selectionAliases.contains(property)) {
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property);
orderByElement.setExpression(orderExpression);
return orderByElement;
}
boolean qualifyReference = joinAliases //
.parallelStream() //
.map(joinAlias -> joinAlias.concat(".")) //
.noneMatch(property::startsWith);
boolean functionIndicator = property.contains("(");
String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias)
? String.format("%s.%s", alias, property)
: property;
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference);
orderByElement.setExpression(orderExpression);
return orderByElement;
}
@Override
public String detectAlias() {
return detectAlias(this.query.getQueryString());
}
/**
* Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
* strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
*
* @param query must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
private String detectAlias(String query) {
if (ParsedType.MERGE.equals(this.parsedType)) {
Merge mergeStatement = (Merge) statement;
return detectAlias(mergeStatement);
} else if (ParsedType.SELECT.equals(this.parsedType)) {
Select selectStatement = (Select) statement;
/*
* For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide
* alias since:
* ValuesStatement has no alias
* SetOperation can have multiple alias for each operation item
*/
if (!(selectStatement instanceof PlainSelect selectBody)) {
return null;
}
return detectAlias(selectBody);
}
return null;
}
/**
* Resolves the alias for the entity to be retrieved from the given {@link PlainSelect}. Note that you only provide
* valid Query strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
*
* @param selectBody must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
private String detectAlias(PlainSelect selectBody) {
if (selectBody.getFromItem() == null) {
return null;
}
Alias alias = selectBody.getFromItem().getAlias();
return alias == null ? null : alias.getName();
}
/**
* Resolves the alias for the given {@link Merge} statement.
*
* @param mergeStatement must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
private String detectAlias(Merge mergeStatement) {
Alias alias = mergeStatement.getUsingAlias();
return alias == null ? null : alias.getName();
}
@Override
public String createCountQueryFor(@Nullable String countProjection) {
@@ -365,111 +330,112 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty");
Select selectStatement = parseSelectStatement(this.query.getQueryString());
Statement statement = (Statement) deserialize(this.serialized);
/*
We only support count queries for {@link PlainSelect}.
*/
if (!(selectStatement instanceof PlainSelect selectBody)) {
if (!(statement instanceof PlainSelect selectBody)) {
return this.query.getQueryString();
}
return createCountQueryFor(this.query, selectBody, countProjection);
}
private static String createCountQueryFor(DeclaredQuery query, PlainSelect selectBody,
@Nullable String countProjection) {
// remove order by
selectBody.setOrderByElements(null);
if (StringUtils.hasText(countProjection)) {
Function jSqlCount = getJSqlCount(Collections.singletonList(countProjection), false);
selectBody.setSelectItems(
Collections.singletonList(SelectItem.from(getJSqlCount(Collections.singletonList(countProjection), false))));
} else {
boolean distinct = selectBody.getDistinct() != null;
selectBody.setDistinct(null); // reset possible distinct
Function jSqlCount = getJSqlCount(
Collections.singletonList(countPropertyNameForSelection(selectBody.getSelectItems(), distinct)), distinct);
selectBody.setSelectItems(Collections.singletonList(SelectItem.from(jSqlCount)));
return selectBody.toString();
}
boolean distinct = selectBody.getDistinct() != null;
selectBody.setDistinct(null); // reset possible distinct
String tableAlias = detectAlias(selectBody);
String countProperty = countPropertyNameForSelection(selectBody.getSelectItems(), distinct, tableAlias);
Function jSqlCount = getJSqlCount(Collections.singletonList(countProperty), distinct);
selectBody.setSelectItems(Collections.singletonList(SelectItem.from(jSqlCount)));
return selectBody.toString();
}
@Override
public String getProjection() {
/**
* Returns the {@link SetOperationList} as a string query with {@link Sort}s applied in the right order.
*
* @param setOperationListStatement
* @param sort
* @return
*/
private static String applySortingToSetOperationList(SetOperationList setOperationListStatement, Sort sort) {
if (this.parsedType != ParsedType.SELECT) {
return "";
}
Assert.hasText(query.getQueryString(), "Query must not be null or empty");
Select selectStatement = (Select) statement;
if (selectStatement instanceof Values) {
return "";
}
Select selectBody = selectStatement;
if (selectStatement instanceof SetOperationList setOperationList) {
// using the first one since for setoperations the projection has to be the same
selectBody = setOperationList.getSelects().get(0);
if (!(selectBody instanceof PlainSelect)) {
return "";
// special case: ValuesStatements are detected as nested OperationListStatements
for (Select select : setOperationListStatement.getSelects()) {
if (select instanceof Values) {
return setOperationListStatement.toString();
}
}
return ((PlainSelect) selectBody).getSelectItems() //
.stream() //
.map(Object::toString) //
.collect(Collectors.joining(", ")).trim();
}
@Override
public Set<String> getJoinAliases() {
return this.getJoinAliases(this.query.getQueryString());
}
/**
* Parses a query string with JSqlParser.
*
* @param query the query to parse
* @return the parsed query
*/
private <T extends Statement> T parseSelectStatement(String query, Class<T> classOfT) {
try {
return classOfT.cast(CCJSqlParserUtil.parse(query));
} catch (JSQLParserException e) {
throw new IllegalArgumentException("The query you provided is not a valid SQL Query", e);
List<OrderByElement> orderByElements = new ArrayList<>(16);
for (Sort.Order order : sort) {
orderByElements.add(getOrderClause(Collections.emptySet(), Collections.emptySet(), null, order));
}
if (setOperationListStatement.getOrderByElements() == null) {
setOperationListStatement.setOrderByElements(orderByElements);
} else {
setOperationListStatement.getOrderByElements().addAll(orderByElements);
}
return setOperationListStatement.toString();
}
/**
* Parses a query string with JSqlParser.
* Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the
* referenced property refers to a join alias, i.e. starts with {@code $alias.}.
*
* @param query the query to parse
* @return the parsed query
* @param joinAliases the join aliases of the original query. Must not be {@literal null}.
* @param alias the alias for the root entity. May be {@literal null}.
* @param order the order object to build the clause for. Must not be {@literal null}.
* @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}.
*/
private Select parseSelectStatement(String query) {
return parseSelectStatement(query, Select.class);
}
private static OrderByElement getOrderClause(Set<String> joinAliases, Set<String> selectionAliases,
@Nullable String alias, Sort.Order order) {
/**
* Checks whether a given projection only contains a single column definition (aka without functions, etc.)
*
* @param projection the projection to analyse
* @return <code>true</code> when the projection only contains a single column definition otherwise <code>false</code>
*/
private boolean onlyASingleColumnProjection(List<SelectItem<?>> projection) {
OrderByElement orderByElement = new OrderByElement();
orderByElement.setAsc(order.getDirection().isAscending());
orderByElement.setAscDescPresent(true);
// this is unfortunately the only way to check without any hacky & hard string regex magic
return projection.size() == 1 && projection.get(0) instanceof SelectItem<?>
&& ((projection.get(0)).getExpression()) instanceof Column;
String property = order.getProperty();
checkSortExpression(order);
if (selectionAliases.contains(property)) {
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property);
orderByElement.setExpression(orderExpression);
return orderByElement;
}
boolean qualifyReference = true;
for (String joinAlias : joinAliases) {
if (property.startsWith(joinAlias.concat("."))) {
qualifyReference = false;
break;
}
}
boolean functionIndicator = property.contains("(");
String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias) ? alias + "." + property
: property;
Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference);
orderByElement.setExpression(orderExpression);
return orderByElement;
}
/**
@@ -481,8 +447,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @param tableAlias the table alias which can be {@literal null}.
* @return
*/
private String countPropertyNameForSelection(List<SelectItem<?>> selectItems, boolean distinct,
@Nullable String tableAlias) {
private static String countPropertyNameForSelection(List<SelectItem<?>> selectItems, boolean distinct) {
if (onlyASingleColumnProjection(selectItems)) {
@@ -491,12 +456,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
return column.getFullyQualifiedName();
}
return query.isNativeQuery() ? (distinct ? "*" : "1") : tableAlias == null ? "*" : tableAlias;
return (distinct ? "*" : "1");
}
@Override
public DeclaredQuery getQuery() {
return this.query;
/**
* Checks whether a given projection only contains a single column definition (aka without functions, etc.)
*
* @param projection the projection to analyse.
* @return {@code true} when the projection only contains a single column definition otherwise {@code false}.
*/
private static boolean onlyASingleColumnProjection(List<SelectItem<?>> projection) {
// this is unfortunately the only way to check without any hacky & hard string regex magic
return projection.size() == 1 && projection.get(0) instanceof SelectItem<?>
&& ((projection.get(0)).getExpression()) instanceof Column;
}
/**
@@ -514,4 +487,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
DELETE, UPDATE, SELECT, INSERT, MERGE, OTHER;
}
/**
* Deserialize the byte array into an object.
*
* @param bytes a serialized object
* @return the result of deserializing the bytes
*/
private static Object deserialize(byte[] bytes) {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
return ois.readObject();
} catch (IOException ex) {
throw new IllegalArgumentException("Failed to deserialize object", ex);
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("Failed to deserialize object type", ex);
}
}
}

View File

@@ -20,9 +20,9 @@ import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.schema.Column;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* A utility class for JSqlParser.
@@ -30,7 +30,9 @@ import java.util.stream.Collectors;
* @author Diego Krupitza
* @author Greg Turnquist
* @since 2.7.0
* @deprecated internal utility class.
*/
@Deprecated
public final class JSqlParserUtils {
private JSqlParserUtils() {}
@@ -42,12 +44,13 @@ public final class JSqlParserUtils {
* @param distinct if it should be a distinct count
* @return the generated count function call
*/
public static Function getJSqlCount(final List<String> countFields, final boolean distinct) {
public static Function getJSqlCount(List<String> countFields, boolean distinct) {
List<Expression> countColumns = countFields //
.stream() //
.map(Column::new) //
.collect(Collectors.toList());
List<Expression> countColumns = new ArrayList<>(countFields.size());
for (String countField : countFields) {
Column column = new Column(countField);
countColumns.add(column);
}
ExpressionList<Expression> countExpression = new ExpressionList<>(countColumns);

View File

@@ -15,14 +15,29 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of {@link QueryEnhancer} to enhance JPA queries using a {@link JpaQueryParser}.
* Implementation of {@link QueryEnhancer} to enhance JPA queries using ANTLR parsers.
*
* @author Greg Turnquist
* @author Mark Paluch
@@ -33,23 +48,59 @@ import org.springframework.util.Assert;
*/
class JpaQueryEnhancer implements QueryEnhancer {
private final DeclaredQuery query;
private final JpaQueryParser queryParser;
private final ParserRuleContext context;
private final ParsedQueryIntrospector introspector;
private final String projection;
private final BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction;
private final BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction;
/**
* Initialize with an {@link JpaQueryParser}.
*
* @param query
* @param queryParser
*/
private JpaQueryEnhancer(DeclaredQuery query, JpaQueryParser queryParser) {
JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector,
@Nullable BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction,
@Nullable BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction) {
this.query = query;
this.queryParser = queryParser;
this.context = context;
this.introspector = introspector;
this.sortFunction = sortFunction;
this.countQueryFunction = countQueryFunction;
this.introspector.visit(context);
List<JpaQueryParsingToken> tokens = introspector.getProjection();
this.projection = tokens.isEmpty() ? "" : render(tokens);
}
static <P extends Parser> ParserRuleContext parse(String query, Function<CharStream, Lexer> lexerFactoryFunction,
Function<TokenStream, P> parserFactoryFunction, Function<P, ParserRuleContext> parseFunction) {
Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query));
P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parseFunction.apply(parser);
}
/**
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using JPQL grammar.
* Apply common configuration (SLL prediction for performance, our own error listeners).
*
* @param query
* @param lexer
* @param parser
*/
static void configureParser(String query, Lexer lexer, Parser parser) {
BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);
}
/**
* Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using JPQL.
@@ -58,11 +109,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, JpqlQueryParser.parseQuery(query.getQueryString()));
return JpqlQueryParser.parseQuery(query.getQueryString());
}
/**
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using HQL grammar.
* Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using HQL.
@@ -71,11 +122,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, HqlQueryParser.parseQuery(query.getQueryString()));
return HqlQueryParser.parseQuery(query.getQueryString());
}
/**
* Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using EQL grammar.
* Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using EQL.
@@ -85,11 +136,54 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
return new JpaQueryEnhancer(query, EqlQueryParser.parseQuery(query.getQueryString()));
return EqlQueryParser.parseQuery(query.getQueryString());
}
protected JpaQueryParser getQueryParsingStrategy() {
return queryParser;
/**
* Checks if the select clause has a new constructor instantiation in the JPA query.
*
* @return Guaranteed to return {@literal true} or {@literal false}.
*/
@Override
public boolean hasConstructorExpression() {
return this.introspector.hasConstructorExpression();
}
/**
* Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
* already find the alias when generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String detectAlias() {
return this.introspector.getAlias();
}
/**
* Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
* generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String getProjection() {
return this.projection;
}
/**
* Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
* placeholder method.
*
* @return empty set
*/
@Override
public Set<String> getJoinAliases() {
return Set.of();
}
/**
* Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
*/
@Override
public DeclaredQuery getQuery() {
throw new UnsupportedOperationException();
}
/**
@@ -100,7 +194,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String applySorting(Sort sort) {
return queryParser.renderSortedQuery(sort);
return render(sortFunction.apply(sort, detectAlias()).visit(context));
}
/**
@@ -115,15 +209,6 @@ class JpaQueryEnhancer implements QueryEnhancer {
return applySorting(sort);
}
/**
* Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
* already find the alias when generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String detectAlias() {
return queryParser.findAlias();
}
/**
* Creates a count query from the original query, with no count projection.
*
@@ -141,44 +226,89 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String createCountQueryFor(@Nullable String countProjection) {
return queryParser.createCountQuery(countProjection);
return render(countQueryFunction.apply(countProjection, detectAlias()).visit(context));
}
/**
* Checks if the select clause has a new constructor instantiation in the JPA query.
* Implements the {@code HQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
* {@link HqlParser} and {@link HqlSortedQueryTransformer}.
*
* @return Guaranteed to return {@literal true} or {@literal false}.
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
@Override
public boolean hasConstructorExpression() {
return queryParser.hasConstructorExpression();
static class HqlQueryParser extends JpaQueryEnhancer {
private HqlQueryParser(String query) {
super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(),
HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new);
}
/**
* Parse a HQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new HqlQueryParser(query);
}
}
/**
* Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
* generating sorted and count queries, this is mainly to serve test cases.
*/
@Override
public String getProjection() {
return queryParser.getProjection();
}
/**
* Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
* placeholder method.
* Implements the {@code EQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
* {@link EqlParser}.
*
* @return empty set
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.2
*/
@Override
public Set<String> getJoinAliases() {
return Set.of();
static class EqlQueryParser extends JpaQueryEnhancer {
private EqlQueryParser(String query) {
super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(),
EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new);
}
/**
* Parse a EQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new EqlQueryParser(query);
}
}
/**
* Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
* Implements the {@code JPQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
* {@link JpqlParser} and {@link JpqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
@Override
public DeclaredQuery getQuery() {
return query;
static class JpqlQueryParser extends JpaQueryEnhancer {
private JpqlQueryParser(String query) {
super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(),
JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new);
}
/**
* Parse a JPQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new JpqlQueryParser(query);
}
}
}

View File

@@ -39,7 +39,6 @@ import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryMethod;
@@ -108,7 +107,7 @@ public class JpaQueryMethod extends QueryMethod {
* @param factory must not be {@literal null}
* @param extractor must not be {@literal null}
*/
protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
QueryExtractor extractor) {
super(method, metadata, factory);
@@ -145,7 +144,6 @@ public class JpaQueryMethod extends QueryMethod {
Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
() -> String.format("Modifying method must not contain %s", Parameters.TYPES));
assertParameterNamesInAnnotatedQuery();
}
private static Class<?> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
@@ -160,29 +158,7 @@ public class JpaQueryMethod extends QueryMethod {
return returnType.getType();
}
private void assertParameterNamesInAnnotatedQuery() {
String annotatedQuery = getAnnotatedQuery();
if (!DeclaredQuery.hasNamedParameter(annotatedQuery)) {
return;
}
for (Parameter parameter : getParameters()) {
if (!parameter.isNamedParameter()) {
continue;
}
if (!StringUtils.hasText(annotatedQuery)
|| !annotatedQuery.contains(String.format(":%s", parameter.getName().get()))
&& !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) {
throw new IllegalStateException(
String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'",
method, parameter.getName(), annotatedQuery));
}
}
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })

View File

@@ -1,142 +0,0 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* Operations needed to parse a JPA query.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
abstract class JpaQueryParser {
private final ParserRuleContext context;
private final ParsedQueryIntrospector introspector;
private final String projection;
private final BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction;
private final BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction;
JpaQueryParser(ParserRuleContext context, ParsedQueryIntrospector introspector,
@Nullable BiFunction<Sort, String, ParseTreeVisitor<? extends Object>> sortFunction,
@Nullable BiFunction<String, String, ParseTreeVisitor<? extends Object>> countQueryFunction) {
this.context = context;
this.introspector = introspector;
this.sortFunction = sortFunction;
this.countQueryFunction = countQueryFunction;
this.introspector.visit(context);
List<JpaQueryParsingToken> tokens = introspector.getProjection();
this.projection = tokens.isEmpty() ? "" : render(tokens);
}
static <P extends Parser> ParserRuleContext parse(String query, Function<CharStream, Lexer> lexerFactoryFunction,
Function<TokenStream, P> parserFactoryFunction, Function<P, ParserRuleContext> parseFunction) {
Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query));
P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer));
configureParser(query, lexer, parser);
return parseFunction.apply(parser);
}
/**
* Generate a query using the original query with an {@literal order by} clause added (or amended) based upon the
* provider {@link Sort} parameter.
*
* @param sort can be {@literal null}
*/
String renderSortedQuery(Sort sort) {
return render(sortFunction.apply(sort, findAlias()).visit(context));
}
/**
* Generate a count-based query derived from the original query.
*
* @param countProjection
*/
String createCountQuery(@Nullable String countProjection) {
return render(countQueryFunction.apply(countProjection, findAlias()).visit(context));
}
/**
* Find the projection of the query.
*/
String getProjection() {
return this.projection;
}
/**
* Find the alias of the query's primary FROM clause
*
* @return can be {@literal null}
*/
@Nullable
String findAlias() {
return this.introspector.getAlias();
}
/**
* Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
*
* @return Guaranteed to be {@literal true} or {@literal false}.
*/
boolean hasConstructorExpression() {
return this.introspector.hasConstructorExpression();
}
/**
* Apply common configuration (SLL prediction for performance, our own error listeners).
*
* @param query
* @param lexer
* @param parser
*/
static void configureParser(String query, Lexer lexer, Parser parser) {
BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.removeErrorListeners();
parser.addErrorListener(errorListener);
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
/**
* Implements the {@code JPQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated
* {@link JpqlParser} and {@link JpqlSortedQueryTransformer}.
*
* @author Greg Turnquist
* @author Mark Paluch
* @since 3.1
*/
class JpqlQueryParser extends JpaQueryParser {
private JpqlQueryParser(String query) {
super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(),
JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new);
}
/**
* Parse a JPQL query.
*
* @param query
* @return the query parser.
* @throws BadJpqlGrammarException
*/
public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
return new JpqlQueryParser(query);
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
/**
@@ -50,7 +51,7 @@ final class NamedQuery extends AbstractJpaQuery {
private final String countQueryName;
private final @Nullable String countProjection;
private final boolean namedCountQueryIsPresent;
private final DeclaredQuery declaredQuery;
private final Lazy<DeclaredQuery> declaredQuery;
private final QueryParameterSetter.QueryMetadataCache metadataCache;
/**
@@ -75,11 +76,6 @@ final class NamedQuery extends AbstractJpaQuery {
this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName);
Query query = em.createNamedQuery(queryName);
String queryString = extractor.extractQueryString(query);
// TODO: Detect whether a named query is a named one.
this.declaredQuery = DeclaredQuery.of(queryString, query != null && query.toString().contains("NativeQuery"));
boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters();
boolean cantExtractQuery = !extractor.canExtractQuery();
@@ -93,6 +89,10 @@ final class NamedQuery extends AbstractJpaQuery {
method));
}
String queryString = extractor.extractQueryString(query);
// TODO: Detect whether a named query is a native one.
this.declaredQuery = Lazy.of(() -> DeclaredQuery.of(queryString, query.toString().contains("NativeQuery")));
this.metadataCache = new QueryParameterSetter.QueryMetadataCache();
}
@@ -188,7 +188,7 @@ final class NamedQuery extends AbstractJpaQuery {
} else {
String countQueryString = declaredQuery.deriveCountQuery(countProjection).getQueryString();
String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString();
cacheKey = countQueryString;
countQuery = em.createQuery(countQueryString, Long.class);
}
@@ -220,7 +220,7 @@ final class NamedQuery extends AbstractJpaQuery {
return type.isInterface() ? Tuple.class : null;
}
return declaredQuery.hasConstructorExpression() //
return declaredQuery.get().hasConstructorExpression() //
? null //
: super.getTypeToRead(returnedType);
}

View File

@@ -42,6 +42,8 @@ import org.springframework.lang.Nullable;
*/
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
private final boolean queryForEntity;
/**
* Creates a new {@link NativeJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}.
*
@@ -57,6 +59,8 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
this.queryForEntity = getQueryMethod().isQueryForEntity();
Parameters<?, ?> parameters = method.getParameters();
if (parameters.hasSortParameter() && !queryString.contains("#sort")) {
@@ -77,9 +81,9 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
@Nullable
private Class<?> getTypeToQueryFor(ReturnedType returnedType) {
Class<?> result = getQueryMethod().isQueryForEntity() ? returnedType.getDomainType() : null;
Class<?> result = queryForEntity ? returnedType.getDomainType() : null;
if (this.getQuery().hasConstructorExpression() || this.getQuery().isDefaultProjection()) {
if (getQuery().hasConstructorExpression() || getQuery().isDefaultProjection()) {
return result;
}

View File

@@ -30,23 +30,11 @@ import org.springframework.lang.Nullable;
public interface QueryEnhancer {
/**
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
* Returns whether the given JPQL query contains a constructor expression.
*
* @param sort the sort specification to apply.
* @return the modified query string.
* @return whether the given JPQL query contains a constructor expression.
*/
default String applySorting(Sort sort) {
return applySorting(sort, detectAlias());
}
/**
* Adds {@literal order by} clause to the JPQL query.
*
* @param sort the sort specification to apply.
* @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
* @return the modified query string.
*/
String applySorting(Sort sort, @Nullable String alias);
boolean hasConstructorExpression();
/**
* Resolves the alias for the entity to be retrieved from the given JPA query.
@@ -56,6 +44,47 @@ public interface QueryEnhancer {
@Nullable
String detectAlias();
/**
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
*
* @return the projection part of the query.
*/
String getProjection();
/**
* Returns the join aliases of the query.
*
* @return the join aliases of the query.
*/
@Deprecated(forRemoval = true)
Set<String> getJoinAliases();
/**
* Gets the query we want to use for enhancements.
*
* @return non-null {@link DeclaredQuery} that wraps the query
*/
@Deprecated(forRemoval = true)
DeclaredQuery getQuery();
/**
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
*
* @param sort the sort specification to apply.
* @return the modified query string.
*/
String applySorting(Sort sort);
/**
* Adds {@literal order by} clause to the JPQL query.
*
* @param sort the sort specification to apply.
* @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
* @return the modified query string.
*/
@Deprecated
String applySorting(Sort sort, @Nullable String alias);
/**
* Creates a count projected query from the given original query.
*
@@ -72,29 +101,4 @@ public interface QueryEnhancer {
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
*/
String createCountQueryFor(@Nullable String countProjection);
/**
* Returns whether the given JPQL query contains a constructor expression.
*
* @return whether the given JPQL query contains a constructor expression.
*/
default boolean hasConstructorExpression() {
return QueryUtils.hasConstructorExpression(getQuery().getQueryString());
}
/**
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
*
* @return the projection part of the query.
*/
String getProjection();
Set<String> getJoinAliases();
/**
* Gets the query we want to use for enhancements.
*
* @return non-null {@link DeclaredQuery} that wraps the query
*/
DeclaredQuery getQuery();
}

View File

@@ -56,7 +56,7 @@ import org.springframework.util.StringUtils;
* @author Greg Turnquist
* @author Yuriy Tsarkov
*/
class StringQuery implements DeclaredQuery {
public class StringQuery implements DeclaredQuery {
private final String query;
private final List<ParameterBinding> bindings;
@@ -64,13 +64,14 @@ class StringQuery implements DeclaredQuery {
private final boolean usesJdbcStyleParameters;
private final boolean isNative;
private final QueryEnhancer queryEnhancer;
private final boolean hasNamedParameters;
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
StringQuery(String query, boolean isNative) {
public StringQuery(String query, boolean isNative) {
Assert.hasText(query, "Query must not be null or empty");
@@ -83,24 +84,17 @@ class StringQuery implements DeclaredQuery {
this.bindings, queryMeta);
this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
this.queryEnhancer = QueryEnhancerFactory.forQuery(this);
}
// TODO: Conflict with eager JpaQueryMethod.assertParameterNamesInAnnotatedQuery validation that attempts parsing
// without pre-processing the query leaving #{#entityName} substitution to a later time.
public static boolean hasNamedParameter(String query) {
if (ObjectUtils.isEmpty(query)) {
return false;
boolean hasNamedParameters = false;
for (ParameterBinding parameterBinding : getParameterBindings()) {
if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) {
hasNamedParameters = true;
break;
}
}
List<ParameterBinding> parameterBindings = new ArrayList<>();
Metadata queryMeta = new Metadata();
ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
parameterBindings, queryMeta);
return parameterBindings.stream().anyMatch(b -> b.getIdentifier().hasName());
this.hasNamedParameters = hasNamedParameters;
}
/**
@@ -161,7 +155,7 @@ class StringQuery implements DeclaredQuery {
@Override
public boolean hasNamedParameter() {
return bindings.stream().anyMatch(b -> b.getIdentifier().hasName());
return hasNamedParameters;
}
@Override
@@ -233,8 +227,8 @@ class StringQuery implements DeclaredQuery {
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*/
String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
List<ParameterBinding> bindings, Metadata queryMeta) {
String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List<ParameterBinding> bindings,
Metadata queryMeta) {
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;

View File

@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
* TCK Tests for {@link EqlQueryParser} mixed into {@link JpaQueryEnhancer}.
* TCK Tests for {@link JpaQueryEnhancer.EqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/

View File

@@ -30,7 +30,8 @@ import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
/**
* Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link EqlQueryParser}.
* Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
* {@link JpaQueryEnhancer.EqlQueryParser}.
*
* @author Greg Turnquist
*/
@@ -718,7 +719,7 @@ class EqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
EqlQueryParser.parseQuery(String.format("""
JpaQueryEnhancer.EqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join u.lossInspectorLimitConfiguration lil

View File

@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
* TCK Tests for {@link HqlQueryParser} mixed into {@link JpaQueryEnhancer}.
* TCK Tests for {@link JpaQueryEnhancer.HqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/

View File

@@ -31,7 +31,8 @@ import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
/**
* Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link HqlQueryParser}.
* Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
* {@link JpaQueryEnhancer.HqlQueryParser}.
*
* @author Greg Turnquist
* @author Christoph Strobl
@@ -869,7 +870,7 @@ class HqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
HqlQueryParser.parseQuery(String.format("""
JpaQueryEnhancer.HqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join fetch u.lossInspectorLimitConfiguration lil

View File

@@ -45,14 +45,7 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@ParameterizedTest // GH-2773
@MethodSource("jpqlCountQueries")
void shouldDeriveJpqlCountQuery(String query, String expected) {
assumeThat(query).as("JSQLParser does not support simple JPQL syntax").doesNotStartWithIgnoringCase("FROM");
assumeThat(query).as("JSQLParser does not support constructor JPQL syntax").doesNotContain(" new ");
assumeThat(query).as("JSQLParser does not support MOD JPQL syntax").doesNotContain("MOD(");
super.shouldDeriveJpqlCountQuery(query, expected);
assumeThat(query).as("JSQLParser does not support JPQL").isNull();
}
@Test

View File

@@ -34,6 +34,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -282,20 +283,6 @@ class JpaQueryMethodUnitTests {
assertThat(method.getNamedCountQueryName()).isEqualTo("HateoasAwareSpringDataWebConfiguration.bar.count");
}
@Test // DATAJPA-185
void rejectsInvalidNamedParameter() {
assertThatThrownBy(() -> getQueryMethod(InvalidRepository.class, "findByAnnotatedQuery", String.class))
.isInstanceOf(IllegalStateException.class)
// Parameter from query
.hasMessageContaining("foo")
// Parameter name from annotation
.hasMessageContaining("param")
// Method name
.hasMessageContaining("findByAnnotatedQuery");
}
@Test // DATAJPA-207
@SuppressWarnings({ "rawtypes", "unchecked" })
void returnsTrueIfReturnTypeIsEntity() {
@@ -529,9 +516,6 @@ class JpaQueryMethodUnitTests {
@Modifying
void updateMethod(String firstname, Sort sort);
// Typo in named parameter
@Query("select u from User u where u.firstname = :foo")
List<User> findByAnnotatedQuery(@Param("param") String param);
}
interface ValidRepository extends Repository<User, Integer> {

View File

@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
* TCK Tests for {@link JpqlQueryParser} mixed into {@link JpaQueryEnhancer}.
* TCK Tests for {@link JpaQueryEnhancer.JpqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/

View File

@@ -31,7 +31,7 @@ import org.springframework.lang.Nullable;
/**
* Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
* {@link JpqlQueryParser}.
* {@link JpaQueryEnhancer.JpqlQueryParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
@@ -736,7 +736,7 @@ class JpqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
JpqlQueryParser.parseQuery(String.format("""
JpaQueryEnhancer.JpqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join u.lossInspectorLimitConfiguration lil

View File

@@ -39,7 +39,7 @@ class QueryEnhancerFactoryUnitTests {
JpaQueryEnhancer queryParsingEnhancer = (JpaQueryEnhancer) queryEnhancer;
assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class);
assertThat(queryParsingEnhancer).isInstanceOf(JpaQueryEnhancer.HqlQueryParser.class);
}
@Test

View File

@@ -50,6 +50,7 @@ import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.util.TypeInformation;
@@ -328,6 +329,9 @@ class SimpleJpaQueryUnitTests {
@Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
List<User> findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable);
// Typo in named parameter
@Query("select u from User u where u.firstname = :foo")
List<User> findByAnnotatedQuery(@Param("param") String param);
}
interface UserProjection {}