Replace derived CriteriaQuery with String-based queries.

Introduce new DSL to construct JPQL queries. Refactor ParameterMetadata to PartTreeParameterBinding.

Disable Keyset pagination with projections for Eclipselink as Eclipselink doesn't consider type hints for JPQL queries.

Closes #3588
Original pull request: #3653
This commit is contained in:
Mark Paluch
2024-08-23 10:23:22 +02:00
parent 7f97137ca0
commit d131eb7a4a
36 changed files with 2567 additions and 785 deletions

View File

@@ -242,8 +242,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return lockModeType == null ? query : query.setLockMode(lockModeType);
}
protected ParameterBinder createBinder() {
return ParameterBinderFactory.createBinder(getQueryMethod().getParameters());
ParameterBinder createBinder() {
return ParameterBinderFactory.createBinder(getQueryMethod().getParameters(), false);
}
protected Query createQuery(JpaParametersParameterAccessor parameters) {

View File

@@ -57,7 +57,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
private final Map<Class<?>, Boolean> knownProjections = new ConcurrentHashMap<>();
private final Lazy<DeclaredQuery> countQuery;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
private final QueryRewriter queryRewriter;
private final QuerySortRewriter querySortRewriter;
private final Lazy<ParameterBinder> countParameterBinder;
@@ -130,11 +129,9 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
String sortedQueryString = getSortedQueryString(sort, returnedType);
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType);
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
// it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the
// parameters in the query do not change.
return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
return parameterBinder.get().bindAndPrepare(query, accessor);
}
/**
@@ -238,9 +235,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
? em.createNativeQuery(queryStringToUse) //
: em.createQuery(queryStringToUse, Long.class);
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryString, query);
countParameterBinder.get().bind(metadata.withQuery(query), accessor, QueryParameterSetter.ErrorHandling.LENIENT);
countParameterBinder.get().bind(new QueryParameterSetter.BindableQuery(query), accessor,
QueryParameterSetter.ErrorHandling.LENIENT);
return query;
}

View File

@@ -51,7 +51,7 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce
* @param values must not be {@literal null}.
* @param em must not be {@literal null}.
*/
HibernateJpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values, EntityManager em) {
HibernateJpaParametersParameterAccessor(JpaParameters parameters, Object[] values, EntityManager em) {
super(parameters, values);

View File

@@ -15,16 +15,12 @@
*/
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.lang.Nullable;
/**
* Special {@link JpaQueryCreator} that creates a count projecting query.
@@ -37,39 +33,33 @@ import org.springframework.lang.Nullable;
public class JpaCountQueryCreator extends JpaQueryCreator {
private final boolean distinct;
private final ReturnedType returnedType;
/**
* Creates a new {@link JpaCountQueryCreator}.
* Creates a new {@link JpaCountQueryCreator}
*
* @param tree
* @param type
* @param builder
* @param returnedType
* @param provider
* @param templates
* @param em
*/
public JpaCountQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder,
ParameterMetadataProvider provider) {
public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, EntityManager em) {
super(tree, type, builder, provider);
super(tree, returnedType, provider, templates, em);
this.distinct = tree.isDistinct();
this.returnedType = returnedType;
}
@Override
protected CriteriaQuery<? extends Object> createCriteriaQuery(CriteriaBuilder builder, ReturnedType type) {
return builder.createQuery(Long.class);
}
protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(returnedType.getDomainType());
if (this.distinct) {
selectStep = selectStep.distinct();
}
@Override
@SuppressWarnings("unchecked")
protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate, Sort sort,
CriteriaQuery<? extends Object> query, CriteriaBuilder builder, Root<?> root) {
CriteriaQuery<? extends Object> select = query.select(getCountQuery(builder, root));
return predicate == null ? select : select.where(predicate);
}
@SuppressWarnings("rawtypes")
private Expression getCountQuery(CriteriaBuilder builder, Root<?> root) {
return distinct ? builder.countDistinct(root) : builder.count(root);
return selectStep.count();
}
}

View File

@@ -15,16 +15,19 @@
*/
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.lang.Nullable;
@@ -39,35 +42,67 @@ class JpaKeysetScrollQueryCreator extends JpaQueryCreator {
private final JpaEntityInformation<?, ?> entityInformation;
private final KeysetScrollPosition scrollPosition;
private final ParameterMetadataProvider provider;
private final List<ParameterBinding> syntheticBindings = new ArrayList<>();
public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder,
ParameterMetadataProvider provider, JpaEntityInformation<?, ?> entityInformation,
KeysetScrollPosition scrollPosition) {
public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, JpaEntityInformation<?, ?> entityInformation, KeysetScrollPosition scrollPosition,
EntityManager em) {
super(tree, type, builder, provider);
super(tree, type, provider, templates, em);
this.entityInformation = entityInformation;
this.scrollPosition = scrollPosition;
this.provider = provider;
}
@Override
protected CriteriaQuery<?> complete(@Nullable Predicate predicate, Sort sort, CriteriaQuery<?> query,
CriteriaBuilder builder, Root<?> root) {
public List<ParameterBinding> getBindings() {
List<ParameterBinding> partTreeBindings = super.getBindings();
List<ParameterBinding> bindings = new ArrayList<>(partTreeBindings.size() + this.syntheticBindings.size());
bindings.addAll(partTreeBindings);
bindings.addAll(this.syntheticBindings);
return bindings;
}
@Override
protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) {
KeysetScrollSpecification<Object> keysetSpec = new KeysetScrollSpecification<>(scrollPosition, sort,
entityInformation);
Predicate keysetPredicate = keysetSpec.createPredicate(root, builder);
CriteriaQuery<?> queryToUse = super.complete(predicate, keysetSpec.sort(), query, builder, root);
JpqlQueryBuilder.Select query = buildQuery(keysetSpec.sort());
if (keysetPredicate != null) {
if (queryToUse.getRestriction() != null) {
return queryToUse.where(builder.and(queryToUse.getRestriction(), keysetPredicate));
}
return queryToUse.where(keysetPredicate);
AtomicInteger counter = new AtomicInteger(provider.getBindings().size());
JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> {
syntheticBindings.add(provider.nextSynthetic(value, scrollPosition));
return JpqlQueryBuilder.expression(render(counter.incrementAndGet()));
});
JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate);
if (predicateToUse != null) {
return query.where(predicateToUse);
}
return queryToUse;
return query;
}
@Nullable
private static JpqlQueryBuilder.Predicate getPredicate(@Nullable JpqlQueryBuilder.Predicate predicate,
@Nullable JpqlQueryBuilder.Predicate keysetPredicate) {
if (keysetPredicate != null) {
if (predicate != null) {
return predicate.nest().and(keysetPredicate.nest());
} else {
return keysetPredicate;
}
}
return predicate;
}
@Override

View File

