Polishing.

Remove method overloads accepting pure strings. Use switch-expressions. Correctly navigate nested joins. Introduce PathExpression interface, refine naming.

See #3588
Original pull request: #3653
This commit is contained in:
Mark Paluch
2024-11-18 14:50:07 +01:00
parent b69ef2b121
commit 39e776cb6e
11 changed files with 498 additions and 491 deletions

View File

@@ -15,10 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.repository.query.parser.Part.Type.IS_NOT_EMPTY;
import static org.springframework.data.repository.query.parser.Part.Type.NOT_CONTAINING;
import static org.springframework.data.repository.query.parser.Part.Type.NOT_LIKE;
import static org.springframework.data.repository.query.parser.Part.Type.SIMPLE_PROPERTY;
import static org.springframework.data.repository.query.parser.Part.Type.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaQuery;
@@ -39,7 +36,6 @@ import java.util.stream.Collectors;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.mapping.PropertyPath;
@@ -183,8 +179,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
QueryUtils.checkSortExpression(order);
try {
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
PropertyPath.from(order.getProperty(), entityType.getJavaType())));
expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
PropertyPath.from(order.getProperty(), entityType.getJavaType()));
} catch (PropertyReferenceException e) {
if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
@@ -227,7 +223,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
requiredSelection = getRequiredSelection(sort, returnedType);
}
List<PathAndOrigin> paths = new ArrayList<>(requiredSelection.size());
List<JpqlQueryBuilder.PathExpression> paths = new ArrayList<>(requiredSelection.size());
for (String selection : requiredSelection) {
paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
PropertyPath.from(selection, returnedType.getDomainType()), true));
@@ -251,7 +247,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
} else {
List<PathAndOrigin> paths = entityType.getIdClassAttributes().stream()//
List<JpqlQueryBuilder.PathExpression> paths = entityType.getIdClassAttributes().stream()//
.map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
PropertyPath.from(it.getName(), returnedType.getDomainType()), true))
.toList();
@@ -320,7 +316,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
PropertyPath property = part.getProperty();
Type type = part.getType();
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas);
JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas));
@@ -385,7 +381,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull();
}
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata));
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(simple));
return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression);
case IS_EMPTY:
case IS_NOT_EMPTY:
@@ -420,8 +416,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
* @param path must not be {@literal null}.
* @return
*/
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) {
return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path));
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.PathExpression path) {
return potentiallyIgnoreCase(path.getPropertyPath(), path);
}
/**

View File

@@ -15,8 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_ASC;
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DESC;
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
import java.util.ArrayList;
import java.util.Arrays;
@@ -26,6 +25,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import org.springframework.data.domain.Sort;
@@ -121,12 +121,12 @@ public final class JpqlQueryBuilder {
}
@Override
public Select instantiate(String resultType, Collection<PathAndOrigin> paths) {
public Select instantiate(String resultType, Collection<JpqlQueryBuilder.PathExpression> paths) {
return new Select(postProcess(new ConstructorExpression(resultType, new Multiselect(from, paths))), from);
}
@Override
public Select select(Collection<PathAndOrigin> paths) {
public Select select(Collection<JpqlQueryBuilder.PathExpression> paths) {
return new Select(postProcess(new Multiselect(from, paths)), from);
}
@@ -177,22 +177,11 @@ public final class JpqlQueryBuilder {
* @return
*/
public static Expression expression(Origin source, PropertyPath path) {
return expression(new PathAndOrigin(path, source, false));
return new PathAndOrigin(path, source, false);
}
/**
* Create a qualified expression for a {@link PropertyPath}.
*
* @param source
* @param path
* @return
*/
public static Expression expression(PathAndOrigin pas) {
return new PathExpression(pas);
}
/**
* Create a simple expression from a string as is.
* Create a simple expression from a string as-is.
*
* @param expression
* @return
@@ -204,10 +193,32 @@ public final class JpqlQueryBuilder {
return new LiteralExpression(expression);
}
public static Expression stringLiteral(String literal) {
/**
* Create a simple numeric literal.
*
* @param literal
* @return
*/
public static Expression literal(Number literal) {
return new LiteralExpression(literal.toString());
}
/**
* Create a simple literal from a string by quoting it.
*
* @param literal
* @return
*/
public static Expression literal(String literal) {
return new StringLiteralExpression(literal);
}
/**
* A parameter placeholder.
*
* @param parameter
* @return
*/
public static Expression parameter(String parameter) {
Assert.hasText(parameter, "Parameter must not be empty or null");
@@ -215,10 +226,23 @@ public final class JpqlQueryBuilder {
return new ParameterExpression(new ParameterPlaceholder(parameter));
}
/**
* A parameter placeholder.
*
* @param placeholder the placeholder to use.
* @return
*/
public static Expression parameter(ParameterPlaceholder placeholder) {
return new ParameterExpression(placeholder);
}
/**
* Create a new ordering expression.
*
* @param sortExpression
* @param order
* @return
*/
public static Expression orderBy(Expression sortExpression, Sort.Order order) {
return new OrderExpression(sortExpression, order);
}
@@ -234,16 +258,6 @@ public final class JpqlQueryBuilder {
return where(expression(source, path));
}
/**
* Start building a {@link Predicate WHERE predicate} by providing the right-hand side.
*
* @param rhs
* @return
*/
public static WhereStep where(PathAndOrigin rhs) {
return where(expression(rhs));
}
/**
* Start building a {@link Predicate WHERE predicate} by providing the right-hand side.
*
@@ -318,16 +332,6 @@ public final class JpqlQueryBuilder {
return new InPredicate(rhs, "NOT IN", value);
}
@Override
public Predicate inMultivalued(Expression value) {
return new MemberOfPredicate(rhs, "IN", value); // TODO: that does not line up in my head - ahahah
}
@Override
public Predicate notInMultivalued(Expression value) {
return new MemberOfPredicate(rhs, "NOT IN", value);
}
@Override
public Predicate memberOf(Expression value) {
return new MemberOfPredicate(rhs, "MEMBER OF", value);
@@ -422,7 +426,7 @@ public final class JpqlQueryBuilder {
* @param paths
* @return
*/
default Select instantiate(Class<?> resultType, Collection<PathAndOrigin> paths) {
default Select instantiate(Class<?> resultType, Collection<JpqlQueryBuilder.PathExpression> paths) {
return instantiate(resultType.getName(), paths);
}
@@ -433,7 +437,7 @@ public final class JpqlQueryBuilder {
* @param paths
* @return
*/
Select instantiate(String resultType, Collection<PathAndOrigin> paths);
Select instantiate(String resultType, Collection<JpqlQueryBuilder.PathExpression> paths);
/**
* Specify a multi-select.
@@ -441,7 +445,7 @@ public final class JpqlQueryBuilder {
* @param paths
* @return
*/
Select select(Collection<PathAndOrigin> paths);
Select select(Collection<JpqlQueryBuilder.PathExpression> paths);
/**
* Select a single attribute.
@@ -449,7 +453,7 @@ public final class JpqlQueryBuilder {
* @param name
* @return
*/
default Select select(PathAndOrigin path) {
default Select select(JpqlQueryBuilder.PathExpression path) {
return select(List.of(path));
}
@@ -479,22 +483,22 @@ public final class JpqlQueryBuilder {
static PathAndOrigin path(Origin origin, String path) {
if(origin instanceof Entity entity) {
if (origin instanceof Entity entity) {
try {
try {
PropertyPath from = PropertyPath.from(path, ClassUtils.forName(entity.entity, Entity.class.getClassLoader()));
return new PathAndOrigin(from, entity, false);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
if(origin instanceof Join join) {
throw new RuntimeException(e);
}
}
if (origin instanceof Join join) {
Origin parent = join.source;
List<String> segments = new ArrayList<>();
segments.add(join.path);
while(!(parent instanceof Entity)) {
if(parent instanceof Join pj) {
while (!(parent instanceof Entity)) {
if (parent instanceof Join pj) {
parent = pj.source;
segments.add(pj.path);
} else {
@@ -502,7 +506,7 @@ public final class JpqlQueryBuilder {
}
}
if(parent instanceof Entity entity) {
if (parent instanceof Entity) {
Collections.reverse(segments);
segments.add(path);
PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, "."));
@@ -561,7 +565,6 @@ public final class JpqlQueryBuilder {
@Override
public String render(RenderContext context) {
return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context)));
}
@@ -577,22 +580,22 @@ public final class JpqlQueryBuilder {
* @param source
* @param paths
*/
record Multiselect(Origin source, Collection<PathAndOrigin> paths) implements Selection {
record Multiselect(Origin source, Collection<JpqlQueryBuilder.PathExpression> paths) implements Selection {
@Override
public String render(RenderContext context) {
StringBuilder builder = new StringBuilder();
for (PathAndOrigin path : paths) {
for (PathExpression path : paths) {
if (!builder.isEmpty()) {
builder.append(", ");
}
builder.append(PathExpression.render(path, context));
if(!context.isConstructorContext()) {
builder.append(" ").append(path.path().getSegment());
builder.append(path.render(context));
if (!context.isConstructorContext()) {
builder.append(" ").append(path.getPropertyPath().getSegment());
}
}
@@ -662,6 +665,18 @@ public final class JpqlQueryBuilder {
String render(RenderContext context);
}
/**
* Extension to {@link Expression} that contains a {@link PropertyPath}. Typically used to represent a selection
* expression or an expression used within sorting or {@code WHERE} clauses.
*/
public interface PathExpression extends Expression {
/**
* @return the associated {@link PropertyPath}.
*/
PropertyPath getPropertyPath();
}
/**
* {@code SELECT} statement.
*/
@@ -718,7 +733,7 @@ public final class JpqlQueryBuilder {
StringBuilder where = new StringBuilder();
StringBuilder orderby = new StringBuilder();
StringBuilder result = new StringBuilder(
"SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.entity(), entity.alias()));
"SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.getEntity(), entity.getAlias()));
if (getWhere() != null) {
where.append(" WHERE ").append(getWhere().render(renderContext));
@@ -874,32 +889,100 @@ public final class JpqlQueryBuilder {
*/
public interface Origin {
String getName(); // TODO: mainly used along records - shoule we call this just name()?
/**
* Returns the simple name of the origin (e.g. {@link Class#getSimpleName()} or JOIN path name).
*
* @return the simple name of the origin (e.g. {@link Class#getSimpleName()})
*/
String getName();
}
/**
* An origin that is used to select data from. selection origins are used with paths to define where a path is
* anchored.
*/
public interface Bindable {
boolean isRoot();
}
/**
* The root entity.
*
* @param entity
* @param simpleName
* @param alias
*/
public record Entity(String entity, String simpleName, String alias) implements Origin {
public static final class Entity implements Origin {
private final String entity;
private final String simpleName;
private final String alias;
/**
* @param entity fully-qualified entity name.
* @param simpleName simple class name.
* @param alias alias to use.
*/
Entity(String entity, String simpleName, String alias) {
this.entity = entity;
this.simpleName = simpleName;
this.alias = alias;
}
public String getEntity() {
return entity;
}
@Override
public String getName() {
return simpleName;
}
public String getAlias() {
return alias;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (Entity) obj;
return Objects.equals(this.entity, that.entity) && Objects.equals(this.simpleName, that.simpleName)
&& Objects.equals(this.alias, that.alias);
}
@Override
public int hashCode() {
return Objects.hash(entity, simpleName, alias);
}
@Override
public String toString() {
return "Entity[" + "entity=" + entity + ", " + "simpleName=" + simpleName + ", " + "alias=" + alias + ']';
}
}
/**
* A joined entity or element collection.
*
* @param source
* @param joinType
* @param path
*/
public record Join(Origin source, String joinType, String path) implements Origin, Expression {
public static final class Join implements Origin, Expression {
private final Origin source;
private final String joinType;
private final String path;
/**
* @param source
* @param joinType
* @param path
*/
Join(Origin source, String joinType, String path) {
this.source = source;
this.joinType = joinType;
this.path = path;
}
@Override
public String getName() {
@@ -908,8 +991,44 @@ public final class JpqlQueryBuilder {
@Override
public String render(RenderContext context) {
return "";
return "%s %s %s".formatted(joinType, context.getAlias(source), path);
}
public Origin source() {
return source;
}
public String joinType() {
return joinType;
}
public String path() {
return path;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (Join) obj;
return Objects.equals(this.source, that.source) && Objects.equals(this.joinType, that.joinType)
&& Objects.equals(this.path, that.path);
}
@Override
public int hashCode() {
return Objects.hash(source, joinType, path);
}
@Override
public String toString() {
return "Join[" + "source=" + source + ", " + "joinType=" + joinType + ", " + "path=" + path + ']';
}
}
/**
@@ -917,17 +1036,6 @@ public final class JpqlQueryBuilder {
*/
public interface WhereStep {
/**
* Create a {@code BETWEEN … AND …} predicate.
*
* @param lower lower boundary.
* @param upper upper boundary.
* @return
*/
default Predicate between(String lower, String upper) {
return between(expression(lower), expression(upper));
}
/**
* Create a {@code BETWEEN … AND …} predicate.
*
@@ -937,16 +1045,6 @@ public final class JpqlQueryBuilder {
*/
Predicate between(Expression lower, Expression upper);
/**
* Create a greater {@code > …} predicate.
*
* @param value the comparison value.
* @return
*/
default Predicate gt(String value) {
return gt(expression(value));
}
/**
* Create a greater {@code > …} predicate.
*
@@ -955,16 +1053,6 @@ public final class JpqlQueryBuilder {
*/
Predicate gt(Expression value);
/**
* Create a greater-or-equals {@code >= …} predicate.
*
* @param value the comparison value.
* @return
*/
default Predicate gte(String value) {
return gte(expression(value));
}
/**
* Create a greater-or-equals {@code >= …} predicate.
*
@@ -973,16 +1061,6 @@ public final class JpqlQueryBuilder {
*/
Predicate gte(Expression value);
/**
* Create a less {@code < …} predicate.
*
* @param value the comparison value.
* @return
*/
default Predicate lt(String value) {
return lt(expression(value));
}
/**
* Create a less {@code < …} predicate.
*
@@ -991,16 +1069,6 @@ public final class JpqlQueryBuilder {
*/
Predicate lt(Expression value);
/**
* Create a less-or-equals {@code <= …} predicate.
*
* @param value the comparison value.
* @return
*/
default Predicate lte(String value) {
return lte(expression(value));
}
/**
* Create a less-or-equals {@code <= …} predicate.
*
@@ -1009,102 +1077,117 @@ public final class JpqlQueryBuilder {
*/
Predicate lte(Expression value);
/**
* Create a {@code IS NULL} predicate.
*
* @return
*/
Predicate isNull();
/**
* Create a {@code IS NOT NULL} predicate.
*
* @return
*/
Predicate isNotNull();
/**
* Create a {@code IS TRUE} predicate.
*
* @return
*/
Predicate isTrue();
/**
* Create a {@code IS FALSE} predicate.
*
* @return
*/
Predicate isFalse();
/**
* Create a {@code IS EMPTY} predicate.
*
* @return
*/
Predicate isEmpty();
/**
* Create a {@code IS NOT EMPTY} predicate.
*
* @return
*/
Predicate isNotEmpty();
default Predicate in(String value) {
return in(expression(value));
}
/**
* Create a {@code IN} predicate.
*
* @param value
* @return
*/
Predicate in(Expression value);
default Predicate notIn(String value) {
return notIn(expression(value));
}
/**
* Create a {@code NOT IN} predicate.
*
* @param value
* @return
*/
Predicate notIn(Expression value);
default Predicate inMultivalued(String value) {
return inMultivalued(expression(value));
}
Predicate inMultivalued(Expression value);
default Predicate notInMultivalued(String value) {
return notInMultivalued(expression(value));
}
Predicate notInMultivalued(Expression value);
default Predicate memberOf(String value) {
return memberOf(expression(value));
}
/**
* Create a {@code MEMBER OF &lt;collection&gt;} predicate.
*
* @param value
* @return
*/
Predicate memberOf(Expression value);
default Predicate notMemberOf(String value) {
return notMemberOf(expression(value));
}
/**
* Create a {@code NOT MEMBER OF &lt;collection&gt;} predicate.
*
* @param value
* @return
*/
Predicate notMemberOf(Expression value);
default Predicate like(String value, String escape) {
return like(expression(value), escape);
}
/**
* Create a {@code LIKE … ESCAPE} predicate.
*
* @param value
* @return
*/
Predicate like(Expression value, String escape);
default Predicate notLike(String value, String escape) {
return notLike(expression(value), escape);
}
/**
* Create a {@code NOT LIKE … ESCAPE} predicate.
*
* @param value
* @return
*/
Predicate notLike(Expression value, String escape);
default Predicate eq(String value) {
return eq(expression(value));
}
/**
* Create a {@code =} (equals) predicate.
*
* @param value
* @return
*/
Predicate eq(Expression value);
default Predicate neq(String value) {
return neq(expression(value));
}
/**
* Create a {@code &lt;&gt;} (not equals) predicate.
*
* @param value
* @return
*/
Predicate neq(Expression value);
}
record PathExpression(PathAndOrigin pas) implements Expression {
@Override
public String render(RenderContext context) {
return render(pas, context);
}
public static String render(PathAndOrigin pas, RenderContext context) {
if (pas.path().hasNext() || !pas.onTheJoin()) {
return context.prefixWithAlias(pas.origin(), pas.path().toDotPath());
} else {
return context.getAlias(pas.origin());
}
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record LiteralExpression(String expression) implements Expression {
@Override
@@ -1243,7 +1326,7 @@ public final class JpqlQueryBuilder {
@Override
public String render(RenderContext context) {
//TODO: should we rather wrap it with nested or check if its a nested predicate before we call render
// TODO: should we rather wrap it with nested or check if its a nested predicate before we call render
return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context));
}
@@ -1299,20 +1382,51 @@ public final class JpqlQueryBuilder {
* @param origin
* @param onTheJoin whether the path should target the join itself instead of matching {@link PropertyPath}.
*/
public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) {
record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) implements PathExpression {
@Override
public PropertyPath getPropertyPath() {
return path;
}
@Override
public String render(RenderContext context) {
if (path().hasNext() || !onTheJoin()) {
return context.prefixWithAlias(origin(), path().toDotPath());
} else {
return context.getAlias(origin());
}
}
}
/**
* Value object capturing parameter placeholder.
*
* @param placeholder
*/
public record ParameterPlaceholder(String placeholder) {
public ParameterPlaceholder {
Assert.hasText(placeholder, "Placeholder must not be null nor empty");
}
/**
* Factory method to create a parameter placeholder using a parameter {@code index}.
*
* @param index the parameter index.
* @return an indexed parameter placeholder.
*/
public static ParameterPlaceholder indexed(int index) {
return new ParameterPlaceholder("?%s".formatted(index));
}
/**
* Factory method to create a parameter placeholder using a parameter {@code name}.
*
* @param name the parameter name.
* @return a named parameter placeholder.
*/
public static ParameterPlaceholder named(String name) {
Assert.hasText(name, "Placeholder name must not be empty");

View File

@@ -15,34 +15,16 @@
*/
package org.springframework.data.jpa.repository.query;
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ELEMENT_COLLECTION;
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_MANY;
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE;
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_MANY;
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SingularAttribute;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
@@ -52,25 +34,12 @@ import org.springframework.util.StringUtils;
*/
class JpqlUtils {
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
static {
Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
persistentAttributeTypes.put(ONE_TO_MANY, null);
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
persistentAttributeTypes.put(MANY_TO_MANY, null);
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
}
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property) {
return toExpressionRecursively(metamodel, source, from, property, false);
}
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property, boolean isForSelection) {
return toExpressionRecursively(metamodel, source, from, property, isForSelection, false);
}
@@ -84,16 +53,13 @@ class JpqlUtils {
* @param hasRequiredOuterJoin has a parent already required an outer join?
* @return the expression
*/
@SuppressWarnings("unchecked")
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
String segment = property.getSegment();
boolean isLeafProperty = !property.hasNext();
boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection,
hasRequiredOuterJoin);
boolean requiresOuterJoin = requiresOuterJoin(metamodel, from, property, isForSelection, hasRequiredOuterJoin);
// if it does not require an outer join and is a leaf, simply get the segment
if (!requiresOuterJoin && isLeafProperty) {
@@ -103,10 +69,7 @@ class JpqlUtils {
// get or create the join
JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment)
: JpqlQueryBuilder.innerJoin(source, segment);
// JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER;
// Join<?, ?> join = QueryUtils.getOrCreateJoin(from, segment, joinType);
//
// if it's a leaf, return the join
if (isLeafProperty) {
return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true);
@@ -114,11 +77,11 @@ class JpqlUtils {
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
// ManagedType<?> managedType = ;
Bindable<?> managedTypeForModel = (Bindable<?>) getManagedTypeForModel(from);
// Attribute<?, ?> joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null);
// recurse with the next property
return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin);
ManagedType<?> managedTypeForModel = QueryUtils.getManagedTypeForModel(from);
Attribute<?, ?> nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from);
return toExpressionRecursively(metamodel, joinSource, (Bindable<?>) nextAttribute, nextProperty, isForSelection,
requiresOuterJoin);
}
/**
@@ -127,17 +90,16 @@ class JpqlUtils {
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999)
*
* @param metamodel
* @param source
* @param bindable
* @param propertyPath
* @param isForSelection
* @param hasRequiredOuterJoin
* @return
*/
static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable<?> bindable,
PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) {
static boolean requiresOuterJoin(Metamodel metamodel, Bindable<?> bindable, PropertyPath propertyPath,
boolean isForSelection, boolean hasRequiredOuterJoin) {
ManagedType<?> managedType = getManagedTypeForModel(bindable);
ManagedType<?> managedType = QueryUtils.getManagedTypeForModel(bindable);
Attribute<?, ?> attribute = getModelForPath(metamodel, propertyPath, managedType, bindable);
boolean isPluralAttribute = bindable instanceof PluralAttribute;
@@ -145,7 +107,7 @@ class JpqlUtils {
return isPluralAttribute;
}
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
if (!QueryUtils.ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
return false;
}
@@ -155,47 +117,14 @@ class JpqlUtils {
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
&& StringUtils.hasText(QueryUtils.getAnnotationProperty(attribute, "mappedBy", ""));
boolean isLeafProperty = !propertyPath.hasNext();
if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) {
return false;
}
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());
if (associationAnnotation == null) {
return defaultValue;
}
Member member = attribute.getJavaMember();
if (!(member instanceof AnnotatedElement annotatedMember)) {
return defaultValue;
}
Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation);
return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
}
@Nullable
private static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
if (model instanceof ManagedType<?> managedType) {
return managedType;
}
if (!(model instanceof SingularAttribute<?, ?> singularAttribute)) {
return null;
}
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
return hasRequiredOuterJoin || QueryUtils.getAnnotationProperty(attribute, "optional", true);
}
@Nullable

View File

@@ -21,11 +21,11 @@ import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.Metamodel;
import java.util.List;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.Metamodel;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
@@ -147,7 +147,7 @@ public record KeysetScrollSpecification<T>(KeysetScrollPosition position, Sort s
public JpqlQueryBuilder.Expression createExpression(String property) {
PropertyPath path = PropertyPath.from(property, from.getBindableJavaType());
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path));
return JpqlUtils.toExpressionRecursively(metamodel, entity, from, path);
}
@Override

View File

@@ -242,17 +242,12 @@ class ParameterBinding {
if (String.class.equals(parameterType) && !noWildcards) {
switch (type) {
case STARTING_WITH:
return String.format("%s%%", escape.escape(value.toString()));
case ENDING_WITH:
return String.format("%%%s", escape.escape(value.toString()));
case CONTAINING:
case NOT_CONTAINING:
return String.format("%%%s%%", escape.escape(value.toString()));
default:
return value;
}
return switch (type) {
case STARTING_WITH -> String.format("%s%%", escape.escape(value.toString()));
case ENDING_WITH -> String.format("%%%s", escape.escape(value.toString()));
case CONTAINING, NOT_CONTAINING -> String.format("%%%s%%", escape.escape(value.toString()));
default -> value;
};
}
return Collection.class.isAssignableFrom(parameterType) //
@@ -710,7 +705,7 @@ class ParameterBinding {
boolean isExpression();
/**
* @return {@code true} if the origin is an expression.
* @return {@code true} if the origin is synthetic (contributed by e.g. KeysetPagination)
*/
boolean isSynthetic();
}

View File

@@ -22,9 +22,10 @@ import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
@@ -57,6 +58,7 @@ import org.springframework.util.Assert;
*/
public class PartTreeJpaQuery extends AbstractJpaQuery {
private static final Logger log = LoggerFactory.getLogger(PartTreeJpaQuery.class);
private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
private final PartTree tree;
@@ -201,7 +203,6 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
return type == Type.IN || type == Type.NOT_IN;
}
/**
* Query preparer to create {@link CriteriaQuery} instances and potentially cache them.
*
@@ -222,6 +223,11 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
String jpql = creator.createQuery(sort);
Query query;
if (log.isDebugEnabled()) {
log.debug(String.format("%s: Derived query for query method [%s]: '%s'", getClass().getSimpleName(),
getQueryMethod(), jpql));
}
try {
query = creator.useTupleQuery() ? em.createQuery(jpql, Tuple.class) : em.createQuery(jpql);
} catch (Exception e) {
@@ -273,11 +279,14 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
JpqlQueryCreator jpqlQueryCreator;
synchronized (cache) {
JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties
if (jpqlQueryCreator != null) {
return jpqlQueryCreator;
}
jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for
// simple properties
}
if (jpqlQueryCreator != null) {
return jpqlQueryCreator;
}
EntityManager entityManager = getEntityManager();

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.HashMap;
import java.util.BitSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@@ -25,11 +25,13 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* Cache for PartTree queries.
*
* @author Christoph Strobl
*/
class PartTreeQueryCache {
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<CacheKey, JpqlQueryCreator>() {
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<CacheKey, JpqlQueryCreator> eldest) {
return size() > 256;
@@ -49,9 +51,14 @@ class PartTreeQueryCache {
static class CacheKey {
private final Sort sort;
private final Map<Integer, Nulled> params;
public CacheKey(Sort sort, Map<Integer, Nulled> params) {
/**
* Bitset of null/non-null parameter values. A 0 bit means the parameter value is {@code null}, a 1 bit means the
* parameter is not {@code null}.
*/
private final BitSet params;
public CacheKey(Sort sort, BitSet params) {
this.sort = sort;
this.params = params;
}
@@ -59,20 +66,22 @@ class PartTreeQueryCache {
static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) {
Object[] values = accessor.getValues();
if (ObjectUtils.isEmpty(values)) {
return new CacheKey(sort, Map.of());
return new CacheKey(sort, new BitSet());
}
return new CacheKey(sort, toNullableMap(values));
}
static Map<Integer, Nulled> toNullableMap(Object[] args) {
static BitSet toNullableMap(Object[] args) {
Map<Integer, Nulled> paramMap = new HashMap<>(args.length);
BitSet bitSet = new BitSet(args.length);
for (int i = 0; i < args.length; i++) {
paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES);
bitSet.set(i, args[i] != null);
}
return paramMap;
return bitSet;
}
@Override
@@ -93,8 +102,4 @@ class PartTreeQueryCache {
}
}
enum Nulled {
YES, NO
}
}

View File

@@ -130,7 +130,7 @@ public abstract class QueryUtils {
private static final Pattern CONSTRUCTOR_EXPRESSION;
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
private static final int VARIABLE_NAME_GROUP_INDEX = 4;
@@ -844,8 +844,7 @@ public abstract class QueryUtils {
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
}
@Nullable
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
@@ -974,7 +973,7 @@ public abstract class QueryUtils {
* @return
*/
@Nullable
private static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
if (model instanceof ManagedType<?> managedType) {
return managedType;

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.EntityManager;
@@ -38,6 +37,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.FieldSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
@@ -52,6 +52,8 @@ import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
/**
* Unit tests for {@link JpaQueryCreator}.
*
* @author Christoph Strobl
*/
class JpaQueryCreatorTests {
@@ -61,7 +63,7 @@ class JpaQueryCreatorTests {
static List<JpqlQueryTemplates> ignoreCaseTemplates = List.of(JpqlQueryTemplates.LOWER, JpqlQueryTemplates.UPPER);
@Test
@Test // GH-3588
void simpleProperty() {
queryCreator(ORDER) //
@@ -72,7 +74,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void simpleNullProperty() {
queryCreator(ORDER) //
@@ -83,7 +85,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void negatingSimpleProperty() {
queryCreator(ORDER) //
@@ -94,7 +96,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void negatingSimpleNullProperty() {
queryCreator(ORDER) //
@@ -105,7 +107,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void simpleAnd() {
queryCreator(ORDER) //
@@ -116,7 +118,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void simpleOr() {
queryCreator(ORDER) //
@@ -127,7 +129,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void simpleAndOr() {
queryCreator(ORDER) //
@@ -139,7 +141,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void distinct() {
queryCreator(ORDER) //
@@ -150,7 +152,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void count() {
queryCreator(ORDER) //
@@ -162,7 +164,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void countWithJoins() {
queryCreator(ORDER) //
@@ -174,7 +176,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void countDistinct() {
queryCreator(ORDER) //
@@ -186,7 +188,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void simplePropertyIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -200,7 +202,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void simplePropertyAllIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -216,7 +218,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void simplePropertyMixedCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -231,7 +233,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void lessThan() {
queryCreator(ORDER) //
@@ -242,7 +244,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void lessThanEqual() {
queryCreator(ORDER) //
@@ -253,7 +255,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void greaterThan() {
queryCreator(ORDER) //
@@ -264,7 +266,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void before() {
queryCreator(ORDER) //
@@ -275,7 +277,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void after() {
queryCreator(ORDER) //
@@ -286,7 +288,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void between() {
queryCreator(ORDER) //
@@ -297,7 +299,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void isNull() {
queryCreator(ORDER) //
@@ -307,7 +309,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void isNotNull() {
queryCreator(ORDER) //
@@ -317,7 +319,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" })
void like(String parameterValue) {
@@ -330,7 +332,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void containingString() {
queryCreator(ORDER) //
@@ -342,7 +344,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void notContainingString() {
queryCreator(ORDER) //
@@ -354,7 +356,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void in() {
queryCreator(ORDER) //
@@ -366,7 +368,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void notIn() {
queryCreator(ORDER) //
@@ -378,7 +380,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void containingSingleEntryElementCollection() {
queryCreator(ORDER) //
@@ -389,7 +391,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void notContainingSingleEntryElementCollection() {
queryCreator(ORDER) //
@@ -400,7 +402,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void likeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -415,7 +417,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" })
void notLike(String parameterValue) {
@@ -428,7 +430,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void notLikeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -443,7 +445,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void startingWith() {
queryCreator(ORDER) //
@@ -455,7 +457,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void startingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -470,7 +472,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void endingWith() {
queryCreator(ORDER) //
@@ -482,7 +484,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void endingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) {
@@ -497,7 +499,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void greaterThanEqual() {
queryCreator(ORDER) //
@@ -508,7 +510,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void isTrue() {
queryCreator(ORDER) //
@@ -518,7 +520,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void isFalse() {
queryCreator(ORDER) //
@@ -528,7 +530,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void empty() {
queryCreator(ORDER) //
@@ -538,7 +540,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void notEmpty() {
queryCreator(ORDER) //
@@ -548,7 +550,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void sortBySingle() {
queryCreator(ORDER) //
@@ -559,7 +561,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void sortByMulti() {
queryCreator(ORDER) //
@@ -571,7 +573,7 @@ class JpaQueryCreatorTests {
}
@Disabled("should we support this?")
@ParameterizedTest
@ParameterizedTest // GH-3588
@FieldSource("ignoreCaseTemplates")
void sortBySingleIngoreCase(JpqlQueryTemplates ingoreCase) {
@@ -583,7 +585,7 @@ class JpaQueryCreatorTests {
ingoreCase.getIgnoreCaseOperator());
}
@Test
@Test // GH-3588
void matchSimpleJoin() {
queryCreator(ORDER) //
@@ -594,19 +596,19 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void matchSimpleNestedJoin() {
queryCreator(ORDER) //
.forTree(Order.class, "findOrderByLineItemsProductNameIs") //
.withParameters("spring") //
.as(QueryCreatorTester::create) //
.expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1",
.expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE p.name = ?1",
Order.class.getName()) //
.validateQuery();
}
@Test
@Test // GH-3588
void matchMultiOnNestedJoin() {
queryCreator(ORDER) //
@@ -614,12 +616,12 @@ class JpaQueryCreatorTests {
.withParameters(10, "spring") //
.as(QueryCreatorTester::create) //
.expectJpql(
"SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE l.quantity > ?1 AND p.name = ?2",
"SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE l.quantity > ?1 AND p.name = ?2",
Order.class.getName()) //
.validateQuery();
}
@Test
@Test // GH-3588
void matchSameEntityMultipleTimes() {
queryCreator(ORDER) //
@@ -627,12 +629,12 @@ class JpaQueryCreatorTests {
.withParameters("spring", "sukrauq") //
.as(QueryCreatorTester::create) //
.expectJpql(
"SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1 AND p.name != ?2",
"SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE p.name = ?1 AND p.name != ?2",
Order.class.getName()) //
.validateQuery();
}
@Test
@Test // GH-3588
void matchSameEntityMultipleTimesViaDifferentProperties() {
queryCreator(ORDER) //
@@ -640,12 +642,12 @@ class JpaQueryCreatorTests {
.withParameters(10, "spring") //
.as(QueryCreatorTester::create) //
.expectJpql(
"SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p INNER JOIN l.product2 join_0 WHERE p.name = ?1 AND join_0.name = ?2",
"SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p LEFT JOIN l.product2 join_0 WHERE p.name = ?1 AND join_0.name = ?2",
Order.class.getName()) //
.validateQuery();
}
@Test
@Test // GH-3588
void dtoProjection() {
queryCreator(ORDER) //
@@ -658,7 +660,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void interfaceProjection() {
queryCreator(ORDER) //
@@ -671,7 +673,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@ValueSource(classes = { Tuple.class, Map.class })
void tupleProjection(Class<?> resultType) {
@@ -685,7 +687,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@ParameterizedTest
@ParameterizedTest // GH-3588
@ValueSource(classes = { Long.class, List.class, Person.class })
void delete(Class<?> resultType) {
@@ -698,7 +700,7 @@ class JpaQueryCreatorTests {
.validateQuery();
}
@Test
@Test // GH-3588
void exists() {
queryCreator(PERSON) //

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.jpa.repository.query.JpqlQueryBuilder.*;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
@@ -28,26 +28,15 @@ import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.AbstractJpqlQuery;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Entity;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Expression;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Join;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.OrderExpression;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Predicate;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.RenderContext;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.SelectStep;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.WhereStep;
/**
* Unit tests for {@link JpqlQueryBuilder}.
*
* @author Christoph Strobl
*/
class JpqlQueryBuilderUnitTests {
@Test
@Test // GH-3588
void placeholdersRenderCorrectly() {
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1");
@@ -56,89 +45,88 @@ class JpqlQueryBuilderUnitTests {
assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1");
}
@Test
void placeholdersErrorOnInvaludInput() {
@Test // GH-3588
void placeholdersErrorOnInvalidInput() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> JpqlQueryBuilder.parameter((String) null));
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter(""));
}
@Test
@Test // GH-3588
void stringLiteralRendersAsQuotedString() {
assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
assertThat(literal("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
/* JPA Spec - 4.6.1 Literals:
> A string literal that includes a single quote is represented by two single quotes--for example: 'literal''s'. */
assertThat(JpqlQueryBuilder.stringLiteral("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'");
assertThat(literal("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'");
}
@Test
@Test // GH-3588
void entity() {
Entity entity = JpqlQueryBuilder.entity(Order.class);
assertThat(entity.alias()).isEqualTo("o");
assertThat(entity.entity()).isEqualTo(Order.class.getName());
assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); // TODO: this really confusing
assertThat(entity.simpleName()).isEqualTo(Order.class.getSimpleName());
assertThat(entity.getAlias()).isEqualTo("o");
assertThat(entity.getEntity()).isEqualTo(Order.class.getName());
assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName());
}
@Test
@Test // GH-3588
void literalExpressionRendersAsIs() {
Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, , , person.firstName))");
Expression expression = expression("CONCAT(person.lastName, , , person.firstName))");
assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, , , person.firstName))");
}
@Test
@Test // GH-3588
void xxx() {
Entity entity = JpqlQueryBuilder.entity(Order.class);
PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date");
String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity));
String fragment = JpqlQueryBuilder.where(orderDate).eq(expression("{d '2024-11-05'}")).render(ctx(entity));
assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}");
// JpqlQueryBuilder.where(PathAndOrigin)
}
@Test
@Test // GH-3588
void predicateRendering() {
Entity entity = JpqlQueryBuilder.entity(Order.class);
WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country"));
RenderContext context = ctx(entity);
assertThat(where.between(expression("'AT'"), expression("'DE'")).render(context))
.isEqualTo("o.country BETWEEN 'AT' AND 'DE'");
assertThat(where.eq(expression("'AT'")).render(context)).isEqualTo("o.country = 'AT'");
assertThat(where.eq(literal("AT")).render(context)).isEqualTo("o.country = 'AT'");
assertThat(where.gt(expression("'AT'")).render(context)).isEqualTo("o.country > 'AT'");
assertThat(where.gte(expression("'AT'")).render(context)).isEqualTo("o.country >= 'AT'");
assertThat(where.between("'AT'", "'DE'").render(ctx(entity))).isEqualTo("o.country BETWEEN 'AT' AND 'DE'");
assertThat(where.eq("'AT'").render(ctx(entity))).isEqualTo("o.country = 'AT'");
assertThat(where.eq(JpqlQueryBuilder.stringLiteral("AT")).render(ctx(entity))).isEqualTo("o.country = 'AT'");
assertThat(where.gt("'AT'").render(ctx(entity))).isEqualTo("o.country > 'AT'");
assertThat(where.gte("'AT'").render(ctx(entity))).isEqualTo("o.country >= 'AT'");
// TODO: that is really really bad
// lange namen
assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')");
assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')");
// 1 in age - cleanup what is not used - remove everything eles
// assertThat(where.inMultivalued("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); //
assertThat(where.isEmpty().render(ctx(entity))).isEqualTo("o.country IS EMPTY");
assertThat(where.isNotEmpty().render(ctx(entity))).isEqualTo("o.country IS NOT EMPTY");
assertThat(where.isTrue().render(ctx(entity))).isEqualTo("o.country = TRUE");
assertThat(where.isFalse().render(ctx(entity))).isEqualTo("o.country = FALSE");
assertThat(where.isNull().render(ctx(entity))).isEqualTo("o.country IS NULL");
assertThat(where.isNotNull().render(ctx(entity))).isEqualTo("o.country IS NOT NULL");
assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
assertThat(where.isEmpty().render(context)).isEqualTo("o.country IS EMPTY");
assertThat(where.isNotEmpty().render(context)).isEqualTo("o.country IS NOT EMPTY");
assertThat(where.isTrue().render(context)).isEqualTo("o.country = TRUE");
assertThat(where.isFalse().render(context)).isEqualTo("o.country = FALSE");
assertThat(where.isNull().render(context)).isEqualTo("o.country IS NULL");
assertThat(where.isNotNull().render(context)).isEqualTo("o.country IS NOT NULL");
assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context))
.isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'");
assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
assertThat(where.notLike(expression("'\\_%'"), "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context))
.isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'");
assertThat(where.lt("'AT'").render(ctx(entity))).isEqualTo("o.country < 'AT'");
assertThat(where.lte("'AT'").render(ctx(entity))).isEqualTo("o.country <= 'AT'");
assertThat(where.memberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' MEMBER OF o.country");
assertThat(where.lt(expression("'AT'")).render(context)).isEqualTo("o.country < 'AT'");
assertThat(where.lte(expression("'AT'")).render(context)).isEqualTo("o.country <= 'AT'");
assertThat(where.memberOf(expression("'AT'")).render(context)).isEqualTo("'AT' MEMBER OF o.country");
// TODO: can we have this where.value(foo).memberOf(pathAndOrigin);
assertThat(where.notMemberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' NOT MEMBER OF o.country");
assertThat(where.neq("'AT'").render(ctx(entity))).isEqualTo("o.country != 'AT'");
assertThat(where.notMemberOf(expression("'AT'")).render(context)).isEqualTo("'AT' NOT MEMBER OF o.country");
assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'");
}
@Test
@Test // GH-3588
void selectRendering() {
// make sure things are immutable
@@ -147,25 +135,12 @@ class JpqlQueryBuilderUnitTests {
assertThat(select.count().render()).startsWith("SELECT COUNT(o)");
assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o ");
assertThat(select.distinct().count().render()).startsWith("SELECT COUNT(DISTINCT o) ");
assertThat(JpqlQueryBuilder.selectFrom(Order.class).select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render())
.startsWith("SELECT o.country ");
assertThat(JpqlQueryBuilder.selectFrom(Order.class)
.select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render())
.startsWith("SELECT o.country ");
}
// @Test
// void sorting() {
//
// JpqlQueryBuilder.orderBy(new OrderExpression() , Sort.Order.asc("country"));
//
// Entity entity = JpqlQueryBuilder.entity(Order.class);
//
// AbstractJpqlQuery query = JpqlQueryBuilder.selectFrom(Order.class)
// .entity()
// .orderBy()
// .where(context -> "1 = 1");
//
// }
@Test
@Test // GH-3588
void joins() {
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
@@ -175,14 +150,14 @@ class JpqlQueryBuilderUnitTests {
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name");
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity));
String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30"))
.and(JpqlQueryBuilder.where(personName).eq(literal("ex40"))).render(ctx(entity));
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'");
}
@Test
void x2() {
@Test // GH-3588
void joinOnPaths() {
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
@@ -191,36 +166,17 @@ class JpqlQueryBuilderUnitTests {
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
}
@Test
void x3() {
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person");
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
// JpqlQueryBuilder.and("x = y", "a = b"); -> x = y AND a = b
// JpqlQueryBuilder.nested(JpqlQueryBuilder.and("x = y", "a = b")) (x = y AND a = b)
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30"))
.and(JpqlQueryBuilder.where(personName).eq(literal("cstrobl"))).render(ctx(entity));
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
}
static RenderContext ctx(Entity... entities) {
Map<Origin, String> aliases = new LinkedHashMap<>(entities.length);
for (Entity entity : entities) {
aliases.put(entity, entity.alias());
aliases.put(entity, entity.getAlias());
}
return new RenderContext(aliases);

View File

@@ -48,24 +48,26 @@ import org.springframework.test.util.ReflectionTestUtils;
class ParameterMetadataProviderIntegrationTests {
@PersistenceContext EntityManager em;
/* TODO
@Test // DATAJPA-758
void forwardsParameterNameIfTransparentlyNamed() throws Exception {
void usesIndexedParametersForExplicityNamedParameters() throws Exception {
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class));
ParameterMetadata<Object> metadata = provider.next(new Part("firstname", User.class));
ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("firstname", User.class));
assertThat(metadata.getName()).isEqualTo("name");
assertThat(metadata.getName()).isNull();
assertThat(metadata.getPosition()).isEqualTo(1);
}
@Test // DATAJPA-758
void forwardsParameterNameIfExplicitlyAnnotated() throws Exception {
void usesIndexedParameters() throws Exception {
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByLastname", String.class));
ParameterMetadata<Object> metadata = provider.next(new Part("lastname", User.class));
ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("lastname", User.class));
assertThat(metadata.getExpression().getName()).isNull();
} */
assertThat(metadata.getName()).isNull();
assertThat(metadata.getPosition()).isEqualTo(1);
}
@Test // DATAJPA-772
void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception {