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:
committed by
Christoph Strobl
parent
3e1c8a28a9
commit
2dc94dde8c
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,7 @@ class QueryEnhancerFactoryUnitTests {
|
||||
|
||||
JpaQueryEnhancer queryParsingEnhancer = (JpaQueryEnhancer) queryEnhancer;
|
||||
|
||||
assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class);
|
||||
assertThat(queryParsingEnhancer).isInstanceOf(JpaQueryEnhancer.HqlQueryParser.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user