diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index ec3739b3c..12073a595 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -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 paths = new ArrayList<>(requiredSelection.size()); + List 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 paths = entityType.getIdClassAttributes().stream()// + List 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 JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) { - return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path)); + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.PathExpression path) { + return potentiallyIgnoreCase(path.getPropertyPath(), path); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index cb53998c3..db6697a9d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -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 paths) { + public Select instantiate(String resultType, Collection paths) { return new Select(postProcess(new ConstructorExpression(resultType, new Multiselect(from, paths))), from); } @Override - public Select select(Collection paths) { + public Select select(Collection 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 paths) { + default Select instantiate(Class resultType, Collection paths) { return instantiate(resultType.getName(), paths); } @@ -433,7 +437,7 @@ public final class JpqlQueryBuilder { * @param paths * @return */ - Select instantiate(String resultType, Collection paths); + Select instantiate(String resultType, Collection paths); /** * Specify a multi-select. @@ -441,7 +445,7 @@ public final class JpqlQueryBuilder { * @param paths * @return */ - Select select(Collection paths); + Select select(Collection 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 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 paths) implements Selection { + record Multiselect(Origin source, Collection 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 <collection>} predicate. + * + * @param value + * @return + */ Predicate memberOf(Expression value); - default Predicate notMemberOf(String value) { - return notMemberOf(expression(value)); - } - + /** + * Create a {@code NOT MEMBER OF <collection>} 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 <>} (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"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index d3b32380c..500a7d4e8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -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> ASSOCIATION_TYPES; - - static { - Map> 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 getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { - - Class 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 diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 844de6059..9ef9d4e79 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -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(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 diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index f8c567f35..922719633 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -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(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 806d37953..dfde858df 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -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(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java index 71f952c2c..51183f4c6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java @@ -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 cache = new LinkedHashMap() { + private final Map cache = new LinkedHashMap<>() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 256; @@ -49,9 +51,14 @@ class PartTreeQueryCache { static class CacheKey { private final Sort sort; - private final Map params; - public CacheKey(Sort sort, Map 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 toNullableMap(Object[] args) { + static BitSet toNullableMap(Object[] args) { - Map 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 - } - } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index c75137267..71919e5ff 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -130,7 +130,7 @@ public abstract class QueryUtils { private static final Pattern CONSTRUCTOR_EXPRESSION; - private static final Map> ASSOCIATION_TYPES; + static final Map> 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 getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { + static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { Class 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; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java index dc2866fa8..9073848ff 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java @@ -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 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) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java index 04fb7079d..114671305 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java @@ -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 aliases = new LinkedHashMap<>(entities.length); for (Entity entity : entities) { - aliases.put(entity, entity.alias()); + aliases.put(entity, entity.getAlias()); } return new RenderContext(aliases); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index 355a34aff..beb8e68a7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -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 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 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 {