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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,10 @@ class EclipseLinkUserRepositoryFinderTests extends UserRepositoryFinderTests {
|
||||
@Override
|
||||
void executesInKeywordForPageCorrectly() {}
|
||||
|
||||
@Disabled
|
||||
@Override
|
||||
void shouldProjectWithKeysetScrolling() {}
|
||||
|
||||
@Disabled
|
||||
@Override
|
||||
void rawMapProjectionWithEntityAndAggregatedValue() {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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, //
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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" })
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user