@@ -31,14 +31,21 @@ import org.springframework.lang.Nullable;
*/
public class JpaParametersParameterAccessor extends ParametersParameterAccessor {
private final JpaParameters parameters;
/**
* Creates a new {@link ParametersParameterAccessor}.
*
* @param parameters must not be {@literal null}.
* @param values must not be {@literal null}.
*/
public JpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values) {
public JpaParametersParameterAccessor(JpaParameters parameters, Object[] values) {
super(parameters, values);
this.parameters = parameters;
}
public JpaParameters getParameters() {
return parameters;
}
@Nullable

View File

@@ -15,28 +15,28 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.QueryUtils.*;
import static org.springframework.data.repository.query.parser.Part.Type.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.ParameterExpression;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Selection;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.SingularAttribute;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.domain.JpaSort;
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;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
@@ -58,54 +58,50 @@ import org.springframework.util.Assert;
* @author Greg Turnquist
* @author Jinmyeong Kim
*/
public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extends Object>, Predicate> {
class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
private final CriteriaBuilder builder;
private final Root<?> root;
private final CriteriaQuery<? extends Object> query;
private final ParameterMetadataProvider provider;
private final ReturnedType returnedType;
private final ParameterMetadataProvider provider;
private final JpqlQueryTemplates templates;
private final PartTree tree;
private final EscapeCharacter escape;
private final EntityType<?> entityType;
private final From<?, ?> from;
private final JpqlQueryBuilder.Entity entity;
/**
* Create a new {@link JpaQueryCreator}.
*
* @param tree must not be {@literal null}.
* @param type must not be {@literal null}.
* @param builder must not be {@literal null}.
* @param templates must not be {@literal null}.
* @param provider must not be {@literal null}.
* @param em must not be {@literal null}.
*/
public JpaQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder,
ParameterMetadataProvider provider) {
public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, EntityManager em) {
super(tree);
this.tree = tree;
CriteriaQuery<?> criteriaQuery = createCriteriaQuery(builder, type);
this.builder = builder;
this.query = criteriaQuery.distinct(tree.isDistinct() && !tree.isCountProjection());
this.root = query.from(type.getDomainType());
this.provider = provider;
this.returnedType = type;
this.provider = provider;
this.templates = templates;
this.escape = provider.getEscape();
this.entityType = em.getMetamodel().entity(type.getDomainType());
this.from = em.getCriteriaBuilder().createQuery().from(type.getDomainType());
this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType());
}
/**
* Creates the {@link CriteriaQuery} to apply predicates on.
*
* @param builder will never be {@literal null}.
* @param type will never be {@literal null}.
* @return must not be {@literal null}.
*/
protected CriteriaQuery<? extends Object> createCriteriaQuery(CriteriaBuilder builder, ReturnedType type) {
From<?, ?> getFrom() {
return from;
}
Class<?> typeToRead = tree.isDelete() ? type.getDomainType() : type.getTypeToRead();
JpqlQueryBuilder.Entity getEntity() {
return entity;
}
return (typeToRead == null) || tree.isExistsProjection() //
? builder.createTupleQuery() //
: builder.createQuery(typeToRead);
public boolean useTupleQuery() {
return returnedType.needsCustomConstruction() && returnedType.getReturnedType().isInterface();
}
/**
@@ -113,102 +109,168 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
*
* @return the parameterExpressions
*/
public List<ParameterMetadata<?>> getParameterExpressions() {
return provider.getExpressions();
public List<ParameterBinding> getBindings() {
return provider.getBindings();
}
@Override
protected Predicate create(Part part, Iterator<Object> iterator) {
return toPredicate(part, root);
public ParameterBinder getBinder() {
return ParameterBinderFactory.createBinder(provider.getParameters(), getBindings());
}
@Override
protected Predicate and(Part part, Predicate base, Iterator<Object> iterator) {
return builder.and(base, toPredicate(part, root));
protected JpqlQueryBuilder.Predicate create(Part part, Iterator<Object> iterator) {
return toPredicate(part);
}
@Override
protected Predicate or(Predicate base, Predicate predicate) {
return builder.or(base, predicate);
protected JpqlQueryBuilder.Predicate and(Part part, JpqlQueryBuilder.Predicate base, Iterator<Object> iterator) {
return base.and(toPredicate(part));
}
@Override
protected JpqlQueryBuilder.Predicate or(JpqlQueryBuilder.Predicate base, JpqlQueryBuilder.Predicate predicate) {
return base.or(predicate);
}
/**
* Finalizes the given {@link Predicate} and applies the given sort. Delegates to
* {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current
* {@link CriteriaQuery} and {@link CriteriaBuilder}.
* Finalizes the given {@link Predicate} and applies the given sort. Delegates to {@link #buildQuery(Sort)} and hands
* it the current {@link JpqlQueryBuilder.Predicate}.
*/
@Override
protected final CriteriaQuery<? extends Object> complete(Predicate predicate, Sort sort) {
return complete(predicate, sort, query, builder, root);
protected final String complete(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) {
JpqlQueryBuilder.AbstractJpqlQuery query = createQuery(predicate, sort);
return query.render();
}
protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) {
JpqlQueryBuilder.Select query = buildQuery(sort);
if (predicate != null) {
return query.where(predicate);
}
return query;
}
/**
* Template method to finalize the given {@link Predicate} using the given {@link CriteriaQuery} and
* {@link CriteriaBuilder}.
* Template method to build a query stub using the given {@link Sort}.
*
* @param predicate
* @param sort
* @param query
* @param builder
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate, Sort sort,
CriteriaQuery<? extends Object> query, CriteriaBuilder builder, Root<?> root) {
protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
JpqlQueryBuilder.Select select = doSelect(sort);
if (tree.isDelete() || tree.isCountProjection()) {
return select;
}
for (Sort.Order order : sort) {
JpqlQueryBuilder.Expression expression;
QueryUtils.checkSortExpression(order);
try {
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from,
PropertyPath.from(order.getProperty(), entityType.getJavaType())));
} catch (PropertyReferenceException e) {
if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
expression = JpqlQueryBuilder.expression(order.getProperty());
} else {
throw e;
}
}
if (order.isIgnoreCase()) {
expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression);
}
select.orderBy(JpqlQueryBuilder.orderBy(expression, order));
}
return select;
}
private JpqlQueryBuilder.Select doSelect(Sort sort) {
JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(entity);
if (tree.isDelete()) {
return selectStep.entity();
}
if (tree.isDistinct()) {
selectStep = selectStep.distinct();
}
if (returnedType.needsCustomConstruction()) {
Collection<String> requiredSelection = getRequiredSelection(sort, returnedType);
List<Selection<?>> selections = new ArrayList<>();
for (String property : requiredSelection) {
PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
selections.add(toExpressionRecursively(root, path, true).alias(property));
List<PathAndOrigin> paths = new ArrayList<>(requiredSelection.size());
for (String selection : requiredSelection) {
paths.add(
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(selection, from.getJavaType()), true));
}
Class<?> typeToRead = returnedType.getReturnedType();
if (useTupleQuery()) {
query = typeToRead.isInterface() //
? query.multiselect(selections) //
: query.select((Selection) builder.construct(typeToRead, //
selections.toArray(new Selection[0])));
return selectStep.select(paths);
} else {
return selectStep.instantiate(returnedType.getReturnedType(), paths);
}
}
} else if (tree.isExistsProjection()) {
if (tree.isExistsProjection()) {
if (root.getModel().hasSingleIdAttribute()) {
if (entityType.hasSingleIdAttribute()) {
SingularAttribute<?, ?> id = root.getModel().getId(root.getModel().getIdType().getJavaType());
query = query.multiselect(root.get((SingularAttribute) id).alias(id.getName()));
SingularAttribute<?, ?> id = entityType.getId(entityType.getIdType().getJavaType());
return selectStep.select(
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(id.getName(), from.getJavaType()), true));
} else {
query = query.multiselect(root.getModel().getIdClassAttributes().stream()//
.map(it -> (Selection<?>) root.get((SingularAttribute) it).alias(it.getName()))
.collect(Collectors.toList()));
List<PathAndOrigin> paths = entityType.getIdClassAttributes().stream()//
.map(it -> JpqlUtils.toExpressionRecursively(entity, from,
PropertyPath.from(it.getName(), from.getJavaType()), true))
.toList();
return selectStep.select(paths);
}
} else {
query = query.select((Root) root);
}
CriteriaQuery<? extends Object> select = query.orderBy(QueryUtils.toOrders(sort, root, builder));
return predicate == null ? select : select.where(predicate);
if (tree.isCountProjection()) {
return selectStep.count();
} else {
return selectStep.entity();
}
}
Collection<String> getRequiredSelection(Sort sort, ReturnedType returnedType) {
return returnedType.getInputProperties();
}
String render(ParameterBinding binding) {
return render(binding.getRequiredPosition());
}
String render(int position) {
return "?" + position;
}
/**
* Creates a {@link Predicate} from the given {@link Part}.
*
* @param part
* @param root
* @return
*/
private Predicate toPredicate(Part part, Root<?> root) {
return new PredicateBuilder(part, root).build();
private JpqlQueryBuilder.Predicate toPredicate(Part part) {
return new PredicateBuilder(part).build();
}
/**
@@ -217,24 +279,20 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
* @author Phil Webb
* @author Oliver Gierke
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private class PredicateBuilder {
private final Part part;
private final Root<?> root;
/**
* Creates a new {@link PredicateBuilder} for the given {@link Part} and {@link Root}.
* Creates a new {@link PredicateBuilder} for the given {@link Part}.
*
* @param part must not be {@literal null}.
* @param root must not be {@literal null}.
*/
public PredicateBuilder(Part part, Root<?> root) {
public PredicateBuilder(Part part) {
Assert.notNull(part, "Part must not be null");
Assert.notNull(root, "Root must not be null");
this.part = part;
this.root = root;
}
/**
@@ -242,83 +300,85 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
*
* @return
*/
public Predicate build() {
public JpqlQueryBuilder.Predicate build() {
PropertyPath property = part.getProperty();
Type type = part.getType();
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(entity, from, property);
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas);
JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas));
switch (type) {
case BETWEEN:
ParameterMetadata<Comparable> first = provider.next(part);
ParameterMetadata<Comparable> second = provider.next(part);
return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
PartTreeParameterBinding first = provider.next(part);
ParameterBinding second = provider.next(part);
return where.between(render(first), render(second));
case AFTER:
case GREATER_THAN:
return builder.greaterThan(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
return where.gt(render(provider.next(part)));
case GREATER_THAN_EQUAL:
return builder.greaterThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
return where.gte(render(provider.next(part)));
case BEFORE:
case LESS_THAN:
return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
return where.lt(render(provider.next(part)));
case LESS_THAN_EQUAL:
return builder.lessThanOrEqualTo(getComparablePath(root, part),
provider.next(part, Comparable.class).getExpression());
return where.lte(render(provider.next(part)));
case IS_NULL:
return getTypedPath(root, part).isNull();
return where.isNull();
case IS_NOT_NULL:
return getTypedPath(root, part).isNotNull();
return where.isNotNull();
case NOT_IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression()).not();
return whereIgnoreCase.notIn(render(provider.next(part, Collection.class)));
case IN:
// cast required for eclipselink workaround, see DATAJPA-433
return upperIfIgnoreCase(getTypedPath(root, part))
.in((Expression<Collection<?>>) provider.next(part, Collection.class).getExpression());
return whereIgnoreCase.in(render(provider.next(part, Collection.class)));
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
case NOT_CONTAINING:
if (property.getLeafProperty().isCollection()) {
where = JpqlQueryBuilder.where(entity, property);
Expression<Collection<Object>> propertyExpression = traversePath(root, property);
ParameterExpression<Object> parameterExpression = provider.next(part).getExpression();
// Can't just call .not() in case of negation as EclipseLink chokes on that.
return type.equals(NOT_CONTAINING) //
? isNotMember(builder, parameterExpression, propertyExpression) //
: isMember(builder, parameterExpression, propertyExpression);
return type.equals(NOT_CONTAINING) ? where.notMemberOf(render(provider.next(part)))
: where.memberOf(render(provider.next(part)));
}
case LIKE:
case NOT_LIKE:
Expression<String> stringPath = getTypedPath(root, part);
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
Expression<String> parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
PartTreeParameterBinding parameter = provider.next(part, String.class);
JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(),
JpqlQueryBuilder.parameter(render(parameter)));
// Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
String escapeChar = Character.toString(escape.getEscapeCharacter());
return
type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING)
? whereIgnoreCase.notLike(parameterExpression, escapeChar)
: whereIgnoreCase.like(parameterExpression, escapeChar);
case TRUE:
Expression<Boolean> truePath = getTypedPath(root, part);
return builder.isTrue(truePath);
return where.isTrue();
case FALSE:
Expression<Boolean> falsePath = getTypedPath(root, part);
return builder.isFalse(falsePath);
return where.isFalse();
case SIMPLE_PROPERTY:
PartTreeParameterBinding simple = provider.next(part);
if (simple.isIsNullParameter()) {
return where.isNull();
}
return whereIgnoreCase.eq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(simple))));
case NEGATING_SIMPLE_PROPERTY:
ParameterMetadata<Object> expression = provider.next(part);
Expression<Object> path = getTypedPath(root, part);
PartTreeParameterBinding negating = provider.next(part);
if (expression.isIsNullParameter()) {
return type.equals(SIMPLE_PROPERTY) ? path.isNull() : path.isNotNull();
} else {
return type.equals(SIMPLE_PROPERTY)
? builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()))
: builder.notEqual(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
if (negating.isIsNullParameter()) {
return where.isNotNull();
}
return whereIgnoreCase
.neq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(negating))));
case IS_EMPTY:
case IS_NOT_EMPTY:
@@ -326,77 +386,69 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties");
}
Expression<Collection<Object>> collectionPath = traversePath(root, property);
return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath);
where = JpqlQueryBuilder.where(entity, property);
return type.equals(IS_NOT_EMPTY) ? where.isNotEmpty() : where.isEmpty();
default:
throw new IllegalArgumentException("Unsupported keyword " + type);
}
}
private <T> Predicate isMember(CriteriaBuilder builder, Expression<T> parameter,
Expression<Collection<T>> property) {
return builder.isMember(parameter, property);
}
private <T> Predicate isNotMember(CriteriaBuilder builder, Expression<T> parameter,
Expression<Collection<T>> property) {
return builder.isNotMember(parameter, property);
/**
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
* requires ignoring case.
*
* @param path must not be {@literal null}.
* @return
*/
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.Origin source, PropertyPath path) {
return potentiallyIgnoreCase(path, JpqlQueryBuilder.expression(source, path));
}
/**
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
* requires ignoring case.
*
* @param expression must not be {@literal null}.
* @param path must not be {@literal null}.
* @return
*/
private <T> Expression<T> upperIfIgnoreCase(Expression<? extends T> expression) {
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin pas) {
return potentiallyIgnoreCase(pas.path(), JpqlQueryBuilder.expression(pas));
}
/**
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
* requires ignoring case.
*
* @return
*/
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PropertyPath path,
JpqlQueryBuilder.Expression expressionValue) {
switch (part.shouldIgnoreCase()) {
case ALWAYS:
Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName()
Assert.isTrue(canUpperCase(path), "Unable to ignore case of " + path.getType().getName()
+ " types, the property '" + part.getProperty().getSegment() + "' must reference a String");
return (Expression<T>) builder.upper((Expression<String>) expression);
return JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expressionValue);
case WHEN_POSSIBLE:
if (canUpperCase(expression)) {
return (Expression<T>) builder.upper((Expression<String>) expression);
if (canUpperCase(path)) {
return JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expressionValue);
}
case NEVER:
default:
return (Expression<T>) expression;
return expressionValue;
}
}
private boolean canUpperCase(Expression<?> expression) {
return String.class.equals(expression.getJavaType());
}
/**
* Returns a path to a {@link Comparable}.
*
* @param root
* @param part
* @return
*/
private Expression<? extends Comparable> getComparablePath(Root<?> root, Part part) {
return getTypedPath(root, part);
}
private <T> Expression<T> getTypedPath(Root<?> root, Part part) {
return toExpressionRecursively(root, part.getProperty());
}
private <T> Expression<T> traversePath(Path<?> root, PropertyPath path) {
Path<Object> result = root.get(path.getSegment());
return (Expression<T>) (path.hasNext() ? traversePath(result, path.next()) : result);
private boolean canUpperCase(PropertyPath path) {
return String.class.equals(path.getType());
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import java.util.List;
import org.springframework.data.domain.Sort;
/**
* @author Mark Paluch
*/
interface JpqlQueryCreator {
boolean useTupleQuery();
String createQuery(Sort sort);
List<ParameterBinding> getBindings();
ParameterBinder getBinder();
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import java.util.Objects;
import org.springframework.data.mapping.PropertyPath;
/**
* @author Mark Paluch
*/
class JpqlUtils {
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
PropertyPath property) {
return toExpressionRecursively(source, from, property, false);
}
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
PropertyPath property, boolean isForSelection) {
return toExpressionRecursively(source, from, property, isForSelection, false);
}
/**
* Creates an expression with proper inner and left joins by recursively navigating the path
*
* @param from the {@link From}
* @param property the property path
* @param isForSelection is the property navigated for the selection or ordering part of the query?
* @param hasRequiredOuterJoin has a parent already required an outer join?
* @param <T> the type of the expression
* @return the expression
*/
@SuppressWarnings("unchecked")
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
String segment = property.getSegment();
boolean isLeafProperty = !property.hasNext();
boolean requiresOuterJoin = QueryUtils.requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin);
// if it does not require an outer join and is a leaf, simply get the segment
if (!requiresOuterJoin && isLeafProperty) {
return new JpqlQueryBuilder.PathAndOrigin(property, source, false);
}
// 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);
}
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
// recurse with the next property
return toExpressionRecursively(joinSource, join, nextProperty, isForSelection, requiresOuterJoin);
}
}

View File

@@ -134,6 +134,29 @@ public class KeysetScrollDelegate {
return CollectionUtils.getFirst(limit, list);
}
public Sort createSort(Sort sort, JpaEntityInformation<?, ?> entity) {
Collection<String> sortById;
Sort sortToUse;
if (entity.hasCompositeId()) {
sortById = new ArrayList<>(entity.getIdAttributeNames());
} else {
sortById = new ArrayList<>(1);
sortById.add(entity.getRequiredIdAttribute().getName());
}
sort.forEach(it -> sortById.remove(it.getProperty()));
if (sortById.isEmpty()) {
sortToUse = sort;
} else {
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
}
return getSortOrders(sortToUse);
}
/**
* Reverse scrolling variant applying {@link Direction#Backward}. In reverse scrolling, we need to flip directions for
* the actual query so that we do not get everything from the top position and apply the limit but rather flip the

View File

@@ -22,8 +22,6 @@ import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.data.domain.KeysetScrollPosition;
@@ -42,7 +40,7 @@ import org.springframework.lang.Nullable;
* @author Christoph Strobl
* @since 3.1
*/
public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort sort,
public record KeysetScrollSpecification<T>(KeysetScrollPosition position, Sort sort,
JpaEntityInformation<?, ?> entity) implements Specification<T> {
public KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEntityInformation<?, ?> entity) {
@@ -63,24 +61,7 @@ public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
Collection<String> sortById;
Sort sortToUse;
if (entity.hasCompositeId()) {
sortById = new ArrayList<>(entity.getIdAttributeNames());
} else {
sortById = new ArrayList<>(1);
sortById.add(entity.getRequiredIdAttribute().getName());
}
sort.forEach(it -> sortById.remove(it.getProperty()));
if (sortById.isEmpty()) {
sortToUse = sort;
} else {
sortToUse = sort.and(Sort.by(sortById.toArray(new String[0])));
}
return delegate.getSortOrders(sortToUse);
return delegate.createSort(sort, entity);
}
@Override
@@ -92,16 +73,24 @@ public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort
public Predicate createPredicate(Root<?> root, CriteriaBuilder criteriaBuilder) {
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
return delegate.createPredicate(position, sort, new JpaQueryStrategy(root, criteriaBuilder));
return delegate.createPredicate(position, sort, new CriteriaBuilderStrategy(root, criteriaBuilder));
}
@Nullable
public JpqlQueryBuilder.Predicate createJpqlPredicate(From<?, ?> from, JpqlQueryBuilder.Entity entity,
ParameterFactory factory) {
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
return delegate.createPredicate(position, sort, new JpqlStrategy(from, entity, factory));
}
@SuppressWarnings("rawtypes")
private static class JpaQueryStrategy implements QueryStrategy<Expression<Comparable>, Predicate> {
private static class CriteriaBuilderStrategy implements QueryStrategy<Expression<Comparable>, Predicate> {
private final From<?, ?> from;
private final CriteriaBuilder cb;
public JpaQueryStrategy(From<?, ?> from, CriteriaBuilder cb) {
public CriteriaBuilderStrategy(From<?, ?> from, CriteriaBuilder cb) {
this.from = from;
this.cb = cb;
@@ -136,4 +125,55 @@ public record KeysetScrollSpecification<T> (KeysetScrollPosition position, Sort
return cb.or(intermediate.toArray(new Predicate[0]));
}
}
private static class JpqlStrategy implements QueryStrategy<JpqlQueryBuilder.Expression, JpqlQueryBuilder.Predicate> {
private final From<?, ?> from;
private final JpqlQueryBuilder.Entity entity;
private final ParameterFactory factory;
public JpqlStrategy(From<?, ?> from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) {
this.from = from;
this.entity = entity;
this.factory = factory;
}
@Override
public JpqlQueryBuilder.Expression createExpression(String property) {
PropertyPath path = PropertyPath.from(property, from.getJavaType());
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, path));
}
@Override
public JpqlQueryBuilder.Predicate compare(Order order, JpqlQueryBuilder.Expression propertyExpression,
Object value) {
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression);
return order.isAscending() ? where.gt(factory.capture(value)) : where.lt(factory.capture(value));
}
@Override
public JpqlQueryBuilder.Predicate compare(JpqlQueryBuilder.Expression propertyExpression, @Nullable Object value) {
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression);
return value == null ? where.isNull() : where.eq(factory.capture(value));
}
@Override
public JpqlQueryBuilder.Predicate and(List<JpqlQueryBuilder.Predicate> intermediate) {
return JpqlQueryBuilder.and(intermediate);
}
@Override
public JpqlQueryBuilder.Predicate or(List<JpqlQueryBuilder.Predicate> intermediate) {
return JpqlQueryBuilder.or(intermediate);
}
}
public interface ParameterFactory {
JpqlQueryBuilder.Expression capture(Object value);
}
}

View File

