Polishing.

See #2409.
This commit is contained in:
Greg L. Turnquist
2022-02-22 14:22:30 -06:00
parent 1fe4cf598d
commit 1f29463cf6
21 changed files with 323 additions and 387 deletions

View File

@@ -24,11 +24,11 @@
<eclipselink>3.0.2</eclipselink>
<hibernate>5.6.0.Final</hibernate>
<jsqlparser>4.3</jsqlparser>
<mysql-connector-java>8.0.23</mysql-connector-java>
<postgresql>42.2.19</postgresql>
<springdata.commons>3.0.0-SNAPSHOT</springdata.commons>
<vavr>0.10.3</vavr>
<jsqlparser.version>4.3</jsqlparser.version>
<hibernate.groupId>org.hibernate</hibernate.groupId>

View File

@@ -229,7 +229,7 @@
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
<version>${jsqlparser}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>

View File

@@ -15,14 +15,16 @@
*/
package org.springframework.data.jpa.repository.query;
import org.springframework.data.domain.Sort;
import java.util.Set;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* The implementation of {@link QueryEnhancer} using {@link QueryUtils}.
*
* @author Diego Krupitza
* @since 2.7.0
*/
public class DefaultQueryEnhancer implements QueryEnhancer {
@@ -33,17 +35,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
}
@Override
public String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable<String> idAttributes) {
return QueryUtils.getExistsQueryString(entityName, countQueryPlaceHolder, idAttributes);
}
@Override
public String getQueryString(String template, String entityName) {
return QueryUtils.getQueryString(template, entityName);
}
@Override
public String applySorting(Sort sort, String alias) {
public String applySorting(Sort sort, @Nullable String alias) {
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
@@ -53,7 +45,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
}
@Override
public String createCountQueryFor(String countProjection) {
public String createCountQueryFor(@Nullable String countProjection) {
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection);
}

View File

@@ -69,19 +69,18 @@ class ExpressionBasedStringQuery extends StringQuery {
* @param query the original query. Must not be {@literal null}.
* @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}.
* @param parser Parser for resolving SpEL expressions. Must not be {@literal null}.
* @param nativeQuery
* @param nativeQuery is a given query native or not
* @return A query supporting SpEL expressions.
*/
static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata, SpelExpressionParser parser,
boolean nativeQuery) {
static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata<?> metadata,
SpelExpressionParser parser, boolean nativeQuery) {
return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery);
}
/**
* @param query, the query expression potentially containing a SpEL expression. Must not be {@literal null}.}
* @param query, the query expression potentially containing a SpEL expression. Must not be {@literal null}.
* @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}.
* @param parser Must not be {@literal null}.
* @return
*/
private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata<?> metadata,
SpelExpressionParser parser) {

View File

@@ -15,38 +15,44 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*;
import static org.springframework.data.jpa.repository.query.QueryUtils.*;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.util.SelectUtils;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
import net.sf.jsqlparser.statement.select.SelectItem;
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 org.springframework.data.domain.Sort;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*;
import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression;
/**
* The implementation of {@link QueryEnhancer} using JSqlParser.
*
* @author Diego Krupitza
* @author Greg Turnquist
* @since 2.7.0
*/
public class JSqlParserQueryEnhancer implements QueryEnhancer {
private static final String DEFAULT_TABLE_ALIAS = "x";
private final DeclaredQuery query;
/**
@@ -57,40 +63,8 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
}
@Override
public String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable<String> idAttributes) {
final Table tableNameWithAlias = getTableWithAlias(entityName, DEFAULT_TABLE_ALIAS);
Function jSqlCount = getJSqlCount(Collections.singletonList(countQueryPlaceHolder), false);
public String applySorting(Sort sort, @Nullable String alias) {
Select select = SelectUtils.buildSelectFromTableAndSelectItems(tableNameWithAlias,
new SelectExpressionItem(jSqlCount));
PlainSelect selectBody = (PlainSelect) select.getSelectBody();
List<Expression> equalityExpressions = Streamable.of(idAttributes).stream() //
.map(field -> {
Expression tableNameField = new Column().withTable(tableNameWithAlias).withColumnName(field);
Expression inputField = new Column(":".concat(field));
return new EqualsTo(tableNameField, inputField);
}).collect(Collectors.toList());
if (equalityExpressions.size() > 1) {
AndExpression rootOfWhereClause = concatenateWithAndExpression(equalityExpressions);
selectBody.setWhere(rootOfWhereClause);
} else if (equalityExpressions.size() == 1) {
selectBody.setWhere(equalityExpressions.get(0));
}
return selectBody.toString();
}
@Override
public String getQueryString(String template, String entityName) {
Assert.hasText(entityName, "Entity name must not be null or empty!");
return String.format(template, entityName);
}
@Override
public String applySorting(Sort sort, String alias) {
String queryString = query.getQueryString();
Assert.hasText(queryString, "Query must not be null or empty!");
@@ -145,6 +119,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
*/
Set<String> getSelectionAliases() {
Select selectStatement = parseSelectStatement(this.query.getQueryString());
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
return this.getSelectionAliases(selectBody);
@@ -189,7 +164,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @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,
final String alias, final Sort.Order order) {
@Nullable final String alias, final Sort.Order order) {
final OrderByElement orderByElement = new OrderByElement();
orderByElement.setAsc(order.getDirection().isAscending());
@@ -233,7 +208,9 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @param query must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
private String detectAlias(String query) {
Select selectStatement = parseSelectStatement(query);
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
return detectAlias(selectBody);
@@ -246,13 +223,15 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @param selectBody must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
private static String detectAlias(PlainSelect selectBody) {
Alias alias = selectBody.getFromItem().getAlias();
return alias == null ? null : alias.getName();
}
@Override
public String createCountQueryFor(String countProjection) {
public String createCountQueryFor(@Nullable String countProjection) {
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty!");
@@ -298,6 +277,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
@Override
public String getProjection() {
Assert.hasText(query.getQueryString(), "Query must not be null or empty!");
Select selectStatement = parseSelectStatement(query.getQueryString());
@@ -321,6 +301,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
* @return the parsed query
*/
private static Select parseSelectStatement(String query) {
try {
return (Select) CCJSqlParserUtil.parse(query);
} catch (JSQLParserException e) {
@@ -329,12 +310,13 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
}
/**
* Checks whether a given projection only contains a single column definition (aka without functions, etc)
* 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) {
// this is unfortunately the only way to check without any hacky & hard string regex magic
return projection.size() == 1 && projection.get(0) instanceof SelectExpressionItem
&& (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column;

View File

@@ -15,15 +15,10 @@
*/
package org.springframework.data.jpa.repository.query;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
@@ -33,58 +28,12 @@ import java.util.stream.Collectors;
* A utility class for JSqlParser.
*
* @author Diego Krupitza
* @author Greg Turnquist
* @since 2.7.0
*/
public final class JSqlParserUtils {
private JSqlParserUtils() {
}
/**
* Generates a JSqlParser table from an entity name and an optional alias name
*
* @param entityName the name of the table
* @param alias the optional alias. Might be {@literal null} or empty
* @return the newly generated table
*/
public static Table getTableWithAlias(String entityName, String alias) {
Table table = new Table(entityName);
return StringUtils.hasText(alias) ? table.withAlias(new Alias(alias)) : table;
}
/**
* Concatenates a list of expression with <code>AND</code>.
*
* @param expressions the list of expressions to concatenate. Has to be non empty and with size >= 2
* @return the root of the concatenated expression
*/
public static AndExpression concatenateWithAndExpression(List<Expression> expressions) {
if (CollectionUtils.isEmpty(expressions) || expressions.size() == 1) {
throw new IllegalArgumentException(
"The list of expression has to be at least of length 2! Otherwise it is not possible to concatinate with an");
}
AndExpression rootAndExpression = new AndExpression();
AndExpression currentLocation = rootAndExpression;
// traverse the list with looking 1 element ahead
for (int i = 0; i < expressions.size(); i++) {
Expression currentExpression = expressions.get(i);
if (currentLocation.getLeftExpression() == null) {
currentLocation.setLeftExpression(currentExpression);
} else if (currentLocation.getRightExpression() == null && i == expressions.size() - 1) {
currentLocation.setRightExpression(currentExpression);
} else {
AndExpression nextAndExpression = new AndExpression();
nextAndExpression.setLeftExpression(currentExpression);
currentLocation.setRightExpression(nextAndExpression);
currentLocation = (AndExpression) currentLocation.getRightExpression();
}
}
return rootAndExpression;
}
private JSqlParserUtils() {}
/**
* Generates a count function call, based on the {@code countFields}.
@@ -94,12 +43,14 @@ public final class JSqlParserUtils {
* @return the generated count function call
*/
public static Function getJSqlCount(final List<String> countFields, final boolean distinct) {
List<Expression> countColumns = countFields //
.stream() //
.map(Column::new) //
.collect(Collectors.toList());
ExpressionList countExpression = new ExpressionList(countColumns);
return new Function() //
.withName("count") //
.withParameters(countExpression) //
@@ -113,11 +64,12 @@ public final class JSqlParserUtils {
* @return the generated lower function call
*/
public static Function getJSqlLower(String column) {
List<Expression> expressions = Collections.singletonList(new Column(column));
ExpressionList lowerParamExpression = new ExpressionList(expressions);
return new Function() //
.withName("lower") //
.withParameters(lowerParamExpression);
}
}

View File

@@ -55,6 +55,7 @@ import org.springframework.util.Assert;
* @author Reda.Housni-Alaoui
* @author Moritz Becker
* @author Andrey Kovalev
* @author Greg Turnquist
*/
public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extends Object>, Predicate> {
@@ -166,8 +167,10 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
Class<?> typeToRead = returnedType.getReturnedType();
query = typeToRead.isInterface() ? query.multiselect(selections)
: query.select((Selection) builder.construct(typeToRead, selections.toArray(new Selection[0])));
query = typeToRead.isInterface() //
? query.multiselect(selections) //
: query.select((Selection) builder.construct(typeToRead, //
selections.toArray(new Selection[0])));
} else if (tree.isExistsProjection()) {
@@ -239,83 +242,84 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
Type type = part.getType();
switch (type) {
case BETWEEN:
ParameterMetadata<Comparable> first = provider.next(part);
ParameterMetadata<Comparable> second = provider.next(part);
return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
case AFTER:
case GREATER_THAN:
return builder.greaterThan(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case GREATER_THAN_EQUAL:
return builder.greaterThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case BEFORE:
case LESS_THAN:
return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
case LESS_THAN_EQUAL:
return builder.lessThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case IS_NULL:
return getTypedPath(root, part).isNull();
case IS_NOT_NULL:
return getTypedPath(root, part).isNotNull();
case NOT_IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
case NOT_CONTAINING:
case BETWEEN:
ParameterMetadata<Comparable> first = provider.next(part);
ParameterMetadata<Comparable> second = provider.next(part);
return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
case AFTER:
case GREATER_THAN:
return builder.greaterThan(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case GREATER_THAN_EQUAL:
return builder.greaterThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case BEFORE:
case LESS_THAN:
return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
case LESS_THAN_EQUAL:
return builder.lessThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
case IS_NULL:
return getTypedPath(root, part).isNull();
case IS_NOT_NULL:
return getTypedPath(root, part).isNotNull();
case NOT_IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
case NOT_CONTAINING:
if (property.getLeafProperty().isCollection()) {
if (property.getLeafProperty().isCollection()) {
Expression<Collection<Object>> propertyExpression = traversePath(root, property);
ParameterExpression<Object> parameterExpression = provider.next(part).getExpression();
Expression<Collection<Object>> propertyExpression = traversePath(root, property);
ParameterExpression<Object> parameterExpression = provider.next(part).getExpression();
// Can't just call .not() in case of negation as EclipseLink chokes on that.
return type.equals(NOT_CONTAINING) ? isNotMember(builder, parameterExpression, propertyExpression)
: isMember(builder, parameterExpression, propertyExpression);
}
// Can't just call .not() in case of negation as EclipseLink chokes on that.
return type.equals(NOT_CONTAINING) //
? isNotMember(builder, parameterExpression, propertyExpression) //
: isMember(builder, parameterExpression, propertyExpression);
}
case LIKE:
case NOT_LIKE:
Expression<String> stringPath = getTypedPath(root, part);
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
Expression<String> parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
case TRUE:
Expression<Boolean> truePath = getTypedPath(root, part);
return builder.isTrue(truePath);
case FALSE:
Expression<Boolean> falsePath = getTypedPath(root, part);
return builder.isFalse(falsePath);
case SIMPLE_PROPERTY:
ParameterMetadata<Object> expression = provider.next(part);
Expression<Object> path = getTypedPath(root, part);
return expression.isIsNullParameter() ? path.isNull()
: builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
case NEGATING_SIMPLE_PROPERTY:
return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)),
upperIfIgnoreCase(provider.next(part).getExpression()));
case IS_EMPTY:
case IS_NOT_EMPTY:
case LIKE:
case NOT_LIKE:
Expression<String> stringPath = getTypedPath(root, part);
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
Expression<String> parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
case TRUE:
Expression<Boolean> truePath = getTypedPath(root, part);
return builder.isTrue(truePath);
case FALSE:
Expression<Boolean> falsePath = getTypedPath(root, part);
return builder.isFalse(falsePath);
case SIMPLE_PROPERTY:
ParameterMetadata<Object> expression = provider.next(part);
Expression<Object> path = getTypedPath(root, part);
return expression.isIsNullParameter() ? path.isNull()
: builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
case NEGATING_SIMPLE_PROPERTY:
return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)),
upperIfIgnoreCase(provider.next(part).getExpression()));
case IS_EMPTY:
case IS_NOT_EMPTY:
if (!property.getLeafProperty().isCollection()) {
throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties!");
}
if (!property.getLeafProperty().isCollection()) {
throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties!");
}
Expression<Collection<Object>> collectionPath = traversePath(root, property);
return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
Expression<Collection<Object>> collectionPath = traversePath(root, property);
return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
default:
throw new IllegalArgumentException("Unsupported keyword " + type);
default:
throw new IllegalArgumentException("Unsupported keyword " + type);
}
}
@@ -340,22 +344,22 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
switch (part.shouldIgnoreCase()) {
case ALWAYS:
case ALWAYS:
Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName()
+ " types, the property '" + part.getProperty().getSegment() + "' must reference a String");
return (Expression<T>) builder.upper((Expression<String>) expression);
case WHEN_POSSIBLE:
if (canUpperCase(expression)) {
Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName()
+ " types, the property '" + part.getProperty().getSegment() + "' must reference a String");
return (Expression<T>) builder.upper((Expression<String>) expression);
}
case NEVER:
default:
case WHEN_POSSIBLE:
return (Expression<T>) expression;
if (canUpperCase(expression)) {
return (Expression<T>) builder.upper((Expression<String>) expression);
}
case NEVER:
default:
return (Expression<T>) expression;
}
}

View File

@@ -24,27 +24,11 @@ import org.springframework.lang.Nullable;
* This interface describes the API for enhancing a given Query.
*
* @author Diego Krupitza
* @author Greg Turnquist
* @since 2.7.0
*/
public interface QueryEnhancer {
/**
* Returns the query string to execute an exists query for the given id attributes.
*
* @param entityName the name of the entity to create the query for, must not be {@literal null}.
* @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
* @param idAttributes the id attributes for the entity, must not be {@literal null}.
*/
String getExistsQueryString(String entityName, String countQueryPlaceHolder, Iterable<String> idAttributes);
/**
* Returns the query string for the given class name.
*
* @param template must not be {@literal null}.
* @param entityName must not be {@literal null}.
* @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
*/
String getQueryString(String template, String entityName);
/**
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
*
@@ -110,7 +94,7 @@ public interface QueryEnhancer {
/**
* Gets the query we want to use for enhancements.
*
* @return non null {@link DeclaredQuery} that wraps the query
* @return non-null {@link DeclaredQuery} that wraps the query
*/
DeclaredQuery getQuery();
}

View File

@@ -22,6 +22,8 @@ import org.apache.commons.logging.LogFactory;
* Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}.
*
* @author Diego Krupitza
* @author Greg Turnquist
* @since 2.7.0
*/
public final class QueryEnhancerFactory {
@@ -29,8 +31,7 @@ public final class QueryEnhancerFactory {
private static final boolean JSQLPARSER_IN_CLASSPATH = isJSqlParserInClassPath();
private QueryEnhancerFactory() {
}
private QueryEnhancerFactory() {}
/**
* Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}.
@@ -39,6 +40,7 @@ public final class QueryEnhancerFactory {
* @return an implementation of {@link QueryEnhancer} that suits the query the most
*/
public static QueryEnhancer forQuery(DeclaredQuery query) {
if (qualifiesForJSqlParserUsage(query)) {
return new JSqlParserQueryEnhancer(query);
} else {
@@ -63,6 +65,7 @@ public final class QueryEnhancerFactory {
* @return <code>true</code> when in classpath otherwise <code>false</code>
*/
private static boolean isJSqlParserInClassPath() {
try {
Class.forName("net.sf.jsqlparser.parser.JSqlParser", false, QueryEnhancerFactory.class.getClassLoader());
LOG.info("JSqlParser is in classpath. If applicable JSqlParser will be used.");
@@ -71,5 +74,4 @@ public final class QueryEnhancerFactory {
return false;
}
}
}

View File

@@ -32,7 +32,6 @@ import jakarta.persistence.criteria.ParameterExpression;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -49,8 +48,7 @@ interface QueryParameterSetter {
void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
/** Noop implementation */
QueryParameterSetter NOOP = (query, values, errorHandling) -> {
};
QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
/**
* {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.

View File

@@ -666,8 +666,8 @@ public abstract class QueryUtils {
}
/**
* Checks if this attribute requires an outer join. This is the case eg. if it hadn't already been fetched with an
* inner join and if it's an a optional association, and if previous paths has already required outer joins. It also
* Checks if this attribute requires an outer join. This is the case e.g. if it hadn't already been fetched with an
* inner join and if it's an optional association, and if previous paths has already required outer joins. It also
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999).
*
* @param from the {@link From} to check for fetches.
@@ -741,6 +741,7 @@ public abstract class QueryUtils {
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
}
@Nullable
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());

View File

@@ -101,10 +101,9 @@ class StringQuery implements DeclaredQuery {
}
@Override
@SuppressWarnings("deprecation")
public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable String countQueryProjection) {
return DeclaredQuery.of(
return DeclaredQuery.of( //
countQuery != null ? countQuery : this.queryEnhancer.createCountQueryFor(countQueryProjection), //
this.isNative);
}
@@ -259,36 +258,37 @@ class StringQuery implements DeclaredQuery {
switch (ParameterBindingType.of(typeSource)) {
case LIKE:
case LIKE:
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
replacement = matcher.group(3);
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
replacement = matcher.group(3);
if (parameterIndex != null) {
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
} else {
checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
if (parameterIndex != null) {
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
} else {
checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
replacement = ":" + parameterName;
}
replacement = ":" + parameterName;
}
break;
break;
case IN:
case IN:
if (parameterIndex != null) {
checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
} else {
checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
}
if (parameterIndex != null) {
checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
} else {
checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
}
break;
break;
case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
default:
case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
default:
bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
: new ParameterBinding(parameterName, null, expression));
bindings.add(parameterIndex != null //
? new ParameterBinding(null, parameterIndex, expression) //
: new ParameterBinding(parameterName, null, expression));
}
if (replacement != null) {

View File

@@ -57,8 +57,6 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@@ -103,12 +101,10 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional
public class UserRepositoryTests {
@PersistenceContext
EntityManager em;
@PersistenceContext EntityManager em;
// CUT
@Autowired
UserRepository repository;
@Autowired UserRepository repository;
// Test fixture
private User firstUser;
@@ -147,7 +143,7 @@ public class UserRepositoryTests {
}
@Test
void testRead() throws Exception {
void testRead() {
flushTestUsers();
@@ -172,7 +168,7 @@ public class UserRepositoryTests {
}
@Test
void savesCollectionCorrectly() throws Exception {
void savesCollectionCorrectly() {
assertThat(repository.saveAll(asList(firstUser, secondUser, thirdUser))) //
.containsExactlyInAnyOrder(firstUser, secondUser, thirdUser);
@@ -186,7 +182,7 @@ public class UserRepositoryTests {
}
@Test
void savingEmptyCollectionIsNoOp() throws Exception {
void savingEmptyCollectionIsNoOp() {
assertThat(repository.saveAll(new ArrayList<>())).isEmpty();
}
@@ -207,7 +203,7 @@ public class UserRepositoryTests {
}
@Test
void existReturnsWhetherAnEntityCanBeLoaded() throws Exception {
void existReturnsWhetherAnEntityCanBeLoaded() {
flushTestUsers();
assertThat(repository.existsById(id)).isTrue();
@@ -237,7 +233,7 @@ public class UserRepositoryTests {
}
@Test
void returnsAllSortedCorrectly() throws Exception {
void returnsAllSortedCorrectly() {
flushTestUsers();
@@ -246,7 +242,7 @@ public class UserRepositoryTests {
}
@Test // DATAJPA-296
void returnsAllIgnoreCaseSortedCorrectly() throws Exception {
void returnsAllIgnoreCaseSortedCorrectly() {
flushTestUsers();
@@ -306,7 +302,7 @@ public class UserRepositoryTests {
}
@Test
void executesManipulatingQuery() throws Exception {
void executesManipulatingQuery() {
flushTestUsers();
repository.renameAllUsersTo("newLastname");
@@ -320,11 +316,11 @@ public class UserRepositoryTests {
flushTestUsers();
repository.findByLastname((String) null);
repository.findByLastname(null);
}
@Test
void testFindByLastname() throws Exception {
void testFindByLastname() {
flushTestUsers();
@@ -335,7 +331,7 @@ public class UserRepositoryTests {
* Tests, that searching by the email address of the reference user returns exactly that instance.
*/
@Test
void testFindByEmailAddress() throws Exception {
void testFindByEmailAddress() {
flushTestUsers();
@@ -358,7 +354,7 @@ public class UserRepositoryTests {
* Tests that all users get deleted by triggering {@link UserRepository#deleteAll()}.
*/
@Test
void deleteAll() throws Exception {
void deleteAll() {
flushTestUsers();
@@ -472,14 +468,14 @@ public class UserRepositoryTests {
}
@Test
void executesSingleEntitySpecificationCorrectly() throws Exception {
void executesSingleEntitySpecificationCorrectly() {
flushTestUsers();
assertThat(repository.findOne(userHasFirstname("Oliver"))).contains(firstUser);
}
@Test
void returnsNullIfNoEntityFoundForSingleEntitySpecification() throws Exception {
void returnsNullIfNoEntityFoundForSingleEntitySpecification() {
flushTestUsers();
assertThat(repository.findOne(userHasLastname("Beauford"))).isNotPresent();
@@ -524,7 +520,7 @@ public class UserRepositoryTests {
}
@Test
void executesMethodWithAnnotatedNamedParametersCorrectly() throws Exception {
void executesMethodWithAnnotatedNamedParametersCorrectly() {
firstUser = repository.save(firstUser);
secondUser = repository.save(secondUser);
@@ -533,7 +529,7 @@ public class UserRepositoryTests {
}
@Test
void executesMethodWithNamedParametersCorrectlyOnMethodsWithQueryCreation() throws Exception {
void executesMethodWithNamedParametersCorrectlyOnMethodsWithQueryCreation() {
firstUser = repository.save(firstUser);
secondUser = repository.save(secondUser);
@@ -542,7 +538,7 @@ public class UserRepositoryTests {
}
@Test
void executesLikeAndOrderByCorrectly() throws Exception {
void executesLikeAndOrderByCorrectly() {
flushTestUsers();
@@ -551,7 +547,7 @@ public class UserRepositoryTests {
}
@Test
void executesNotLikeCorrectly() throws Exception {
void executesNotLikeCorrectly() {
flushTestUsers();
@@ -559,7 +555,7 @@ public class UserRepositoryTests {
}
@Test
void executesSimpleNotCorrectly() throws Exception {
void executesSimpleNotCorrectly() {
flushTestUsers();
@@ -567,21 +563,21 @@ public class UserRepositoryTests {
}
@Test
void returnsSameListIfNoSpecGiven() throws Exception {
void returnsSameListIfNoSpecGiven() {
flushTestUsers();
assertSameElements(repository.findAll(), repository.findAll((Specification<User>) null));
}
@Test
void returnsSameListIfNoSortIsGiven() throws Exception {
void returnsSameListIfNoSortIsGiven() {
flushTestUsers();
assertSameElements(repository.findAll(Sort.unsorted()), repository.findAll());
}
@Test
void returnsSamePageIfNoSpecGiven() throws Exception {
void returnsSamePageIfNoSpecGiven() {
Pageable pageable = PageRequest.of(0, 1);
@@ -590,14 +586,14 @@ public class UserRepositoryTests {
}
@Test
void returnsAllAsPageIfNoPageableIsGiven() throws Exception {
void returnsAllAsPageIfNoPageableIsGiven() {
flushTestUsers();
assertThat(repository.findAll(Pageable.unpaged())).isEqualTo(new PageImpl<>(repository.findAll()));
}
@Test
void removeDetachedObject() throws Exception {
void removeDetachedObject() {
flushTestUsers();
@@ -608,14 +604,14 @@ public class UserRepositoryTests {
}
@Test
void executesPagedSpecificationsCorrectly() throws Exception {
void executesPagedSpecificationsCorrectly() {
Page<User> result = executeSpecWithSort(Sort.unsorted());
assertThat(result.getContent()).isSubsetOf(firstUser, thirdUser);
}
@Test
void executesPagedSpecificationsWithSortCorrectly() throws Exception {
void executesPagedSpecificationsWithSortCorrectly() {
Page<User> result = executeSpecWithSort(Sort.by(Direction.ASC, "lastname"));
@@ -623,7 +619,7 @@ public class UserRepositoryTests {
}
@Test
void executesPagedSpecificationWithSortCorrectly2() throws Exception {
void executesPagedSpecificationWithSortCorrectly2() {
Page<User> result = executeSpecWithSort(Sort.by(Direction.DESC, "lastname"));
@@ -631,7 +627,7 @@ public class UserRepositoryTests {
}
@Test
void executesQueryMethodWithDeepTraversalCorrectly() throws Exception {
void executesQueryMethodWithDeepTraversalCorrectly() {
flushTestUsers();
@@ -644,7 +640,7 @@ public class UserRepositoryTests {
}
@Test
void executesFindByColleaguesLastnameCorrectly() throws Exception {
void executesFindByColleaguesLastnameCorrectly() {
flushTestUsers();
@@ -658,7 +654,7 @@ public class UserRepositoryTests {
}
@Test
void executesFindByNotNullLastnameCorrectly() throws Exception {
void executesFindByNotNullLastnameCorrectly() {
flushTestUsers();
@@ -666,7 +662,7 @@ public class UserRepositoryTests {
}
@Test
void executesFindByNullLastnameCorrectly() throws Exception {
void executesFindByNullLastnameCorrectly() {
flushTestUsers();
User forthUser = repository.save(new User("Foo", null, "email@address.com"));
@@ -675,7 +671,7 @@ public class UserRepositoryTests {
}
@Test
void findsSortedByLastname() throws Exception {
void findsSortedByLastname() {
flushTestUsers();
@@ -1676,7 +1672,7 @@ public class UserRepositoryTests {
}
@Test // DATAJPA-606
void findByEmptyCollectionOfStrings() throws Exception {
void findByEmptyCollectionOfStrings() {
flushTestUsers();
@@ -1685,7 +1681,7 @@ public class UserRepositoryTests {
}
@Test // DATAJPA-606
void findByEmptyCollectionOfIntegers() throws Exception {
void findByEmptyCollectionOfIntegers() {
flushTestUsers();
@@ -1694,7 +1690,7 @@ public class UserRepositoryTests {
}
@Test // DATAJPA-606
void findByEmptyArrayOfIntegers() throws Exception {
void findByEmptyArrayOfIntegers() {
flushTestUsers();
@@ -1736,7 +1732,7 @@ public class UserRepositoryTests {
flushTestUsers();
try (Stream<User> stream = repository.streamAllPaged(PageRequest.of(0, 2));) {
try (Stream<User> stream = repository.streamAllPaged(PageRequest.of(0, 2))) {
assertThat(stream).hasSize(2);
}
}
@@ -2088,13 +2084,11 @@ public class UserRepositoryTests {
User prototype = new User();
prototype.setFirstname("v");
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> {
repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.sortBy(Sort.by("firstname")).oneValue());
});
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> repository.findBy(
of(prototype,
matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname",
GenericPropertyMatcher::contains)), //
q -> q.sortBy(Sort.by("firstname")).oneValue()));
}
@Test // GH-2294
@@ -2552,7 +2546,7 @@ public class UserRepositoryTests {
}
@Test // DATAJPA-1307
void testFindByEmailAddressJdbcStyleParameter() throws Exception {
void testFindByEmailAddressJdbcStyleParameter() {
flushTestUsers();

View File

@@ -24,7 +24,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
@@ -42,8 +41,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
class ExpressionBasedStringQueryUnitTests {
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
@Mock
JpaEntityMetadata<?> metadata;
@Mock JpaEntityMetadata<?> metadata;
@Test // DATAJPA-170
void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() {
@@ -93,6 +91,7 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void shouldDetectComplexNativeQueriesWithSpelAsNonNative() {
StringQuery query = new ExpressionBasedStringQuery(
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )\"\n"
+ "+ \"AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)\"\n"
@@ -105,16 +104,17 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() {
StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, SPEL_PARSER, true);
assertThat(query.isNativeQuery()).isFalse();
}
@Test
void shouldDetectSimpleNativeQueriesWithoutSpelAsNonNative() {
void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() {
StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, SPEL_PARSER, true);
assertThat(query.isNativeQuery()).isTrue();
}
}

View File

@@ -35,7 +35,6 @@ 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;
@@ -74,10 +73,8 @@ public class JpaQueryMethodUnitTests {
private static final String METHOD_NAME = "findByFirstname";
@Mock
QueryExtractor extractor;
@Mock
RepositoryMetadata metadata;
@Mock QueryExtractor extractor;
@Mock RepositoryMetadata metadata;
private ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
private Method invalidReturnType;
@@ -520,7 +517,7 @@ public class JpaQueryMethodUnitTests {
interface ValidRepository extends Repository<User, Integer> {
@Query(value = "Select u from User u where u.lastname = ?1", nativeQuery = true)
@Query(value = "select u from User u where u.lastname = ?1", nativeQuery = true)
List<User> findByLastname(String lastname);
@Query(name = "HateoasAwareSpringDataWebConfiguration.bar")

View File

@@ -15,9 +15,9 @@
*/
package org.springframework.data.jpa.repository.query;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link QueryEnhancerFactory}.
@@ -28,18 +28,22 @@ class QueryEnhancerFactoryUnitTests {
@Test
void createsDefaultImplementationForNonNativeQuery() {
StringQuery query = new StringQuery("Select new User(u.firstname) from User u", false);
StringQuery query = new StringQuery("select new User(u.firstname) from User u", false);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query);
assertThat(queryEnhancer) //
.isInstanceOf(DefaultQueryEnhancer.class);
}
@Test
void createsJSqlImplementationForNativeQuery() {
StringQuery query = new StringQuery("Select * from User", true);
StringQuery query = new StringQuery("select * from User", true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query);
assertThat(queryEnhancer) //
.isInstanceOf(JSqlParserQueryEnhancer.class);
}

View File

@@ -15,6 +15,14 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -24,16 +32,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.JpaSort;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.*;
/**
* Unit test for {@link QueryEnhancer}.
* Unit tests for {@link QueryEnhancer}.
*
* @author Diego Krupitza
*/
@@ -114,6 +114,7 @@ class QueryEnhancerUnitTests {
}
public static Stream<Arguments> detectsAliasWithUCorrectlySource() {
return Stream.of( //
Arguments.of(new StringQuery(QUERY, true), "u"), //
Arguments.of(new StringQuery(SIMPLE_QUERY, false), "u"), //
@@ -130,6 +131,7 @@ class QueryEnhancerUnitTests {
void allowsFullyQualifiedEntityNamesInQuery() {
StringQuery query = new StringQuery(FQ_QUERY, true);
assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
assertCountQuery(FQ_QUERY, "select count(u) from org.acme.domain.User$Foo_Bar u", true);
}
@@ -138,6 +140,7 @@ class QueryEnhancerUnitTests {
void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() {
StringQuery query = new StringQuery("select p from Person p left join p.address address", true);
endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("address.city")), "order by address.city asc");
endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("address.city", "lastname"), "p"),
"order by address.city asc, p.lastname asc");
@@ -147,6 +150,7 @@ class QueryEnhancerUnitTests {
void extendsExistingOrderByClausesCorrectly() {
StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("firstname"), "p"),
"order by p.lastname asc, p.firstname asc");
}
@@ -157,6 +161,7 @@ class QueryEnhancerUnitTests {
Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
StringQuery query = new StringQuery("select p from Person p", true);
endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by lower(p.firstname) asc");
}
@@ -166,11 +171,12 @@ class QueryEnhancerUnitTests {
Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by p.lastname asc, lower(p.firstname) asc");
}
@Test // DATAJPA-342
void usesReturnedVariableInCOuntProjectionIfSet() {
void usesReturnedVariableInCountProjectionIfSet() {
assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc",
"select count(distinct m.genre) from Media m where m.user = ?1", true);
@@ -185,6 +191,7 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-148
void doesNotPrefixSortsIfFunction() {
StringQuery query = new StringQuery("select p from Person p", true);
Sort sort = Sort.by("sum(foo)");
@@ -207,6 +214,7 @@ class QueryEnhancerUnitTests {
Sort sort = Sort.by("lastname");
StringQuery originalQuery = new StringQuery("select p from Person p ORDER BY p.firstname", true);
String query = getEnhancer(originalQuery).applySorting(sort, "p");
endsIgnoringCase(query, "ORDER BY p.firstname, p.lastname asc");
}
@@ -222,15 +230,17 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-456
void createCountQueryFromTheGivenCountProjection() {
StringQuery query = new StringQuery("select p.lastname,p.firstname from Person p", true);
assertThat(getEnhancer(query).createCountQueryFor("p.lastname"))
.isEqualToIgnoringCase("select count(p.lastname) from Person p");
}
@Test // DATAJPA-726
void detectsAliassesInPlainJoins() {
void detectsAliasesInPlainJoins() {
StringQuery query = new StringQuery("select p from Customer c join c.productOrder p where p.delaye = true", true);
StringQuery query = new StringQuery("select p from Customer c join c.productOrder p where p.delay = true", true);
Sort sort = Sort.by("p.lineItems");
endsIgnoringCase(getEnhancer(query).applySorting(sort, "c"), "order by p.lineItems asc");
@@ -238,13 +248,17 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-736
void supportsNonAsciiCharactersInEntityNames() {
StringQuery query = new StringQuery("select u from Usèr u", true);
assertThat(getEnhancer(query).createCountQueryFor()).isEqualToIgnoringCase("select count(u) from Usèr u");
}
@Test // DATAJPA-798
void detectsAliasInQueryContainingLineBreaks() {
StringQuery query = new StringQuery("select \n u \n from \n User \nu", true);
assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
}
@@ -259,6 +273,7 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-815
void doesPrefixPropertyWithNative() {
StringQuery query = new StringQuery("Select * from Cat c join Dog d", true);
Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
@@ -267,7 +282,9 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-938
void detectsConstructorExpressionInDistinctQuery() {
StringQuery query = new StringQuery("select distinct new Foo() from Bar b", false);
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
}
@@ -286,19 +303,25 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-938
void detectsConstructorExpressionWithLineBreaks() {
StringQuery query = new StringQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false);
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
}
@Test // DATAJPA-960
void doesNotQualifySortIfNoAliasDetectedNonNative() {
StringQuery query = new StringQuery("from mytable where ?1 is null", false);
assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWith("order by firstname asc");
}
@Test // DATAJPA-960
void doesNotQualifySortIfNoAliasDetectedNative() {
StringQuery query = new StringQuery("Select * from mytable where ?1 is null", true);
endsIgnoringCase(getEnhancer(query).applySorting(Sort.by("firstname")), "order by firstname asc");
}
@@ -308,15 +331,17 @@ class QueryEnhancerUnitTests {
StringQuery query = new StringQuery("select p from Person p", true);
Sort sort = Sort.by("case when foo then bar");
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> getEnhancer(query).applySorting(sort, "p"));
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixUnsageJpaSortFunctionCalls() {
void doesNotPrefixUnsafeJpaSortFunctionCalls() {
JpaSort sort = JpaSort.unsafe("sum(foo)");
StringQuery query = new StringQuery("select p from Person p", true);
endsIgnoringCase(getEnhancer(query).applySorting(sort, "p"), "order by sum(foo) asc");
}
@@ -349,7 +374,7 @@ class QueryEnhancerUnitTests {
}
@Test // DATAJPA-965, DATAJPA-970
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainesAliasedFunctionForDifferentProperty() {
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
StringQuery query = new StringQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m", true);
Sort sort = Sort.by("name", "avgPrice");
@@ -464,11 +489,13 @@ class QueryEnhancerUnitTests {
@Test
void createCountQuerySupportsLineBreaksInSelectClause() {
StringQuery query = new StringQuery("select user.age,\n" + //
" user.name\n" + //
" from User user\n" + //
" where user.age = 18\n" + //
" order\nby\nuser.name\n ", true);
assertThat(getEnhancer(query).createCountQueryFor())
.isEqualToIgnoringCase("select count(user) from User user where user.age = 18");
}
@@ -562,12 +589,11 @@ class QueryEnhancerUnitTests {
@MethodSource("findProjectionClauseWithDistinctSource")
void findProjectionClauseWithDistinct(DeclaredQuery query, String expected) {
SoftAssertions.assertSoftly(sofly -> {
sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected);
});
SoftAssertions.assertSoftly(sofly -> sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected));
}
public static Stream<Arguments> findProjectionClauseWithDistinctSource() {
return Stream.of( //
Arguments.of(new StringQuery("select * from x", true), "*"), //
Arguments.of(new StringQuery("select a, b, c from x", true), "a, b, c"), //
@@ -591,17 +617,21 @@ class QueryEnhancerUnitTests {
// This is a required behavior the testcase in #findProjectionClauseWithSubselect tells why
String queryString = "select * from (select x from y)";
StringQuery query = new StringQuery(queryString, true);
assertThat(getEnhancer(query).getProjection()).isEqualTo("*");
}
@Test // DATAJPA-1696
void findProjectionClauseWithIncludedFrom() {
StringQuery query = new StringQuery("select x, frommage, y from t", true);
assertThat(getEnhancer(query).getProjection()).isEqualTo("x, frommage, y");
}
@Test
void countProjectionDistrinctQueryIncludesNewLineAfterFromAndBeforeJoin() {
void countProjectionDistinctQueryIncludesNewLineAfterFromAndBeforeJoin() {
StringQuery originalQuery = new StringQuery(
"SELECT DISTINCT entity1\nFROM Entity1 entity1\nLEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true);
@@ -611,17 +641,21 @@ class QueryEnhancerUnitTests {
@Test
void countProjectionDistinctQueryIncludesNewLineAfterEntity() {
StringQuery originalQuery = new StringQuery(
"SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true);
assertCountQuery(originalQuery,
"select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key");
}
@Test
void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() {
StringQuery originalQuery = new StringQuery(
"SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key\nwhere entity1.id = 1799",
true);
assertCountQuery(originalQuery,
"select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799");
}
@@ -651,6 +685,7 @@ class QueryEnhancerUnitTests {
@Test // GH-2441
void correctFunctionAliasWithComplexNestedFunctions() {
String queryString = "\nSELECT \nCAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\nFROM\ncity c";
StringQuery nativeQuery = new StringQuery(queryString, true);
@@ -661,9 +696,11 @@ class QueryEnhancerUnitTests {
@Test // GH-2441
void correctApplySortOnComplexNestedFunctionQuery() {
String queryString = "SELECT dd.institutesIds FROM (\n" + " SELECT\n"
String queryString = "SELECT dd.institutesIds FROM (\n" //
+ " SELECT\n" //
+ " CAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\n"
+ " FROM\n" + " city c\n"
+ " FROM\n" //
+ " city c\n" //
+ " ) dd";
StringQuery nativeQuery = new StringQuery(queryString, true);
@@ -671,10 +708,12 @@ class QueryEnhancerUnitTests {
QueryEnhancer queryEnhancer = getEnhancer(nativeQuery);
String result = queryEnhancer.applySorting(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds")));
assertThat(result).containsIgnoringCase("order by dd.institutesIds");
}
public static Stream<Arguments> detectsJoinAliasesCorrectlySource() {
return Stream.of( //
Arguments.of("select p from Person p left outer join x.foo b2_$ar", Collections.singletonList("b2_$ar")), //
Arguments.of("select p from Person p left join x.foo b2_$ar", Collections.singletonList("b2_$ar")), //
@@ -695,6 +734,7 @@ class QueryEnhancerUnitTests {
}
private static void endsIgnoringCase(String original, String endWithIgnoreCase) {
// https://github.com/assertj/assertj-core/pull/2451
// can be removed when upgrading to version 3.23.0 assertJ
assertThat(original.toUpperCase()).endsWith(endWithIgnoreCase.toUpperCase());
@@ -703,5 +743,4 @@ class QueryEnhancerUnitTests {
private static QueryEnhancer getEnhancer(DeclaredQuery query) {
return QueryEnhancerFactory.forQuery(query);
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindin
*
* @author Jens Schauder
* @author Mark Paluch
* @author Diego Krupitza
*/
class QueryParameterSetterFactoryUnitTests {

View File

@@ -46,7 +46,6 @@ import jakarta.persistence.spi.PersistenceProviderResolverHolder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.jpa.domain.sample.Category;
@@ -66,13 +65,13 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
* @author Sébastien Péralta
* @author Jens Schauder
* @author Patrice Blanchardie
* @author Diego Krupitza
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:infrastructure.xml")
public class QueryUtilsIntegrationTests {
@PersistenceContext
EntityManager em;
@PersistenceContext EntityManager em;
@Test // DATAJPA-403
void reusesExistingJoinForExpression() {
@@ -183,7 +182,7 @@ public class QueryUtilsIntegrationTests {
CriteriaQuery<InvoiceItem> query = builder.createQuery(InvoiceItem.class);
Root<InvoiceItem> root = query.from(InvoiceItem.class);
// given an existing inner join an nested optional
// given an existing inner join a nested optional
root.join("invoice").join("order");
QueryUtils.toExpressionRecursively(root, PropertyPath.from("invoice.order.customer.name", InvoiceItem.class),
@@ -237,7 +236,7 @@ public class QueryUtilsIntegrationTests {
}
@Test // DATAJPA-454
void createsJoingToTraverseCollectionPath() {
void createsJoinToTraverseCollectionPath() {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
@@ -319,7 +318,7 @@ public class QueryUtilsIntegrationTests {
* https://github.com/javaee/jpa-spec/issues/169 Compare to: {@link EclipseLinkQueryUtilsIntegrationTests}
*/
@Test // DATAJPA-1238
void demonstrateDifferentBehavorOfGetJoin() {
void demonstrateDifferentBehaviorOfGetJoin() {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
@@ -348,40 +347,32 @@ public class QueryUtilsIntegrationTests {
@SuppressWarnings("unused")
static class Merchant {
@Id
String id;
@OneToMany
Set<Employee> employees;
@Id String id;
@OneToMany Set<Employee> employees;
@OneToOne
Address address;
@OneToOne Address address;
}
@Entity
@SuppressWarnings("unused")
static class Address {
@Id
String id;
@OneToOne(mappedBy = "address")
Merchant merchant;
@Id String id;
@OneToOne(mappedBy = "address") Merchant merchant;
}
@Entity
@SuppressWarnings("unused")
static class Employee {
@Id
String id;
@OneToMany
Set<Credential> credentials;
@Id String id;
@OneToMany Set<Credential> credentials;
}
@Entity
@SuppressWarnings("unused")
static class Credential {
@Id
String id;
@Id String id;
String uid;
}
@@ -399,8 +390,7 @@ public class QueryUtilsIntegrationTests {
}
@Override
public void clearCachedProviders() {
}
public void clearCachedProviders() {}
}
}

View File

@@ -51,7 +51,7 @@ class QueryUtilsUnitTests {
private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
@Test
void createsCountQueryCorrectly() throws Exception {
void createsCountQueryCorrectly() {
assertCountQuery(QUERY, COUNT_QUERY);
}
@@ -64,47 +64,45 @@ class QueryUtilsUnitTests {
}
@Test
void createsCountQueryForDistinctQueries() throws Exception {
void createsCountQueryForDistinctQueries() {
assertCountQuery("select distinct u from User u where u.foo = ?",
"select count(distinct u) from User u where u.foo = ?");
}
@Test
void createsCountQueryForConstructorQueries() throws Exception {
void createsCountQueryForConstructorQueries() {
assertCountQuery("select distinct new User(u.name) from User u where u.foo = ?",
"select count(distinct u) from User u where u.foo = ?");
}
@Test
void createsCountQueryForJoins() throws Exception {
void createsCountQueryForJoins() {
assertCountQuery("select distinct new User(u.name) from User u left outer join u.roles r WHERE r = ?",
"select count(distinct u) from User u left outer join u.roles r WHERE r = ?");
}
@Test
void createsCountQueryForQueriesWithSubSelects() throws Exception {
void createsCountQueryForQueriesWithSubSelects() {
assertCountQuery("select u from User u left outer join u.roles r where r in (select r from Role)",
"select count(u) from User u left outer join u.roles r where r in (select r from Role)");
}
@Test
void createsCountQueryForAliasesCorrectly() throws Exception {
void createsCountQueryForAliasesCorrectly() {
assertCountQuery("select u from User as u", "select count(u) from User as u");
}
@Test
void allowsShortJpaSyntax() throws Exception {
void allowsShortJpaSyntax() {
assertCountQuery(SIMPLE_QUERY, COUNT_QUERY);
}
@Test
void detectsAliasCorrectly() throws Exception {
void detectsAliasCorrectly() {
assertThat(detectAlias(QUERY)).isEqualTo("u");
assertThat(detectAlias(SIMPLE_QUERY)).isEqualTo("u");
@@ -180,14 +178,14 @@ class QueryUtilsUnitTests {
}
@Test // DATAJPA-342
void usesReturnedVariableInCOuntProjectionIfSet() {
void usesReturnedVariableInCountProjectionIfSet() {
assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc",
"select count(distinct m.genre) from Media m where m.user = ?1");
}
@Test // DATAJPA-343
void projectsCOuntQueriesForQueriesWithSubselects() {
void projectsCountQueriesForQueriesWithSubselects() {
assertCountQuery("select o from Foo o where cb.id in (select b from Bar b)",
"select count(o) from Foo o where cb.id in (select b from Bar b)");
@@ -233,7 +231,7 @@ class QueryUtilsUnitTests {
}
@Test // DATAJPA-726
void detectsAliassesInPlainJoins() {
void detectsAliasesInPlainJoins() {
String query = "select p from Customer c join c.productOrder p where p.delayed = true";
Sort sort = Sort.by("p.lineItems");
@@ -294,7 +292,7 @@ class QueryUtilsUnitTests {
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixUnsageJpaSortFunctionCalls() {
void doesNotPrefixUnsafeJpaSortFunctionCalls() {
JpaSort sort = JpaSort.unsafe("sum(foo)");
assertThat(applySorting("select p from Person p", sort, "p")).endsWith("order by sum(foo) asc");
@@ -328,7 +326,7 @@ class QueryUtilsUnitTests {
}
@Test // DATAJPA-965, DATAJPA-970
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainesAliasedFunctionForDifferentProperty() {
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m";
Sort sort = Sort.by("name", "avgPrice");
@@ -400,8 +398,8 @@ class QueryUtilsUnitTests {
@Test // DATAJPA-1363
void discoversAliasWithComplexFunction() {
assertThat(QueryUtils
.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
assertThat(
QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) //
.contains("myAlias");
}

View File

@@ -23,7 +23,6 @@ import java.util.List;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.StringQuery.InParameterBinding;
import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;