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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 <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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) //
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user