@@ -56,8 +56,6 @@ final class NamedQuery extends AbstractJpaQuery {
private final @Nullable String countProjection;
private final boolean namedCountQueryIsPresent;
private final Lazy<DeclaredQuery> declaredQuery;
private final QueryParameterSetter.QueryMetadataCache metadataCache;
private final QueryRewriter queryRewriter;
/**
* Creates a new {@link NamedQuery}.
@@ -100,7 +98,6 @@ final class NamedQuery extends AbstractJpaQuery {
this.declaredQuery = Lazy
.of(() -> DeclaredQuery.of(queryString, method.isNativeQuery() || query.toString().contains("NativeQuery")));
this.metadataCache = new QueryParameterSetter.QueryMetadataCache();
}
/**
@@ -175,9 +172,7 @@ final class NamedQuery extends AbstractJpaQuery {
? em.createNamedQuery(queryName) //
: em.createNamedQuery(queryName, typeToRead);
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryName, query);
return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
return parameterBinder.get().bindAndPrepare(query, accessor);
}
@Override
@@ -199,9 +194,7 @@ final class NamedQuery extends AbstractJpaQuery {
countQuery = em.createQuery(countQueryString, Long.class);
}
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(cacheKey, countQuery);
return parameterBinder.get().bind(countQuery, metadata, accessor);
return parameterBinder.get().bind(countQuery, accessor);
}
@Override

View File

@@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* {@link ParameterBinder} is used to bind method parameters to a {@link Query}. This is usually done whenever an
@@ -33,7 +34,7 @@ import org.springframework.util.Assert;
* @author Jens Schauder
* @author Yanming Zhou
*/
public class ParameterBinder {
class ParameterBinder {
static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters";
@@ -72,18 +73,18 @@ public class ParameterBinder {
this.useJpaForPaging = useJpaForPaging;
}
public <T extends Query> T bind(T jpaQuery, QueryParameterSetter.QueryMetadata metadata,
public <T extends Query> T bind(T jpaQuery,
JpaParametersParameterAccessor accessor) {
bind(metadata.withQuery(jpaQuery), accessor, ErrorHandling.STRICT);
bind(new QueryParameterSetter.BindableQuery(jpaQuery), accessor, ErrorHandling.STRICT);
return jpaQuery;
}
public void bind(QueryParameterSetter.BindableQuery query, JpaParametersParameterAccessor accessor,
ErrorHandling errorHandling) {
ErrorHandler errorHandler) {
for (QueryParameterSetter setter : parameterSetters) {
setter.setParameter(query, accessor, errorHandling);
setter.setParameter(query, accessor, errorHandler);
}
}
@@ -91,13 +92,12 @@ public class ParameterBinder {
* Binds the parameters to the given query and applies special parameter types (e.g. pagination).
*
* @param query must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param accessor must not be {@literal null}.
*/
Query bindAndPrepare(Query query, QueryParameterSetter.QueryMetadata metadata,
Query bindAndPrepare(Query query,
JpaParametersParameterAccessor accessor) {
bind(query, metadata, accessor);
bind(query, accessor);
Pageable pageable = accessor.getPageable();

View File

@@ -23,7 +23,6 @@ import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.util.Assert;
/**
@@ -40,37 +39,37 @@ class ParameterBinderFactory {
* otherwise.
*
* @param parameters method parameters that are available for binding, must not be {@literal null}.
* @param preferNamedParameters
* @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a
* {@link jakarta.persistence.Query}
*/
static ParameterBinder createBinder(JpaParameters parameters) {
static ParameterBinder createBinder(JpaParameters parameters, boolean preferNamedParameters) {
Assert.notNull(parameters, "JpaParameters must not be null");
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters);
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters, preferNamedParameters);
List<ParameterBinding> bindings = getBindings(parameters);
return new ParameterBinder(parameters, createSetters(bindings, setterFactory));
}
/**
* Creates a {@link ParameterBinder} that just matches method parameter to parameters of a
* {@link jakarta.persistence.criteria.CriteriaQuery}.
* Creates a {@link ParameterBinder} that matches method parameter to parameters of a
* {@link jakarta.persistence.Query} and that can bind synthetic parameters.
*
* @param parameters method parameters that are available for binding, must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param bindings parameter bindings for method argument and synthetic parameters, must not be {@literal null}.
* @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a
* {@link jakarta.persistence.criteria.CriteriaQuery}
* {@link jakarta.persistence.Query}
*/
static ParameterBinder createCriteriaBinder(JpaParameters parameters, List<ParameterMetadata<?>> metadata) {
static ParameterBinder createBinder(JpaParameters parameters, List<ParameterBinding> bindings) {
Assert.notNull(parameters, "JpaParameters must not be null");
Assert.notNull(metadata, "Parameter metadata must not be null");
Assert.notNull(bindings, "Parameter bindings must not be null");
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata);
List<ParameterBinding> bindings = getBindings(parameters);
return new ParameterBinder(parameters, createSetters(bindings, setterFactory));
return new ParameterBinder(parameters,
createSetters(bindings, QueryParameterSetterFactory.forPartTreeQuery(parameters),
QueryParameterSetterFactory.forSynthetic()));
}
/**
@@ -97,15 +96,16 @@ class ParameterBinderFactory {
QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser,
evaluationContextProvider);
QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters);
QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters,
query.hasNamedParameter());
return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory),
!query.usesPaging());
}
private static List<ParameterBinding> getBindings(JpaParameters parameters) {
static List<ParameterBinding> getBindings(JpaParameters parameters) {
List<ParameterBinding> result = new ArrayList<>();
List<ParameterBinding> result = new ArrayList<>(parameters.getNumberOfParameters());
int bindableParameterIndex = 0;
for (JpaParameter parameter : parameters) {
@@ -143,7 +143,7 @@ class ParameterBinderFactory {
for (QueryParameterSetterFactory strategy : strategies) {
QueryParameterSetter setter = strategy.create(binding, declaredQuery);
QueryParameterSetter setter = strategy.create(binding);
if (setter != null) {
return setter;

View File

@@ -21,13 +21,19 @@ import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -186,6 +192,115 @@ class ParameterBinding {
return other.getClass() == getClass() && other.getOrigin().equals(getOrigin());
}
/**
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
* {@code IN} parameter.
*
* @author Thomas Darimont
* @author Mark Paluch
*/
static class PartTreeParameterBinding extends ParameterBinding {
private final Class<?> parameterType;
private final JpqlQueryTemplates templates;
private final EscapeCharacter escape;
private final Type type;
private final boolean ignoreCase;
private final boolean noWildcards;
public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class<?> parameterType,
Part part, @Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape) {
super(identifier, origin);
this.parameterType = parameterType;
this.templates = templates;
this.escape = escape;
this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType();
this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase());
this.noWildcards = part.getProperty().getLeafProperty().isCollection();
}
/**
* Returns whether the parameter shall be considered an {@literal IS NULL} parameter.
*/
public boolean isIsNullParameter() {
return Type.IS_NULL.equals(type);
}
@Override
public Object prepare(@Nullable Object value) {
if (value == null || parameterType == null) {
return value;
}
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 Collection.class.isAssignableFrom(parameterType) //
? potentiallyIgnoreCase(ignoreCase, toCollection(value)) //
: value;
}
@Nullable
@SuppressWarnings("unchecked")
private Collection<?> potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection<?> collection) {
if (!ignoreCase || CollectionUtils.isEmpty(collection)) {
return collection;
}
return ((Collection<String>) collection).stream() //
.map(it -> it == null //
? null //
: templates.ignoreCase(it)) //
.collect(Collectors.toList());
}
/**
* Returns the given argument as {@link Collection} which means it will return it as is if it's a
* {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element
* {@link Collections}.
*
* @param value the value to be converted to a {@link Collection}.
* @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value.
*/
@Nullable
private static Collection<?> toCollection(@Nullable Object value) {
if (value == null) {
return null;
}
if (value instanceof Collection<?> collection) {
return collection.isEmpty() ? null : collection;
}
if (ObjectUtils.isArray(value)) {
List<Object> collection = Arrays.asList(ObjectUtils.toObjectArray(value));
return collection.isEmpty() ? null : collection;
}
return Collections.singleton(value);
}
}
/**
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
* {@code IN} parameter.
@@ -349,7 +464,7 @@ class ParameterBinding {
* @author Mark Paluch
* @since 3.1.2
*/
sealed interface BindingIdentifier permits Named,Indexed,NamedAndIndexed {
sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
/**
* Creates an identifier for the given {@code name}.
@@ -495,7 +610,7 @@ class ParameterBinding {
* @author Mark Paluch
* @since 3.1.2
*/
sealed interface ParameterOrigin permits Expression,MethodInvocationArgument {
sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic {
/**
* Creates a {@link Expression} for the given {@code expression}.
@@ -507,6 +622,17 @@ class ParameterBinding {
return new Expression(expression);
}
/**
* Creates a {@link Expression} for the given {@code expression} string.
*
* @param value the captured value.
* @param source source from which this value is derived.
* @return {@link Synthetic} for the given {@code value}.
*/
static Synthetic synthetic(@Nullable Object value, Object source) {
return new Synthetic(value, source);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code name}
*
@@ -539,6 +665,16 @@ class ParameterBinding {
return ofParameter(identifier);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code position}.
*
* @param position the parameter position (1-based) from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code position}.
*/
static MethodInvocationArgument ofParameter(Parameter parameter) {
return ofParameter(parameter.getIndex() + 1);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code position}.
*
@@ -568,6 +704,11 @@ class ParameterBinding {
* @return {@code true} if the origin is an expression.
*/
boolean isExpression();
/**
* @return {@code true} if the origin is an expression.
*/
boolean isSynthetic();
}
/**
@@ -588,6 +729,36 @@ class ParameterBinding {
public boolean isExpression() {
return true;
}
@Override
public boolean isSynthetic() {
return true;
}
}
/**
* Value object capturing the expression of which a binding parameter originates.
*
* @param value
* @param source
* @author Mark Paluch
*/
public record Synthetic(@Nullable Object value, Object source) implements ParameterOrigin {
@Override
public boolean isMethodArgument() {
return false;
}
@Override
public boolean isExpression() {
return false;
}
@Override
public boolean isSynthetic() {
return true;
}
}
/**
@@ -608,5 +779,10 @@ class ParameterBinding {
public boolean isExpression() {
return false;
}
@Override
public boolean isSynthetic() {
return false;
}
}
}

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.ParameterBinding.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.ParameterExpression;
import java.util.ArrayList;
import java.util.Arrays;
@@ -24,10 +25,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
@@ -56,83 +57,87 @@ import org.springframework.util.ObjectUtils;
*/
public class ParameterMetadataProvider {
private final CriteriaBuilder builder;
private final Iterator<? extends Parameter> parameters;
private final List<ParameterMetadata<?>> expressions;
private final List<ParameterBinding> bindings;
private final @Nullable Iterator<Object> bindableParameterValues;
private final EscapeCharacter escape;
private final JpqlQueryTemplates templates;
private final JpaParameters jpaParameters;
private int position;
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and
* {@link ParametersParameterAccessor}.
*
* @param builder must not be {@literal null}.
* @param accessor must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
public ParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor,
EscapeCharacter escape) {
this(builder, accessor.iterator(), accessor.getParameters(), escape);
public ParameterMetadataProvider(JpaParametersParameterAccessor accessor,
EscapeCharacter escape, JpqlQueryTemplates templates) {
this(accessor.iterator(), accessor.getParameters(), escape, templates);
}
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with
* support for parameter value customizations via {@link PersistenceProvider}.
*
* @param builder must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
public ParameterMetadataProvider(CriteriaBuilder builder, Parameters<?, ?> parameters, EscapeCharacter escape) {
this(builder, null, parameters, escape);
public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape,
JpqlQueryTemplates templates) {
this(null, parameters, escape, templates);
}
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} an {@link Iterable} of all
* bindable parameter values, and {@link Parameters}.
*
* @param builder must not be {@literal null}.
* @param bindableParameterValues may be {@literal null}.
* @param parameters must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
private ParameterMetadataProvider(CriteriaBuilder builder, @Nullable Iterator<Object> bindableParameterValues,
Parameters<?, ?> parameters, EscapeCharacter escape) {
private ParameterMetadataProvider(@Nullable Iterator<Object> bindableParameterValues, JpaParameters parameters,
EscapeCharacter escape, JpqlQueryTemplates templates) {
Assert.notNull(builder, "CriteriaBuilder must not be null");
Assert.notNull(parameters, "Parameters must not be null");
Assert.notNull(escape, "EscapeCharacter must not be null");
Assert.notNull(templates, "JpqlQueryTemplates must not be null");
this.builder = builder;
this.jpaParameters = parameters;
this.parameters = parameters.getBindableParameters().iterator();
this.expressions = new ArrayList<>();
this.bindings = new ArrayList<>();
this.bindableParameterValues = bindableParameterValues;
this.escape = escape;
this.templates = templates;
}
/**
* Returns all {@link ParameterMetadata}s built.
* Returns all {@link ParameterBinding}s built.
*
* @return the expressions
* @return the bindings.
*/
public List<ParameterMetadata<?>> getExpressions() {
return expressions;
public List<ParameterBinding> getBindings() {
return bindings;
}
/**
* Builds a new {@link ParameterMetadata} for given {@link Part} and the next {@link Parameter}.
* Builds a new {@link PartTreeParameterBinding} for given {@link Part} and the next {@link Parameter}.
*/
@SuppressWarnings("unchecked")
public <T> ParameterMetadata<T> next(Part part) {
public <T> PartTreeParameterBinding next(Part part) {
Assert.isTrue(parameters.hasNext(), () -> String.format("No parameter available for part %s", part));
Parameter parameter = parameters.next();
return (ParameterMetadata<T>) next(part, parameter.getType(), parameter);
return next(part, parameter.getType(), parameter);
}
/**
* Builds a new {@link ParameterMetadata} of the given {@link Part} and type. Forwards the underlying
* Builds a new {@link PartTreeParameterBinding} of the given {@link Part} and type. Forwards the underlying
* {@link Parameters} as well.
*
* @param <T> is the type parameter of the returned {@link ParameterMetadata}.
@@ -140,15 +145,15 @@ public class ParameterMetadataProvider {
* @return ParameterMetadata for the next parameter.
*/
@SuppressWarnings("unchecked")
public <T> ParameterMetadata<? extends T> next(Part part, Class<T> type) {
public <T> PartTreeParameterBinding next(Part part, Class<T> type) {
Parameter parameter = parameters.next();
Class<?> typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type;
return (ParameterMetadata<? extends T>) next(part, typeToUse, parameter);
return next(part, typeToUse, parameter);
}
/**
* Builds a new {@link ParameterMetadata} for the given type and name.
* Builds a new {@link PartTreeParameterBinding} for the given type and name.
*
* @param <T> type parameter for the returned {@link ParameterMetadata}.
* @param part must not be {@literal null}.
@@ -156,7 +161,7 @@ public class ParameterMetadataProvider {
* @param parameter providing the name for the returned {@link ParameterMetadata}.
* @return a new {@link ParameterMetadata} for the given type and name.
*/
private <T> ParameterMetadata<T> next(Part part, Class<T> type, Parameter parameter) {
private <T> PartTreeParameterBinding next(Part part, Class<T> type, Parameter parameter) {
Assert.notNull(type, "Type must not be null");
@@ -166,37 +171,57 @@ public class ParameterMetadataProvider {
@SuppressWarnings("unchecked")
Class<T> reifiedType = Expression.class.equals(type) ? (Class<T>) Object.class : type;
Supplier<String> name = () -> parameter.getName()
.orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named"));
ParameterExpression<T> expression = parameter.isExplicitlyNamed() //
? builder.parameter(reifiedType, name.get()) //
: builder.parameter(reifiedType);
Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next();
ParameterMetadata<T> metadata = new ParameterMetadata<>(expression, part, value, escape);
expressions.add(metadata);
int currentPosition = ++position;
return metadata;
BindingIdentifier bindingIdentifier = BindingIdentifier.of(currentPosition);
/* identifier refers to bindable parameters, not _all_ parameters index */
MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(bindingIdentifier);
PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier, methodParameter, reifiedType,
part, value, templates, escape);
bindings.add(binding);
return binding;
}
EscapeCharacter getEscape() {
return escape;
}
/**
* Builds a new synthetic {@link ParameterBinding} for the given value.
*
* @param value
* @param source
* @return a new {@link ParameterBinding} for the given value and source.
*/
public ParameterBinding nextSynthetic(Object value, Object source) {
int currentPosition = ++position;
return new ParameterBinding(BindingIdentifier.of(currentPosition), ParameterOrigin.synthetic(value, source));
}
public JpaParameters getParameters() {
return this.jpaParameters;
}
/**
* @author Oliver Gierke
* @author Thomas Darimont
* @author Andrey Kovalev
* @param <T>
*/
public static class ParameterMetadata<T> {
public static class ParameterMetadata {
static final Object PLACEHOLDER = new Object();
private final Class<?> parameterType;
private final Type type;
private final ParameterExpression<T> expression;
private final int position;
private final JpqlQueryTemplates templates;
private final EscapeCharacter escape;
private final boolean ignoreCase;
private final boolean noWildcards;
@@ -204,10 +229,12 @@ public class ParameterMetadataProvider {
/**
* Creates a new {@link ParameterMetadata}.
*/
public ParameterMetadata(ParameterExpression<T> expression, Part part, @Nullable Object value,
EscapeCharacter escape) {
public ParameterMetadata(Class<?> parameterType, Part part, @Nullable Object value, EscapeCharacter escape,
int position, JpqlQueryTemplates templates) {
this.expression = expression;
this.parameterType = parameterType;
this.position = position;
this.templates = templates;
this.type = value == null
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
? Type.IS_NULL
@@ -217,13 +244,12 @@ public class ParameterMetadataProvider {
this.escape = escape;
}
/**
* Returns the {@link ParameterExpression}.
*
* @return the expression
*/
public ParameterExpression<T> getExpression() {
return expression;
public int getPosition() {
return position;
}
public Class<?> getParameterType() {
return parameterType;
}
/**
@@ -241,11 +267,11 @@ public class ParameterMetadataProvider {
@Nullable
public Object prepare(@Nullable Object value) {
if (value == null || expression.getJavaType() == null) {
if (value == null || parameterType == null) {
return value;
}
if (String.class.equals(expression.getJavaType()) && !noWildcards) {
if (String.class.equals(parameterType) && !noWildcards) {
switch (type) {
case STARTING_WITH:
@@ -260,8 +286,8 @@ public class ParameterMetadataProvider {
}
}
return Collection.class.isAssignableFrom(expression.getJavaType()) //
? upperIfIgnoreCase(ignoreCase, toCollection(value)) //
return Collection.class.isAssignableFrom(parameterType) //
? potentiallyIgnoreCase(ignoreCase, toCollection(value)) //
: value;
}
@@ -295,7 +321,7 @@ public class ParameterMetadataProvider {
@Nullable
@SuppressWarnings("unchecked")
private static Collection<?> upperIfIgnoreCase(boolean ignoreCase, @Nullable Collection<?> collection) {
private Collection<?> potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection<?> collection) {
if (!ignoreCase || CollectionUtils.isEmpty(collection)) {
return collection;
@@ -304,8 +330,9 @@ public class ParameterMetadataProvider {
return ((Collection<String>) collection).stream() //
.map(it -> it == null //
? null //
: it.toUpperCase()) //
: templates.ignoreCase(it)) //
.collect(Collectors.toList());
}
}
}

View File

@@ -18,12 +18,13 @@ package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.Query;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Map;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
@@ -33,8 +34,8 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ScrollExecution;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.Part;
@@ -42,6 +43,7 @@ import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link AbstractJpaQuery} implementation based on a {@link PartTree}.
@@ -55,6 +57,8 @@ import org.springframework.lang.Nullable;
*/
public class PartTreeJpaQuery extends AbstractJpaQuery {
private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
private final PartTree tree;
private final JpaParameters parameters;
@@ -93,15 +97,12 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();
this.entityInformation = new JpaMetamodelEntityInformation<>(domainClass, em.getMetamodel(), persistenceUnitUtil);
boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically()
|| method.isScrollQuery();
try {
this.tree = new PartTree(method.getName(), domainClass);
validate(tree, parameters, method.toString());
this.countQuery = new CountQueryPreparer(recreationRequired);
this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(recreationRequired);
this.countQuery = new CountQueryPreparer();
this.query = tree.isCountProjection() ? countQuery : new QueryPreparer();
} catch (Exception o_O) {
throw new IllegalArgumentException(
@@ -200,6 +201,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
return type == Type.IN || type == Type.NOT_IN;
}
/**
* Query preparer to create {@link CriteriaQuery} instances and potentially cache them.
*
@@ -208,50 +210,35 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
*/
private class QueryPreparer {
private final @Nullable CriteriaQuery<?> cachedCriteriaQuery;
private final ReentrantLock lock = new ReentrantLock();
private final @Nullable ParameterBinder cachedParameterBinder;
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
QueryPreparer(boolean recreateQueries) {
JpaQueryCreator creator = createCreator(null);
if (recreateQueries) {
this.cachedCriteriaQuery = null;
this.cachedParameterBinder = null;
} else {
this.cachedCriteriaQuery = creator.createQuery();
this.cachedParameterBinder = getBinder(creator.getParameterExpressions());
private final Map<Sort, JpqlQueryCreator> cache = new LinkedHashMap<Sort, JpqlQueryCreator>() {
@Override
protected boolean removeEldestEntry(Map.Entry<Sort, JpqlQueryCreator> eldest) {
return size() > 256;
}
}
};
/**
* Creates a new {@link Query} for the given parameter values.
*/
public Query createQuery(JpaParametersParameterAccessor accessor) {
CriteriaQuery<?> criteriaQuery = cachedCriteriaQuery;
ParameterBinder parameterBinder = cachedParameterBinder;
Sort sort = getDynamicSort(accessor);
JpqlQueryCreator creator = createCreator(sort, accessor);
String jpql = creator.createQuery(sort);
Query query;
if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) {
JpaQueryCreator creator = createCreator(accessor);
criteriaQuery = creator.createQuery(getDynamicSort(accessor));
List<ParameterMetadata<?>> expressions = creator.getParameterExpressions();
parameterBinder = getBinder(expressions);
try {
query = creator.useTupleQuery() ? em.createQuery(jpql, Tuple.class) : em.createQuery(jpql);
} catch (Exception e) {
throw new BadJpqlGrammarException(e.getMessage(), jpql, e);
}
if (parameterBinder == null) {
throw new IllegalStateException("ParameterBinder is null");
}
TypedQuery<?> query = createQuery(criteriaQuery);
ParameterBinder binder = creator.getBinder();
ScrollPosition scrollPosition = accessor.getParameters().hasScrollPositionParameter()
? accessor.getScrollPosition()
: null;
return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache),
scrollPosition);
return restrictMaxResultsIfNecessary(invokeBinding(binder, query, accessor), scrollPosition);
}
/**
@@ -289,65 +276,85 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
return query;
}
/**
* Checks whether we are working with a cached {@link CriteriaQuery} and synchronizes the creation of a
* {@link TypedQuery} instance from it. This is due to non-thread-safety in the {@link CriteriaQuery} implementation
* of some persistence providers (i.e. Hibernate in this case), see DATAJPA-396.
*
* @param criteriaQuery must not be {@literal null}.
*/
private TypedQuery<?> createQuery(CriteriaQuery<?> criteriaQuery) {
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
if (this.cachedCriteriaQuery != null) {
lock.lock();
try {
return getEntityManager().createQuery(criteriaQuery);
} finally {
lock.unlock();
synchronized (cache) {
JpqlQueryCreator jpqlQueryCreator = cache.get(sort);
if (jpqlQueryCreator != null) {
return jpqlQueryCreator;
}
}
return getEntityManager().createQuery(criteriaQuery);
}
protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
EntityManager entityManager = getEntityManager();
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
ResultProcessor processor = getQueryMethod().getResultProcessor();
ParameterMetadataProvider provider;
ReturnedType returnedType;
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates);
ReturnedType returnedType = processor.withDynamicProjection(accessor).getReturnedType();
if (accessor != null) {
provider = new ParameterMetadataProvider(builder, accessor, escape);
returnedType = processor.withDynamicProjection(accessor).getReturnedType();
} else {
provider = new ParameterMetadataProvider(builder, parameters, escape);
returnedType = processor.getReturnedType();
if (accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) {
return new JpaKeysetScrollQueryCreator(tree, returnedType, provider, templates, entityInformation, keyset,
entityManager);
}
if (accessor != null && accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) {
return new JpaKeysetScrollQueryCreator(tree, returnedType, builder, provider, entityInformation, keyset);
JpqlQueryCreator creator = new CacheableJpqlQueryCreator(sort,
new JpaQueryCreator(tree, returnedType, provider, templates, em));
if (accessor.getParameters().hasDynamicProjection()) {
return creator;
}
return new JpaQueryCreator(tree, returnedType, builder, provider);
synchronized (cache) {
cache.put(sort, creator);
}
return creator;
}
static class CacheableJpqlQueryCreator implements JpqlQueryCreator {
private final Sort expectedSort;
private final String query;
private final boolean useTupleQuery;
private final List<ParameterBinding> parameterBindings;
private final ParameterBinder binder;
public CacheableJpqlQueryCreator(Sort expectedSort, JpqlQueryCreator delegate) {
this.expectedSort = expectedSort;
this.query = delegate.createQuery(expectedSort);
this.useTupleQuery = delegate.useTupleQuery();
this.parameterBindings = delegate.getBindings();
this.binder = delegate.getBinder();
}
@Override
public boolean useTupleQuery() {
return useTupleQuery;
}
@Override
public String createQuery(Sort sort) {
Assert.isTrue(sort.equals(expectedSort), "Expected sort does not match");
return query;
}
@Override
public List<ParameterBinding> getBindings() {
return parameterBindings;
}
@Override
public ParameterBinder getBinder() {
return binder;
}
}
/**
* Invokes parameter binding on the given {@link TypedQuery}.
*/
protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
QueryParameterSetter.QueryMetadataCache metadataCache) {
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("query", query);
return binder.bindAndPrepare(query, metadata, accessor);
}
private ParameterBinder getBinder(List<ParameterMetadata<?>> expressions) {
return ParameterBinderFactory.createCriteriaBinder(parameters, expressions);
protected Query invokeBinding(ParameterBinder binder, Query query, JpaParametersParameterAccessor accessor) {
return binder.bindAndPrepare(query, accessor);
}
private Sort getDynamicSort(JpaParametersParameterAccessor accessor) {
@@ -366,37 +373,70 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
*/
private class CountQueryPreparer extends QueryPreparer {
CountQueryPreparer(boolean recreateQueries) {
super(recreateQueries);
}
private volatile JpqlQueryCreator cached;
@Override
protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) {
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
EntityManager entityManager = getEntityManager();
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
JpqlQueryCreator cached = this.cached;
ParameterMetadataProvider provider;
if (accessor != null) {
provider = new ParameterMetadataProvider(builder, accessor, escape);
} else {
provider = new ParameterMetadataProvider(builder, parameters, escape);
if (cached != null) {
return cached;
}
return new JpaCountQueryCreator(tree, getQueryMethod().getResultProcessor().getReturnedType(), builder, provider);
ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates);
JpaCountQueryCreator creator = new JpaCountQueryCreator(tree,
getQueryMethod().getResultProcessor().getReturnedType(), provider, templates, em);
if (!accessor.getParameters().hasDynamicProjection()) {
return this.cached = new CacheableJpqlCountQueryCreator(creator);
}
return creator;
}
/**
* Customizes binding by skipping the pagination.
*/
@Override
protected Query invokeBinding(ParameterBinder binder, TypedQuery<?> query, JpaParametersParameterAccessor accessor,
QueryParameterSetter.QueryMetadataCache metadataCache) {
protected Query invokeBinding(ParameterBinder binder, Query query, JpaParametersParameterAccessor accessor) {
return binder.bind(query, accessor);
}
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("countquery", query);
static class CacheableJpqlCountQueryCreator implements JpqlQueryCreator {
return binder.bind(query, metadata, accessor);
private final String query;
private final boolean useTupleQuery;
private final List<ParameterBinding> parameterBindings;
private final ParameterBinder binder;
public CacheableJpqlCountQueryCreator(JpqlQueryCreator delegate) {
this.query = delegate.createQuery(Sort.unsorted());
this.useTupleQuery = delegate.useTupleQuery();
this.parameterBindings = delegate.getBindings();
this.binder = delegate.getBinder();
}
@Override
public boolean useTupleQuery() {
return useTupleQuery;
}
@Override
public String createQuery(Sort sort) {
return query;
}
@Override
public List<ParameterBinding> getBindings() {
return parameterBindings;
}
@Override
public ParameterBinder getBinder() {
return binder;
}
}
}
}

View File

@@ -23,17 +23,16 @@ import jakarta.persistence.TemporalType;
import jakarta.persistence.criteria.ParameterExpression;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* The interface encapsulates the setting of query parameters which might use a significant number of variations of
@@ -45,158 +44,159 @@ import org.springframework.util.Assert;
*/
interface QueryParameterSetter {
void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
/** Noop implementation */
QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
QueryParameterSetter NOOP = (query, values, errorHandler) -> {};
/**
* {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified.
* Creates a new {@link QueryParameterSetter} for the given value extractor, JPA parameter and potentially the
* temporal type.
*
* @param valueExtractor
* @param parameter
* @param temporalType
* @return
*/
static QueryParameterSetter create(Function<JpaParametersParameterAccessor, Object> valueExtractor,
Parameter<?> parameter, @Nullable TemporalType temporalType) {
return temporalType == null ? new NamedOrIndexedQueryParameterSetter(valueExtractor, parameter)
: new TemporalParameterSetter(valueExtractor, parameter, temporalType);
}
void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler);
/**
* {@link QueryParameterSetter} for named or indexed parameters.
*/
class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter {
private final Function<JpaParametersParameterAccessor, Object> valueExtractor;
private final Parameter<?> parameter;
private final @Nullable TemporalType temporalType;
/**
* @param valueExtractor must not be {@literal null}.
* @param parameter must not be {@literal null}.
* @param temporalType may be {@literal null}.
*/
NamedOrIndexedQueryParameterSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor,
Parameter<?> parameter, @Nullable TemporalType temporalType) {
private NamedOrIndexedQueryParameterSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor,
Parameter<?> parameter) {
Assert.notNull(valueExtractor, "ValueExtractor must not be null");
this.valueExtractor = valueExtractor;
this.parameter = parameter;
this.temporalType = temporalType;
}
@Override
public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler) {
Object value = valueExtractor.apply(accessor);
try {
setParameter(query, value, errorHandler);
} catch (RuntimeException e) {
errorHandler.handleError(e);
}
}
@SuppressWarnings("unchecked")
@Override
public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor,
ErrorHandling errorHandling) {
private void setParameter(BindableQuery query, Object value, ErrorHandler errorHandler) {
if (temporalType != null) {
Object extractedValue = valueExtractor.apply(accessor);
Date value = (Date) accessor.potentiallyUnwrap(extractedValue);
// One would think we can simply use parameter to identify the parameter we want to set.
// But that does not work with list valued parameters. At least Hibernate tries to bind them by name.
// TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is
// fixed.
if (parameter instanceof ParameterExpression) {
errorHandling.execute(() -> query.setParameter((Parameter<Date>) parameter, value, temporalType));
} else if (query.hasNamedParameters() && parameter.getName() != null) {
errorHandling.execute(() -> query.setParameter(parameter.getName(), value, temporalType));
} else {
Integer position = parameter.getPosition();
if (position != null //
&& (query.getParameters().size() >= parameter.getPosition() //
|| query.registerExcessParameters() //
|| errorHandling == LENIENT)) {
errorHandling.execute(() -> query.setParameter(parameter.getPosition(), value, temporalType));
}
}
if (parameter instanceof ParameterExpression) {
query.setParameter((Parameter<Object>) parameter, value);
} else if (query.hasNamedParameters() && parameter.getName() != null) {
query.setParameter(parameter.getName(), value);
} else {
Object value = valueExtractor.apply(accessor);
Integer position = parameter.getPosition();
if (parameter instanceof ParameterExpression) {
errorHandling.execute(() -> query.setParameter((Parameter<Object>) parameter, value));
} else if (query.hasNamedParameters() && parameter.getName() != null) {
errorHandling.execute(() -> query.setParameter(parameter.getName(), value));
} else {
Integer position = parameter.getPosition();
if (position != null //
&& (query.getParameters().size() >= position //
|| errorHandling == LENIENT //
|| query.registerExcessParameters())) {
errorHandling.execute(() -> query.setParameter(position, value));
}
if (position != null //
&& (query.getParameters().size() >= position //
|| errorHandler == LENIENT //
|| query.registerExcessParameters())) {
query.setParameter(position, value);
}
}
}
}
enum ErrorHandling {
/**
* {@link QueryParameterSetter} for named or indexed parameters that have a {@link TemporalType} specified.
*/
class TemporalParameterSetter implements QueryParameterSetter {
private final Function<JpaParametersParameterAccessor, Object> valueExtractor;
private final Parameter<?> parameter;
private final TemporalType temporalType;
private TemporalParameterSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor,
Parameter<?> parameter, TemporalType temporalType) {
this.valueExtractor = valueExtractor;
this.parameter = parameter;
this.temporalType = temporalType;
}
@Override
public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler) {
Date value = (Date) accessor.potentiallyUnwrap(valueExtractor.apply(accessor));
try {
setParameter(query, value, errorHandler);
} catch (RuntimeException e) {
errorHandler.handleError(e);
}
}
@SuppressWarnings("unchecked")
private void setParameter(BindableQuery query, Date date, ErrorHandler errorHandler) {
// One would think we can simply use parameter to identify the parameter we want to set.
// But that does not work with list valued parameters. At least Hibernate tries to bind them by name.
// TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is
// fixed.
if (parameter instanceof ParameterExpression) {
query.setParameter((Parameter<Date>) parameter, date, temporalType);
} else if (query.hasNamedParameters() && parameter.getName() != null) {
query.setParameter(parameter.getName(), date, temporalType);
} else {
Integer position = parameter.getPosition();
if (position != null //
&& (query.getParameters().size() >= parameter.getPosition() //
|| query.registerExcessParameters() //
|| errorHandler == LENIENT)) {
query.setParameter(parameter.getPosition(), date, temporalType);
}
}
}
}
enum ErrorHandling implements ErrorHandler {
STRICT {
@Override
public void execute(Runnable block) {
block.run();
public void handleError(Throwable t) {
if (t instanceof RuntimeException rx) {
throw rx;
}
throw new RuntimeException(t);
}
},
LENIENT {
@Override
public void execute(Runnable block) {
try {
block.run();
} catch (RuntimeException rex) {
LOG.info("Silently ignoring", rex);
}
public void handleError(Throwable t) {
LOG.info("Silently ignoring", t);
}
};
private static final Log LOG = LogFactory.getLog(ErrorHandling.class);
abstract void execute(Runnable block);
}
/**
* Cache for {@link QueryMetadata}. Optimizes for small cache sizes on a best-effort basis.
*/
class QueryMetadataCache {
private Map<String, QueryMetadata> cache = Collections.emptyMap();
/**
* Retrieve the {@link QueryMetadata} for a given {@code cacheKey}.
*
* @param cacheKey
* @param query
* @return
*/
public QueryMetadata getMetadata(String cacheKey, Query query) {
QueryMetadata queryMetadata = cache.get(cacheKey);
if (queryMetadata == null) {
queryMetadata = new QueryMetadata(query);
Map<String, QueryMetadata> cache;
if (this.cache.isEmpty()) {
cache = Collections.singletonMap(cacheKey, queryMetadata);
} else {
cache = new HashMap<>(this.cache);
cache.put(cacheKey, queryMetadata);
}
synchronized (this) {
this.cache = cache;
}
}
return queryMetadata;
}
}
/**
@@ -224,23 +224,6 @@ interface QueryParameterSetter {
&& unwrapClass(query).getName().startsWith("org.eclipse");
}
QueryMetadata(QueryMetadata metadata) {
this.namedParameters = metadata.namedParameters;
this.parameters = metadata.parameters;
this.registerExcessParameters = metadata.registerExcessParameters;
}
/**
* Create a {@link BindableQuery} for a {@link Query}.
*
* @param query
* @return
*/
public BindableQuery withQuery(Query query) {
return new BindableQuery(this, query);
}
/**
* @return
*/
@@ -294,13 +277,7 @@ interface QueryParameterSetter {
private final Query query;
private final Query unwrapped;
BindableQuery(QueryMetadata metadata, Query query) {
super(metadata);
this.query = query;
this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query;
}
private BindableQuery(Query query) {
BindableQuery(Query query) {
super(query);
this.query = query;
this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query;

View File

@@ -18,7 +18,6 @@ package org.springframework.data.jpa.repository.query;
import jakarta.persistence.Query;
import jakarta.persistence.TemporalType;
import java.util.List;
import java.util.function.Function;
import org.springframework.data.expression.ValueEvaluationContext;
@@ -28,8 +27,6 @@ import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.spel.EvaluationContextProvider;
@@ -49,36 +46,45 @@ import org.springframework.util.Assert;
*/
abstract class QueryParameterSetterFactory {
/**
* Creates a {@link QueryParameterSetter} for the given {@link ParameterBinding}. This factory may return
* {@literal null} if it doesn't support the given {@link ParameterBinding}.
*
* @param binding the parameter binding to create a {@link QueryParameterSetter} for.
* @return
*/
@Nullable
abstract QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery);
abstract QueryParameterSetter create(ParameterBinding binding);
/**
* Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}.
*
* @param parameters must not be {@literal null}.
* @param preferNamedParameters whether to prefer named parameters.
* @return a basic {@link QueryParameterSetterFactory} that can handle named and index parameters.
*/
static QueryParameterSetterFactory basic(JpaParameters parameters) {
Assert.notNull(parameters, "JpaParameters must not be null");
return new BasicQueryParameterSetterFactory(parameters);
static QueryParameterSetterFactory basic(JpaParameters parameters, boolean preferNamedParameters) {
return new BasicQueryParameterSetterFactory(parameters, preferNamedParameters);
}
/**
* Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and
* {@link ParameterMetadata}.
* Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters}.
*
* @param parameters must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @return a {@link QueryParameterSetterFactory} for criteria Queries.
* @return a {@link QueryParameterSetterFactory} for Part-Tree Queries.
*/
static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, List<ParameterMetadata<?>> metadata) {
static QueryParameterSetterFactory forPartTreeQuery(JpaParameters parameters) {
return new PartTreeQueryParameterSetterFactory(parameters);
}
Assert.notNull(parameters, "JpaParameters must not be null");
Assert.notNull(metadata, "ParameterMetadata must not be null");
return new CriteriaQueryParameterSetterFactory(parameters, metadata);
/**
* Creates a new {@link QueryParameterSetterFactory} to bind
* {@link org.springframework.data.jpa.repository.query.ParameterBinding.Synthetic} parameters.
*
* @return a {@link QueryParameterSetterFactory} for JPQL Queries.
*/
static QueryParameterSetterFactory forSynthetic() {
return new SyntheticParameterSetterFactory();
}
/**
@@ -93,10 +99,6 @@ abstract class QueryParameterSetterFactory {
*/
static QueryParameterSetterFactory parsing(ValueExpressionParser parser,
ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(parser, "ValueExpressionParser must not be null");
Assert.notNull(evaluationContextProvider, "ValueEvaluationContextProvider must not be null");
return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider);
}
@@ -115,7 +117,7 @@ abstract class QueryParameterSetterFactory {
? parameter.getRequiredTemporalType() //
: null;
return new NamedOrIndexedQueryParameterSetter(valueExtractor.andThen(binding::prepare),
return QueryParameterSetter.create(valueExtractor.andThen(binding::prepare),
ParameterImpl.of(parameter, binding), temporalType);
}
@@ -182,7 +184,7 @@ abstract class QueryParameterSetterFactory {
@Nullable
@Override
public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
public QueryParameterSetter create(ParameterBinding binding) {
if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) {
return null;
@@ -206,6 +208,25 @@ abstract class QueryParameterSetterFactory {
}
}
/**
* Handles synthetic bindings that have been captured during parameter augmenting.
*
* @author Mark Paluch
* @since 4.0
*/
private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory {
@Override
public QueryParameterSetter create(ParameterBinding binding) {
if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) {
return null;
}
return createSetter(values -> s.value(), binding, null);
}
}
/**
* Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters.
*
@@ -217,30 +238,33 @@ abstract class QueryParameterSetterFactory {
private static class BasicQueryParameterSetterFactory extends QueryParameterSetterFactory {
private final JpaParameters parameters;
private final boolean preferNamedParameters;
/**
* @param parameters must not be {@literal null}.
* @param preferNamedParameters whether to use named parameters.
*/
BasicQueryParameterSetterFactory(JpaParameters parameters) {
BasicQueryParameterSetterFactory(JpaParameters parameters, boolean preferNamedParameters) {
Assert.notNull(parameters, "JpaParameters must not be null");
this.parameters = parameters;
this.preferNamedParameters = preferNamedParameters;
}
@Override
public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
public QueryParameterSetter create(ParameterBinding binding) {
Assert.notNull(binding, "Binding must not be null");
JpaParameter parameter;
if (!(binding.getOrigin() instanceof MethodInvocationArgument mia)) {
return QueryParameterSetter.NOOP;
return null;
}
BindingIdentifier identifier = mia.identifier();
JpaParameter parameter;
if (declaredQuery.hasNamedParameter() && identifier.hasName()) {
if (preferNamedParameters && identifier.hasName()) {
parameter = findParameterForBinding(parameters, identifier.getName());
} else if (identifier.hasPosition()) {
parameter = findParameterForBinding(parameters, identifier.getPosition() - 1);
@@ -255,7 +279,7 @@ abstract class QueryParameterSetterFactory {
}
@Nullable
private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) {
protected Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) {
return accessor.getValue(parameter);
}
}
@@ -263,60 +287,46 @@ abstract class QueryParameterSetterFactory {
/**
* @author Jens Schauder
* @author Oliver Gierke
* @author Mark Paluch
* @see QueryParameterSetterFactory
*/
private static class CriteriaQueryParameterSetterFactory extends QueryParameterSetterFactory {
private static class PartTreeQueryParameterSetterFactory extends BasicQueryParameterSetterFactory {
private final JpaParameters parameters;
private final List<ParameterMetadata<?>> parameterMetadata;
/**
* Creates a new {@link QueryParameterSetterFactory} from the given {@link JpaParameters} and
* {@link ParameterMetadata}.
*
* @param parameters must not be {@literal null}.
* @param metadata must not be {@literal null}.
*/
CriteriaQueryParameterSetterFactory(JpaParameters parameters, List<ParameterMetadata<?>> metadata) {
Assert.notNull(parameters, "JpaParameters must not be null");
Assert.notNull(metadata, "Expressions must not be null");
this.parameters = parameters;
this.parameterMetadata = metadata;
private PartTreeQueryParameterSetterFactory(JpaParameters parameters) {
super(parameters, false);
this.parameters = parameters.getBindableParameters();
}
@Override
public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) {
public QueryParameterSetter create(ParameterBinding binding) {
if (!binding.getOrigin().isMethodArgument()) {
return null;
}
int parameterIndex = binding.getRequiredPosition() - 1;
Assert.isTrue( //
parameterIndex < parameterMetadata.size(), //
parameterIndex < parameters.getNumberOfParameters(), //
() -> String.format( //
"At least %s parameter(s) provided but only %s parameter(s) present in query", //
binding.getRequiredPosition(), //
parameterMetadata.size() //
parameters.getNumberOfParameters() //
) //
);
ParameterMetadata<?> metadata = parameterMetadata.get(parameterIndex);
if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) {
if (metadata.isIsNullParameter()) {
return QueryParameterSetter.NOOP;
if (ptb.isIsNullParameter()) {
return QueryParameterSetter.NOOP;
}
return super.create(binding);
}
JpaParameter parameter = parameters.getBindableParameter(parameterIndex);
TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getRequiredTemporalType() : null;
return new NamedOrIndexedQueryParameterSetter(values -> getAndPrepare(parameter, metadata, values),
metadata.getExpression(), temporalType);
}
@Nullable
private Object getAndPrepare(JpaParameter parameter, ParameterMetadata<?> metadata,
JpaParametersParameterAccessor accessor) {
return metadata.prepare(accessor.getValue(parameter));
return null;
}
}
@@ -360,7 +370,6 @@ abstract class QueryParameterSetterFactory {
public Class<T> getParameterType() {
return parameterType;
}
}
}

View File

@@ -801,7 +801,7 @@ public abstract class QueryUtils {
* @param hasRequiredOuterJoin has a parent already required an outer join?
* @return whether an outer join is to be used for integrating this attribute in a query.
*/
private static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean isForSelection,
static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean isForSelection,
boolean hasRequiredOuterJoin) {
// already inner joined so outer join is useless
@@ -871,7 +871,7 @@ public abstract class QueryUtils {
* @param joinType the join type to create if none was found
* @return will never be {@literal null}.
*/
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute, JoinType joinType) {
static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute, JoinType joinType) {
for (Fetch<?, ?> fetch : from.getFetches()) {

View File

@@ -50,7 +50,6 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery {
private final StoredProcedureAttributes procedureAttributes;
private final boolean useNamedParameters;
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
/**
* Creates a new {@link StoredProcedureJpaQuery}.
@@ -90,9 +89,7 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery {
protected StoredProcedureQuery doCreateQuery(JpaParametersParameterAccessor accessor) {
StoredProcedureQuery storedProcedure = createStoredProcedure();
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("singleton", storedProcedure);
return parameterBinder.get().bind(storedProcedure, metadata, accessor);
return parameterBinder.get().bind(storedProcedure, accessor);
}
@Override

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;
import java.util.function.Function;
/**
* @author Mark Paluch
*/
public class JpqlQueryTemplates {
public static final JpqlQueryTemplates UPPER = new JpqlQueryTemplates("UPPER", String::toUpperCase);
public static final JpqlQueryTemplates LOWER = new JpqlQueryTemplates("LOWER", String::toLowerCase);
private final String ignoreCaseOperator;
private final Function<String, String> ignoreCaseFunction;
JpqlQueryTemplates(String ignoreCaseOperator, Function<String, String> ignoreCaseFunction) {
this.ignoreCaseOperator = ignoreCaseOperator;
this.ignoreCaseFunction = ignoreCaseFunction;
}
public static JpqlQueryTemplates of(String ignoreCaseOperator, Function<String, String> ignoreCaseFunction) {
return new JpqlQueryTemplates(ignoreCaseOperator, ignoreCaseFunction);
}
public String ignoreCase(String value) {
return ignoreCaseFunction.apply(value);
}
public String getIgnoreCaseOperator() {
return ignoreCaseOperator;
}
}

View File

@@ -36,6 +36,10 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests {
@Override
void executesInKeywordForPageCorrectly() {}
@Disabled
@Override
void shouldProjectWithKeysetScrolling() {}
@Disabled
@Override
void rawMapProjectionWithEntityAndAggregatedValue() {}

View File

@@ -3318,6 +3318,15 @@ class UserRepositoryTests {
flushTestUsers();
/*
TODO: Hibernate-generated HQL for the CriteriaBuilder-based API. Yields only one result in contrast to the CriteriaBuilder one.
Query query = em.createQuery("select alias_544097980 from org.springframework.data.jpa.domain.sample.User alias_544097980 left join alias_544097980.attributes alias_975381534 where alias_975381534 in (?1)")
.setParameter(1, asList("cOOl", "hIP"));
List resultList = query.getResultList();
*/
List<User> result = repository.findByAttributesIgnoreCaseIn(new HashSet<>(asList("cOOl", "hIP")));
assertThat(result).containsOnly(firstUser, secondUser);

View File

@@ -19,21 +19,17 @@ import static org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import java.lang.reflect.Method;
import java.util.List;
import org.hibernate.query.spi.SqmQuery;
import org.hibernate.query.sqm.tree.expression.SqmDistinct;
import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.query.sqm.tree.select.SqmSelectClause;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
@@ -63,25 +59,15 @@ class JpaCountQueryCreatorIntegrationTests {
AbstractRepositoryMetadata.getMetadata(SomeRepository.class), new SpelAwareProxyProjectionFactory(), provider);
PartTree tree = new PartTree("findDistinctByRolesIn", User.class);
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(entityManager.getCriteriaBuilder(),
queryMethod.getParameters(), EscapeCharacter.DEFAULT);
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(
queryMethod.getParameters(), EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER);
JpaCountQueryCreator creator = new JpaCountQueryCreator(tree, queryMethod.getResultProcessor().getReturnedType(),
entityManager.getCriteriaBuilder(), metadataProvider);
metadataProvider, JpqlQueryTemplates.UPPER, entityManager);
TypedQuery<? extends Object> query = entityManager.createQuery(creator.createQuery());
String query = creator.createQuery();
SqmQuery sqmQuery = ((SqmQuery) query);
SqmSelectStatement<?> select = (SqmSelectStatement<?>) sqmQuery.getSqmStatement();
// Verify distinct (should this even be there for a count query?)
SqmSelectClause clause = select.getQuerySpec().getSelectClause();
assertThat(clause.isDistinct()).isTrue();
// Verify count(distinct(…))
SqmFunction<?> function = ((SqmFunction<?>) clause.getSelectionItems().get(0));
assertThat(function.getFunctionName()).isEqualTo("count");
assertThat(function.getArguments().get(0)).isInstanceOf(SqmDistinct.class);
assertThat(query).startsWith("SELECT COUNT(DISTINCT u)");
}
interface SomeRepository extends Repository<User, Integer> {

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.lang.reflect.Method;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Unit tests for {@link JpaKeysetScrollQueryCreator}.
*
* @author Mark Paluch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:infrastructure.xml")
class JpaKeysetScrollQueryCreatorTests {
@PersistenceContext EntityManager entityManager;
@Test // GH-3588
void shouldCreateContinuationQuery() throws Exception {
Map<String, Object> keys = Map.of("id", "10", "firstname", "John", "emailAddress", "john@example.com");
KeysetScrollPosition position = ScrollPosition.of(keys, ScrollPosition.Direction.BACKWARD);
Method method = MyRepo.class.getMethod("findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc",
String.class, ScrollPosition.class);
PersistenceProvider provider = PersistenceProvider.fromEntityManager(entityManager);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, AbstractRepositoryMetadata.getMetadata(MyRepo.class),
new SpelAwareProxyProjectionFactory(), provider);
PartTree tree = new PartTree("findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc", User.class);
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(
queryMethod.getParameters(), EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER);
JpaMetamodelEntityInformation<User, User> entityInformation = new JpaMetamodelEntityInformation<>(User.class,
entityManager.getMetamodel(), entityManager.getEntityManagerFactory().getPersistenceUnitUtil());
JpaKeysetScrollQueryCreator creator = new JpaKeysetScrollQueryCreator(tree,
queryMethod.getResultProcessor().getReturnedType(), metadataProvider, JpqlQueryTemplates.UPPER,
entityInformation, position, entityManager);
String query = creator.createQuery();
assertThat(query).containsIgnoringWhitespaces("""
SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE (u.firstname LIKE ?1 ESCAPE '\\')
AND (u.firstname < ?2
OR u.firstname = ?3 AND u.emailAddress < ?4
OR u.firstname = ?5 AND u.emailAddress = ?6 AND u.id < ?7)
ORDER BY u.firstname desc, u.emailAddress desc, u.id desc
""");
}
interface MyRepo extends Repository<User, String> {
Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(String firstname,
ScrollPosition position);
}
}

View File

@@ -69,7 +69,7 @@ class JpaParametersParameterAccessorTests {
private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) {
ParameterBinderFactory.createBinder(parameters)
ParameterBinderFactory.createBinder(parameters, true)
.bind( //
QueryParameterSetter.BindableQuery.from(query), //
accessor, //

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import static jakarta.persistence.TemporalType.*;
import static java.util.Arrays.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.*;
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
import jakarta.persistence.Parameter;
@@ -34,7 +35,8 @@ import java.util.function.Function;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.*;
/**
* Unit tests fir {@link NamedOrIndexedQueryParameterSetter}.
@@ -79,7 +81,7 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
for (Parameter parameter : parameters) {
for (TemporalType temporalType : temporalTypes) {
NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( //
QueryParameterSetter setter = QueryParameterSetter.create( //
firstValueExtractor, //
parameter, //
temporalType //
@@ -87,7 +89,7 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
softly
.assertThatThrownBy(
() -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, STRICT)) //
() -> setter.setParameter(BindableQuery.from(query), methodArguments, STRICT)) //
.describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", //
parameter.getClass(), //
parameter.getName(), //
@@ -108,7 +110,7 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
for (Parameter<?> parameter : parameters) {
for (TemporalType temporalType : temporalTypes) {
NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( //
QueryParameterSetter setter = QueryParameterSetter.create( //
firstValueExtractor, //
parameter, //
temporalType //
@@ -116,7 +118,7 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
softly
.assertThatCode(
() -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT)) //
() -> setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT)) //
.describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", //
parameter.getClass(), //
parameter.getName(), //
@@ -141,13 +143,13 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
for (TemporalType temporalType : temporalTypes) {
NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( //
QueryParameterSetter setter = QueryParameterSetter.create( //
firstValueExtractor, //
new ParameterImpl(null, 11), // parameter position is beyond number of parametes in query (0)
temporalType //
);
setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT);
setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT);
if (temporalType == null) {
verify(query).setParameter(eq(11), any(Date.class));
@@ -171,13 +173,13 @@ class NamedOrIndexedQueryParameterSetterUnitTests {
for (TemporalType temporalType : temporalTypes) {
NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( //
QueryParameterSetter setter = QueryParameterSetter.create( //
firstValueExtractor, //
new ParameterImpl(null, null), // no position (and no name) makes a success of a setParameter impossible
temporalType //
);
setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT);
setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT);
if (temporalType == null) {
verify(query, never()).setParameter(anyInt(), any(Date.class));

View File

@@ -274,13 +274,13 @@ class ParameterBinderUnitTests {
}
private void bind(Method method, JpaParameters parameters, Object[] values) {
ParameterBinderFactory.createBinder(parameters).bind(QueryParameterSetter.BindableQuery.from(query),
ParameterBinderFactory.createBinder(parameters, false).bind(QueryParameterSetter.BindableQuery.from(query),
getAccessor(method, values), QueryParameterSetter.ErrorHandling.STRICT);
}
private void bindAndPrepare(Method method, Object[] values) {
ParameterBinderFactory.createBinder(createParameters(method)).bindAndPrepare(query,
new QueryParameterSetter.QueryMetadata(query), getAccessor(method, values));
ParameterBinderFactory.createBinder(createParameters(method), false).bindAndPrepare(query,
getAccessor(method, values));
}
private JpaParametersParameterAccessor getAccessor(Method method, Object... values) {

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2017-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.ParameterExpression;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Integration tests for {@link ParameterMetadataProvider}.
*
* @author Oliver Gierke
* @author Jens Schauder
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:infrastructure.xml")
class ParameterExpressionProviderTests {
@PersistenceContext EntityManager em;
@Test // DATADOC-99
@SuppressWarnings("rawtypes")
void createsParameterExpressionWithMostConcreteType() throws Exception {
Method method = SampleRepository.class.getMethod("findByIdGreaterThan", int.class);
Parameters<?, ?> parameters = new DefaultParameters(ParametersSource.of(method));
ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, new Object[] { 1 });
Part part = new Part("IdGreaterThan", User.class);
CriteriaBuilder builder = em.getCriteriaBuilder();
ParameterMetadataProvider provider = new ParameterMetadataProvider(builder, accessor, EscapeCharacter.DEFAULT);
ParameterExpression<? extends Comparable> expression = provider.next(part, Comparable.class).getExpression();
assertThat(expression.getParameterType()).isEqualTo(Integer.class);
}
interface SampleRepository {
User findByIdGreaterThan(int id);
}
}

View File

@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
@@ -48,14 +48,14 @@ import org.springframework.test.util.ReflectionTestUtils;
class ParameterMetadataProviderIntegrationTests {
@PersistenceContext EntityManager em;
/* TODO
@Test // DATAJPA-758
void forwardsParameterNameIfTransparentlyNamed() throws Exception {
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class));
ParameterMetadata<Object> metadata = provider.next(new Part("firstname", User.class));
assertThat(metadata.getExpression().getName()).isEqualTo("name");
assertThat(metadata.getName()).isEqualTo("name");
}
@Test // DATAJPA-758
@@ -65,15 +65,15 @@ class ParameterMetadataProviderIntegrationTests {
ParameterMetadata<Object> metadata = provider.next(new Part("lastname", User.class));
assertThat(metadata.getExpression().getName()).isNull();
}
} */
@Test // DATAJPA-772
void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception {
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByAgeContaining", Integer.class));
ParameterMetadata<Object> metadata = provider.next(new Part("ageContaining", User.class));
ParameterBinding.PartTreeParameterBinding binding = provider.next(new Part("ageContaining", User.class));
assertThat(metadata.prepare(1)).isEqualTo(1);
assertThat(binding.prepare(1)).isEqualTo(1);
}
private ParameterMetadataProvider createProvider(Method method) {
@@ -81,7 +81,8 @@ class ParameterMetadataProviderIntegrationTests {
JpaParameters parameters = new JpaParameters(ParametersSource.of(method));
simulateDiscoveredParametername(parameters);
return new ParameterMetadataProvider(em.getCriteriaBuilder(), parameters, EscapeCharacter.DEFAULT);
return new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
JpqlQueryTemplates.UPPER);
}
@SuppressWarnings({ "unchecked", "ConstantConditions" })

View File

@@ -18,8 +18,6 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.util.Collections;
import org.eclipse.persistence.internal.jpa.querydef.ParameterExpressionImpl;
@@ -30,7 +28,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.parser.Part;
/**
@@ -51,13 +50,11 @@ class ParameterMetadataProviderUnitTests {
@Test // DATAJPA-863
void errorMessageMentionsParametersWhenParametersAreExhausted() {
CriteriaBuilder builder = mock(CriteriaBuilder.class);
Parameters<?, ?> parameters = mock(Parameters.class, RETURNS_DEEP_STUBS);
JpaParameters parameters = mock(JpaParameters.class, RETURNS_DEEP_STUBS);
when(parameters.getBindableParameters().iterator()).thenReturn(Collections.emptyListIterator());
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(builder, parameters,
EscapeCharacter.DEFAULT);
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters,
EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER);
assertThatExceptionOfType(RuntimeException.class) //
.isThrownBy(() -> metadataProvider.next(mock(Part.class))) //
@@ -68,6 +65,7 @@ class ParameterMetadataProviderUnitTests {
void returnAugmentedValueForStringExpressions() {
when(part.getProperty().getLeafProperty().isCollection()).thenReturn(false);
when(part.getProperty().getType()).thenReturn((Class) String.class);
assertThat(createParameterMetadata(Part.Type.STARTING_WITH).prepare("starting with")).isEqualTo("starting with%");
assertThat(createParameterMetadata(Part.Type.ENDING_WITH).prepare("ending with")).isEqualTo("%ending with");
@@ -82,6 +80,6 @@ class ParameterMetadataProviderUnitTests {
private ParameterMetadataProvider.ParameterMetadata createParameterMetadata(Part.Type partType) {
when(part.getType()).thenReturn(partType);
return new ParameterMetadataProvider.ParameterMetadata<>(parameterExpression, part, null, EscapeCharacter.DEFAULT);
return new ParameterMetadataProvider.ParameterMetadata(part.getProperty().getType(), part, null, EscapeCharacter.DEFAULT, 1, JpqlQueryTemplates.LOWER);
}
}

View File

@@ -17,9 +17,7 @@ import org.springframework.aop.framework.Advised;
*/
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.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@@ -39,9 +37,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.HibernateUtils;
import org.springframework.data.jpa.provider.PersistenceProvider;
@@ -151,7 +151,7 @@ class PartTreeJpaQueryIntegrationTests {
Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {}));
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is empty");
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles IS EMPTY");
}
@Test // DATAJPA-1074, HHH-15432
@@ -162,7 +162,18 @@ class PartTreeJpaQueryIntegrationTests {
Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {}));
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is not empty");
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles IS NOT EMPTY");
}
@Test //
void containingCollection() throws Exception {
JpaQueryMethod queryMethod = getQueryMethod("findByRolesContaining", Role.class);
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager);
Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { new Role() }));
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("MEMBER OF u.roles");
}
@Test // DATAJPA-1074
@@ -170,7 +181,8 @@ class PartTreeJpaQueryIntegrationTests {
JpaQueryMethod method = getQueryMethod("findByFirstnameIsEmpty");
assertThatIllegalArgumentException().isThrownBy(() -> new PartTreeJpaQuery(method, entityManager));
assertThatIllegalArgumentException().isThrownBy(
() -> new PartTreeJpaQuery(method, entityManager).createQuery(getAccessor(method, new Object[] {})));
}
@Test // DATAJPA-1182
@@ -297,6 +309,8 @@ class PartTreeJpaQueryIntegrationTests {
List<User> findByFirstnameIsEmpty();
List<User> findByRolesContaining(Role role);
// should fail, since we can't compare scalar values to collections
List<User> findById(Collection<Integer> ids);

View File

@@ -18,13 +18,12 @@ package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
@@ -48,12 +47,12 @@ class QueryParameterSetterFactoryUnitTests {
// we have one bindable parameter
when(parameters.getBindableParameters().iterator()).thenReturn(Stream.of(mock(JpaParameter.class)).iterator());
setterFactory = QueryParameterSetterFactory.basic(parameters);
setterFactory = QueryParameterSetterFactory.basic(parameters, true);
}
@Test // DATAJPA-1058
void noExceptionWhenQueryDoesNotContainNamedParameters() {
setterFactory.create(binding, DeclaredQuery.of("from Employee e", false));
setterFactory.create(binding);
}
@Test // DATAJPA-1058
@@ -62,8 +61,8 @@ class QueryParameterSetterFactoryUnitTests {
when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter("NamedParameter", 1));
assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> setterFactory.create(binding,
DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) //
.isThrownBy(() -> setterFactory.create(binding
)) //
.withMessageContaining("Java 8") //
.withMessageContaining("@Param") //
.withMessageContaining("-parameters");
@@ -73,16 +72,14 @@ class QueryParameterSetterFactoryUnitTests {
void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() {
// no parameter present in the criteria query
List<ParameterMetadataProvider.ParameterMetadata<?>> metadata = Collections.emptyList();
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata);
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forPartTreeQuery(parameters);
// one argument present in the method signature
when(binding.getRequiredPosition()).thenReturn(1);
when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1));
assertThatExceptionOfType(IllegalArgumentException.class) //
.isThrownBy(() -> setterFactory.create(binding,
DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) //
.isThrownBy(() -> setterFactory.create(binding)) //
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query");
}
@@ -90,14 +87,14 @@ class QueryParameterSetterFactoryUnitTests {
void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() {
// no parameter present in the criteria query
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters);
QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters, false);
// one argument present in the method signature
when(binding.getRequiredPosition()).thenReturn(1);
when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1));
assertThatExceptionOfType(IllegalArgumentException.class) //
.isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = ?1", false))) //
.isThrownBy(() -> setterFactory.create(binding)) //
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query");
}
}

View File

@@ -32,7 +32,7 @@ public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
----
We create a query using the JPA criteria API from this, but, essentially, this translates into the following query: `select u from User u where u.emailAddress = ?1 and u.lastname = ?2`. Spring Data JPA does a property check and traverses nested properties, as described in xref:repositories/query-methods-details.adoc#repositories.query-methods.query-property-expressions[Property Expressions].
We create a query using JPQL translating into the following query: `select u from User u where u.emailAddress = ?1 and u.lastname = ?2`. Spring Data JPA does a property check and traverses nested properties, as described in xref:repositories/query-methods-details.adoc#repositories.query-methods.query-property-expressions[Property Expressions].
====
The following table describes the keywords supported for JPA and what a method containing that keyword translates to: