Polishing.
Make usage of ParameterExpression more explicit. Add JPQL rendering tests. Favor Metamodel over From for building jpql queries. Align IsNull and IsNotNull handling. Support Derived Delete and Exists, consider null values when caching queries. See #3588 Original pull request: #3653
This commit is contained in:
committed by
Mark Paluch
parent
d131eb7a4a
commit
b69ef2b121
@@ -79,7 +79,7 @@ class JpaKeysetScrollQueryCreator extends JpaQueryCreator {
|
||||
JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> {
|
||||
|
||||
syntheticBindings.add(provider.nextSynthetic(value, scrollPosition));
|
||||
return JpqlQueryBuilder.expression(render(counter.incrementAndGet()));
|
||||
return placeholder(counter.incrementAndGet());
|
||||
});
|
||||
JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate);
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class JpaParameters extends Parameters<JpaParameters, JpaParameter> {
|
||||
super(parametersSource, parameterFactory);
|
||||
}
|
||||
|
||||
private JpaParameters(List<JpaParameter> parameters) {
|
||||
JpaParameters(List<JpaParameter> parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,30 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.springframework.data.repository.query.parser.Part.Type.*;
|
||||
import static org.springframework.data.repository.query.parser.Part.Type.IS_NOT_EMPTY;
|
||||
import static org.springframework.data.repository.query.parser.Part.Type.NOT_CONTAINING;
|
||||
import static org.springframework.data.repository.query.parser.Part.Type.NOT_LIKE;
|
||||
import static org.springframework.data.repository.query.parser.Part.Type.SIMPLE_PROPERTY;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.From;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.metamodel.Attribute;
|
||||
import jakarta.persistence.metamodel.Bindable;
|
||||
import jakarta.persistence.metamodel.EntityType;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
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.domain.JpaSort;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
|
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding;
|
||||
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
|
||||
@@ -56,6 +63,7 @@ import org.springframework.util.Assert;
|
||||
* @author Moritz Becker
|
||||
* @author Andrey Kovalev
|
||||
* @author Greg Turnquist
|
||||
* @author Christoph Strobl
|
||||
* @author Jinmyeong Kim
|
||||
*/
|
||||
class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
|
||||
@@ -66,8 +74,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
private final PartTree tree;
|
||||
private final EscapeCharacter escape;
|
||||
private final EntityType<?> entityType;
|
||||
private final From<?, ?> from;
|
||||
private final JpqlQueryBuilder.Entity entity;
|
||||
private final Metamodel metamodel;
|
||||
|
||||
/**
|
||||
* Create a new {@link JpaQueryCreator}.
|
||||
@@ -88,12 +96,12 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
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());
|
||||
this.metamodel = em.getMetamodel();
|
||||
}
|
||||
|
||||
From<?, ?> getFrom() {
|
||||
return from;
|
||||
Bindable<?> getFrom() {
|
||||
return entityType;
|
||||
}
|
||||
|
||||
JpqlQueryBuilder.Entity getEntity() {
|
||||
@@ -175,7 +183,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
QueryUtils.checkSortExpression(order);
|
||||
|
||||
try {
|
||||
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from,
|
||||
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
|
||||
PropertyPath.from(order.getProperty(), entityType.getJavaType())));
|
||||
} catch (PropertyReferenceException e) {
|
||||
|
||||
@@ -210,12 +218,19 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
|
||||
if (returnedType.needsCustomConstruction()) {
|
||||
|
||||
Collection<String> requiredSelection = getRequiredSelection(sort, returnedType);
|
||||
Collection<String> requiredSelection = null;
|
||||
if (returnedType.getReturnedType().getPackageName().startsWith("java.util")
|
||||
|| returnedType.getReturnedType().getPackageName().startsWith("jakarta.persistence")) {
|
||||
requiredSelection = metamodel.managedType(returnedType.getDomainType()).getAttributes().stream()
|
||||
.map(Attribute::getName).collect(Collectors.toList());
|
||||
} else {
|
||||
requiredSelection = getRequiredSelection(sort, returnedType);
|
||||
}
|
||||
|
||||
List<PathAndOrigin> paths = new ArrayList<>(requiredSelection.size());
|
||||
for (String selection : requiredSelection) {
|
||||
paths.add(
|
||||
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(selection, from.getJavaType()), true));
|
||||
paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
|
||||
PropertyPath.from(selection, returnedType.getDomainType()), true));
|
||||
}
|
||||
|
||||
if (useTupleQuery()) {
|
||||
@@ -231,14 +246,14 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
if (entityType.hasSingleIdAttribute()) {
|
||||
|
||||
SingularAttribute<?, ?> id = entityType.getId(entityType.getIdType().getJavaType());
|
||||
return selectStep.select(
|
||||
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(id.getName(), from.getJavaType()), true));
|
||||
return selectStep.select(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
|
||||
PropertyPath.from(id.getName(), returnedType.getDomainType()), true));
|
||||
|
||||
} else {
|
||||
|
||||
List<PathAndOrigin> paths = entityType.getIdClassAttributes().stream()//
|
||||
.map(it -> JpqlUtils.toExpressionRecursively(entity, from,
|
||||
PropertyPath.from(it.getName(), from.getJavaType()), true))
|
||||
.map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
|
||||
PropertyPath.from(it.getName(), returnedType.getDomainType()), true))
|
||||
.toList();
|
||||
return selectStep.select(paths);
|
||||
}
|
||||
@@ -255,12 +270,12 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
return returnedType.getInputProperties();
|
||||
}
|
||||
|
||||
String render(ParameterBinding binding) {
|
||||
return render(binding.getRequiredPosition());
|
||||
JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) {
|
||||
return placeholder(binding.getRequiredPosition());
|
||||
}
|
||||
|
||||
String render(int position) {
|
||||
return "?" + position;
|
||||
JpqlQueryBuilder.Expression placeholder(int position) {
|
||||
return JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(position));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,7 +320,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
PropertyPath property = part.getProperty();
|
||||
Type type = part.getType();
|
||||
|
||||
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(entity, from, property);
|
||||
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
|
||||
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas);
|
||||
JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas));
|
||||
|
||||
@@ -313,25 +328,25 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
case BETWEEN:
|
||||
PartTreeParameterBinding first = provider.next(part);
|
||||
ParameterBinding second = provider.next(part);
|
||||
return where.between(render(first), render(second));
|
||||
return where.between(placeholder(first), placeholder(second));
|
||||
case AFTER:
|
||||
case GREATER_THAN:
|
||||
return where.gt(render(provider.next(part)));
|
||||
return where.gt(placeholder(provider.next(part)));
|
||||
case GREATER_THAN_EQUAL:
|
||||
return where.gte(render(provider.next(part)));
|
||||
return where.gte(placeholder(provider.next(part)));
|
||||
case BEFORE:
|
||||
case LESS_THAN:
|
||||
return where.lt(render(provider.next(part)));
|
||||
return where.lt(placeholder(provider.next(part)));
|
||||
case LESS_THAN_EQUAL:
|
||||
return where.lte(render(provider.next(part)));
|
||||
return where.lte(placeholder(provider.next(part)));
|
||||
case IS_NULL:
|
||||
return where.isNull();
|
||||
case IS_NOT_NULL:
|
||||
return where.isNotNull();
|
||||
case NOT_IN:
|
||||
return whereIgnoreCase.notIn(render(provider.next(part, Collection.class)));
|
||||
return whereIgnoreCase.notIn(placeholder(provider.next(part, Collection.class)));
|
||||
case IN:
|
||||
return whereIgnoreCase.in(render(provider.next(part, Collection.class)));
|
||||
return whereIgnoreCase.in(placeholder(provider.next(part, Collection.class)));
|
||||
case STARTING_WITH:
|
||||
case ENDING_WITH:
|
||||
case CONTAINING:
|
||||
@@ -340,8 +355,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
if (property.getLeafProperty().isCollection()) {
|
||||
where = JpqlQueryBuilder.where(entity, property);
|
||||
|
||||
return type.equals(NOT_CONTAINING) ? where.notMemberOf(render(provider.next(part)))
|
||||
: where.memberOf(render(provider.next(part)));
|
||||
return type.equals(NOT_CONTAINING) ? where.notMemberOf(placeholder(provider.next(part)))
|
||||
: where.memberOf(placeholder(provider.next(part)));
|
||||
}
|
||||
|
||||
case LIKE:
|
||||
@@ -349,7 +364,7 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
|
||||
PartTreeParameterBinding parameter = provider.next(part, String.class);
|
||||
JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(),
|
||||
JpqlQueryBuilder.parameter(render(parameter)));
|
||||
placeholder(parameter));
|
||||
// Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
|
||||
String escapeChar = Character.toString(escape.getEscapeCharacter());
|
||||
return
|
||||
@@ -362,23 +377,16 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
case FALSE:
|
||||
return where.isFalse();
|
||||
case SIMPLE_PROPERTY:
|
||||
case NEGATING_SIMPLE_PROPERTY:
|
||||
|
||||
PartTreeParameterBinding simple = provider.next(part);
|
||||
|
||||
if (simple.isIsNullParameter()) {
|
||||
return where.isNull();
|
||||
return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull();
|
||||
}
|
||||
|
||||
return whereIgnoreCase.eq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(simple))));
|
||||
case NEGATING_SIMPLE_PROPERTY:
|
||||
|
||||
PartTreeParameterBinding negating = provider.next(part);
|
||||
|
||||
if (negating.isIsNullParameter()) {
|
||||
return where.isNotNull();
|
||||
}
|
||||
|
||||
return whereIgnoreCase
|
||||
.neq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(negating))));
|
||||
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata));
|
||||
return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression);
|
||||
case IS_EMPTY:
|
||||
case IS_NOT_EMPTY:
|
||||
|
||||
@@ -412,8 +420,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
|
||||
* @param path must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin pas) {
|
||||
return potentiallyIgnoreCase(pas.path(), JpqlQueryBuilder.expression(pas));
|
||||
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) {
|
||||
return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
|
||||
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_ASC;
|
||||
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DESC;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -32,7 +33,9 @@ import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.util.Predicates;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A Domain-Specific Language to build JPQL queries using Java code.
|
||||
@@ -189,7 +192,7 @@ public final class JpqlQueryBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple expression from a string.
|
||||
* Create a simple expression from a string as is.
|
||||
*
|
||||
* @param expression
|
||||
* @return
|
||||
@@ -201,11 +204,19 @@ public final class JpqlQueryBuilder {
|
||||
return new LiteralExpression(expression);
|
||||
}
|
||||
|
||||
public static Expression stringLiteral(String literal) {
|
||||
return new StringLiteralExpression(literal);
|
||||
}
|
||||
|
||||
public static Expression parameter(String parameter) {
|
||||
|
||||
Assert.hasText(parameter, "Parameter must not be empty or null");
|
||||
|
||||
return new ParameterExpression(parameter);
|
||||
return new ParameterExpression(new ParameterPlaceholder(parameter));
|
||||
}
|
||||
|
||||
public static Expression parameter(ParameterPlaceholder placeholder) {
|
||||
return new ParameterExpression(placeholder);
|
||||
}
|
||||
|
||||
public static Expression orderBy(Expression sortExpression, Sort.Order order) {
|
||||
@@ -279,12 +290,12 @@ public final class JpqlQueryBuilder {
|
||||
|
||||
@Override
|
||||
public Predicate isTrue() {
|
||||
return new LhsPredicate(rhs, "IS TRUE");
|
||||
return new LhsPredicate(rhs, "= TRUE");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate isFalse() {
|
||||
return new LhsPredicate(rhs, "IS FALSE");
|
||||
return new LhsPredicate(rhs, "= FALSE");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -309,7 +320,7 @@ public final class JpqlQueryBuilder {
|
||||
|
||||
@Override
|
||||
public Predicate inMultivalued(Expression value) {
|
||||
return new MemberOfPredicate(rhs, "IN", value);
|
||||
return new MemberOfPredicate(rhs, "IN", value); // TODO: that does not line up in my head - ahahah
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -466,6 +477,42 @@ public final class JpqlQueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
static PathAndOrigin path(Origin origin, String path) {
|
||||
|
||||
if(origin instanceof Entity entity) {
|
||||
|
||||
try {
|
||||
PropertyPath from = PropertyPath.from(path, ClassUtils.forName(entity.entity, Entity.class.getClassLoader()));
|
||||
return new PathAndOrigin(from, entity, false);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if(origin instanceof Join join) {
|
||||
|
||||
Origin parent = join.source;
|
||||
List<String> segments = new ArrayList<>();
|
||||
segments.add(join.path);
|
||||
while(!(parent instanceof Entity)) {
|
||||
if(parent instanceof Join pj) {
|
||||
parent = pj.source;
|
||||
segments.add(pj.path);
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(parent instanceof Entity entity) {
|
||||
Collections.reverse(segments);
|
||||
segments.add(path);
|
||||
PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, "."));
|
||||
return new PathAndOrigin(path1.path().getLeafProperty(), origin, false);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(" oh no ");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity selection.
|
||||
*
|
||||
@@ -513,7 +560,9 @@ public final class JpqlQueryBuilder {
|
||||
|
||||
@Override
|
||||
public String render(RenderContext context) {
|
||||
return "new %s(%s)".formatted(resultType, multiselect.render(context));
|
||||
|
||||
|
||||
return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -542,7 +591,9 @@ public final class JpqlQueryBuilder {
|
||||
}
|
||||
|
||||
builder.append(PathExpression.render(path, context));
|
||||
builder.append(" ").append(path.path().getSegment());
|
||||
if(!context.isConstructorContext()) {
|
||||
builder.append(" ").append(path.path().getSegment());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
@@ -583,7 +634,7 @@ public final class JpqlQueryBuilder {
|
||||
* @param other
|
||||
* @return a composed predicate combining this and {@code other} using the AND operator.
|
||||
*/
|
||||
default Predicate and(Predicate other) {
|
||||
default Predicate and(Predicate other) { // don't like the structuring of this and the nest() thing
|
||||
return new AndPredicate(this, other);
|
||||
}
|
||||
|
||||
@@ -799,6 +850,22 @@ public final class JpqlQueryBuilder {
|
||||
String alias = getAlias(source);
|
||||
return ObjectUtils.isEmpty(source) ? fragment : alias + "." + fragment;
|
||||
}
|
||||
|
||||
public boolean isConstructorContext() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class ConstructorContext extends RenderContext {
|
||||
|
||||
ConstructorContext(RenderContext rootContext) {
|
||||
super(rootContext.aliases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstructorContext() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -807,7 +874,7 @@ public final class JpqlQueryBuilder {
|
||||
*/
|
||||
public interface Origin {
|
||||
|
||||
String getName();
|
||||
String getName(); // TODO: mainly used along records - shoule we call this just name()?
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1051,11 +1118,28 @@ public final class JpqlQueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
record ParameterExpression(String parameter) implements Expression {
|
||||
record StringLiteralExpression(String literal) implements Expression {
|
||||
|
||||
@Override
|
||||
public String render(RenderContext context) {
|
||||
return parameter;
|
||||
return "'%s'".formatted(literal.replaceAll("'", "''"));
|
||||
}
|
||||
|
||||
public String raw() {
|
||||
return literal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return render(RenderContext.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
record ParameterExpression(ParameterPlaceholder parameter) implements Expression {
|
||||
|
||||
@Override
|
||||
public String render(RenderContext context) {
|
||||
return parameter.placeholder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1158,6 +1242,8 @@ public final class JpqlQueryBuilder {
|
||||
|
||||
@Override
|
||||
public String render(RenderContext context) {
|
||||
|
||||
//TODO: should we rather wrap it with nested or check if its a nested predicate before we call render
|
||||
return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context));
|
||||
}
|
||||
|
||||
@@ -1216,4 +1302,21 @@ public final class JpqlQueryBuilder {
|
||||
public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) {
|
||||
|
||||
}
|
||||
|
||||
public record ParameterPlaceholder(String placeholder) {
|
||||
|
||||
public ParameterPlaceholder {
|
||||
Assert.hasText(placeholder, "Placeholder must not be null nor empty");
|
||||
}
|
||||
|
||||
public static ParameterPlaceholder indexed(int index) {
|
||||
return new ParameterPlaceholder("?%s".formatted(index));
|
||||
}
|
||||
|
||||
public static ParameterPlaceholder named(String name) {
|
||||
|
||||
Assert.hasText(name, "Placeholder name must not be empty");
|
||||
return new ParameterPlaceholder(":%s".formatted(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,64 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ELEMENT_COLLECTION;
|
||||
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_MANY;
|
||||
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE;
|
||||
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_MANY;
|
||||
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE;
|
||||
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.criteria.From;
|
||||
import jakarta.persistence.criteria.Join;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import jakarta.persistence.metamodel.Attribute;
|
||||
import jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
|
||||
import jakarta.persistence.metamodel.Bindable;
|
||||
import jakarta.persistence.metamodel.ManagedType;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
import jakarta.persistence.metamodel.PluralAttribute;
|
||||
import jakarta.persistence.metamodel.SingularAttribute;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Member;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class JpqlUtils {
|
||||
|
||||
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
|
||||
PropertyPath property) {
|
||||
return toExpressionRecursively(source, from, property, false);
|
||||
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
|
||||
|
||||
static {
|
||||
Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
|
||||
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
|
||||
persistentAttributeTypes.put(ONE_TO_MANY, null);
|
||||
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
|
||||
persistentAttributeTypes.put(MANY_TO_MANY, null);
|
||||
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
|
||||
|
||||
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
|
||||
}
|
||||
|
||||
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
|
||||
PropertyPath property, boolean isForSelection) {
|
||||
return toExpressionRecursively(source, from, property, isForSelection, false);
|
||||
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
|
||||
Bindable<?> from, PropertyPath property) {
|
||||
return toExpressionRecursively(metamodel, source, from, property, false);
|
||||
}
|
||||
|
||||
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
|
||||
Bindable<?> from, PropertyPath property, boolean isForSelection) {
|
||||
return toExpressionRecursively(metamodel, source, from, property, isForSelection, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,18 +82,18 @@ class JpqlUtils {
|
||||
* @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) {
|
||||
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
|
||||
Bindable<?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
|
||||
|
||||
String segment = property.getSegment();
|
||||
|
||||
boolean isLeafProperty = !property.hasNext();
|
||||
|
||||
boolean requiresOuterJoin = QueryUtils.requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin);
|
||||
boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection,
|
||||
hasRequiredOuterJoin);
|
||||
|
||||
// if it does not require an outer join and is a leaf, simply get the segment
|
||||
if (!requiresOuterJoin && isLeafProperty) {
|
||||
@@ -66,9 +103,10 @@ class JpqlUtils {
|
||||
// get or create the join
|
||||
JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment)
|
||||
: JpqlQueryBuilder.innerJoin(source, segment);
|
||||
JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER;
|
||||
Join<?, ?> join = QueryUtils.getOrCreateJoin(from, segment, joinType);
|
||||
// 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);
|
||||
@@ -76,7 +114,110 @@ class JpqlUtils {
|
||||
|
||||
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
|
||||
|
||||
// ManagedType<?> managedType = ;
|
||||
Bindable<?> managedTypeForModel = (Bindable<?>) getManagedTypeForModel(from);
|
||||
// Attribute<?, ?> joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null);
|
||||
// recurse with the next property
|
||||
return toExpressionRecursively(joinSource, join, nextProperty, isForSelection, requiresOuterJoin);
|
||||
return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this attribute requires an outer join. This is the case e.g. if it hadn't already been fetched with an
|
||||
* inner join and if it's an optional association, and if previous paths has already required outer joins. It also
|
||||
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999)
|
||||
*
|
||||
* @param metamodel
|
||||
* @param source
|
||||
* @param bindable
|
||||
* @param propertyPath
|
||||
* @param isForSelection
|
||||
* @param hasRequiredOuterJoin
|
||||
* @return
|
||||
*/
|
||||
static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable<?> bindable,
|
||||
PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) {
|
||||
|
||||
ManagedType<?> managedType = getManagedTypeForModel(bindable);
|
||||
Attribute<?, ?> attribute = getModelForPath(metamodel, propertyPath, managedType, bindable);
|
||||
|
||||
boolean isPluralAttribute = bindable instanceof PluralAttribute;
|
||||
if (attribute == null) {
|
||||
return isPluralAttribute;
|
||||
}
|
||||
|
||||
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isCollection = attribute.isCollection();
|
||||
|
||||
// if this path is an optional one to one attribute navigated from the not owning side we also need an
|
||||
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
|
||||
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
|
||||
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
|
||||
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
|
||||
|
||||
boolean isLeafProperty = !propertyPath.hasNext();
|
||||
if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
|
||||
|
||||
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
|
||||
|
||||
if (associationAnnotation == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Member member = attribute.getJavaMember();
|
||||
|
||||
if (!(member instanceof AnnotatedElement annotatedMember)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation);
|
||||
return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
|
||||
|
||||
if (model instanceof ManagedType<?> managedType) {
|
||||
return managedType;
|
||||
}
|
||||
|
||||
if (!(model instanceof SingularAttribute<?, ?> singularAttribute)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Attribute<?, ?> getModelForPath(Metamodel metamodel, PropertyPath path,
|
||||
@Nullable ManagedType<?> managedType, Bindable<?> fallback) {
|
||||
|
||||
String segment = path.getSegment();
|
||||
if (managedType != null) {
|
||||
try {
|
||||
return managedType.getAttribute(segment);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// ManagedType may be erased for some vendor if the attribute is declared as generic
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> fallbackType = fallback.getBindableJavaType();
|
||||
try {
|
||||
return metamodel.managedType(fallbackType).getAttribute(segment);
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import jakarta.persistence.criteria.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.metamodel.Bindable;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
import org.springframework.data.domain.KeysetScrollPosition;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
@@ -77,11 +79,11 @@ public record KeysetScrollSpecification<T>(KeysetScrollPosition position, Sort s
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public JpqlQueryBuilder.Predicate createJpqlPredicate(From<?, ?> from, JpqlQueryBuilder.Entity entity,
|
||||
public JpqlQueryBuilder.Predicate createJpqlPredicate(Bindable<?> from, JpqlQueryBuilder.Entity entity,
|
||||
ParameterFactory factory) {
|
||||
|
||||
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
|
||||
return delegate.createPredicate(position, sort, new JpqlStrategy(from, entity, factory));
|
||||
return delegate.createPredicate(position, sort, new JpqlStrategy(null, from, entity, factory));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@@ -128,22 +130,24 @@ public record KeysetScrollSpecification<T>(KeysetScrollPosition position, Sort s
|
||||
|
||||
private static class JpqlStrategy implements QueryStrategy<JpqlQueryBuilder.Expression, JpqlQueryBuilder.Predicate> {
|
||||
|
||||
private final From<?, ?> from;
|
||||
private final Bindable<?> from;
|
||||
private final JpqlQueryBuilder.Entity entity;
|
||||
private final ParameterFactory factory;
|
||||
private final Metamodel metamodel;
|
||||
|
||||
public JpqlStrategy(From<?, ?> from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) {
|
||||
public JpqlStrategy(Metamodel metamodel, Bindable<?> from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) {
|
||||
|
||||
this.from = from;
|
||||
this.entity = entity;
|
||||
this.factory = factory;
|
||||
this.metamodel = metamodel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpqlQueryBuilder.Expression createExpression(String property) {
|
||||
|
||||
PropertyPath path = PropertyPath.from(property, from.getJavaType());
|
||||
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, path));
|
||||
PropertyPath path = PropertyPath.from(property, from.getBindableJavaType());
|
||||
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class ParameterBinding {
|
||||
|
||||
@@ -217,7 +218,10 @@ class ParameterBinding {
|
||||
this.templates = templates;
|
||||
this.escape = escape;
|
||||
|
||||
this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType();
|
||||
this.type = value == null
|
||||
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
|
||||
? Type.IS_NULL
|
||||
: part.getType();
|
||||
this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase());
|
||||
this.noWildcards = part.getProperty().getLeafProperty().isCollection();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
private final PartTree tree;
|
||||
private final JpaParameters parameters;
|
||||
|
||||
private final QueryPreparer query;
|
||||
private final QueryPreparer queryPreparer;
|
||||
private final QueryPreparer countQuery;
|
||||
private final EntityManager em;
|
||||
private final EscapeCharacter escape;
|
||||
@@ -102,7 +102,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
this.tree = new PartTree(method.getName(), domainClass);
|
||||
validate(tree, parameters, method.toString());
|
||||
this.countQuery = new CountQueryPreparer();
|
||||
this.query = tree.isCountProjection() ? countQuery : new QueryPreparer();
|
||||
this.queryPreparer = tree.isCountProjection() ? countQuery : new QueryPreparer();
|
||||
|
||||
} catch (Exception o_O) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -112,7 +112,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
|
||||
@Override
|
||||
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
|
||||
return query.createQuery(accessor);
|
||||
return queryPreparer.createQuery(accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,12 +210,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
*/
|
||||
private class QueryPreparer {
|
||||
|
||||
private final Map<Sort, JpqlQueryCreator> cache = new LinkedHashMap<Sort, JpqlQueryCreator>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Sort, JpqlQueryCreator> eldest) {
|
||||
return size() > 256;
|
||||
}
|
||||
};
|
||||
private final PartTreeQueryCache cache = new PartTreeQueryCache();
|
||||
|
||||
/**
|
||||
* Creates a new {@link Query} for the given parameter values.
|
||||
@@ -279,7 +274,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
|
||||
synchronized (cache) {
|
||||
JpqlQueryCreator jpqlQueryCreator = cache.get(sort);
|
||||
JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties
|
||||
if (jpqlQueryCreator != null) {
|
||||
return jpqlQueryCreator;
|
||||
}
|
||||
@@ -304,7 +299,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
}
|
||||
|
||||
synchronized (cache) {
|
||||
cache.put(sort, creator);
|
||||
cache.put(sort, accessor, creator);
|
||||
}
|
||||
|
||||
return creator;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class PartTreeQueryCache {
|
||||
|
||||
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<CacheKey, JpqlQueryCreator>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<CacheKey, JpqlQueryCreator> eldest) {
|
||||
return size() > 256;
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
JpqlQueryCreator get(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
return cache.get(CacheKey.of(sort, accessor));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
JpqlQueryCreator put(Sort sort, JpaParametersParameterAccessor accessor, JpqlQueryCreator creator) {
|
||||
return cache.put(CacheKey.of(sort, accessor), creator);
|
||||
}
|
||||
|
||||
static class CacheKey {
|
||||
|
||||
private final Sort sort;
|
||||
private final Map<Integer, Nulled> params;
|
||||
|
||||
public CacheKey(Sort sort, Map<Integer, Nulled> params) {
|
||||
this.sort = sort;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
|
||||
Object[] values = accessor.getValues();
|
||||
if (ObjectUtils.isEmpty(values)) {
|
||||
return new CacheKey(sort, Map.of());
|
||||
}
|
||||
|
||||
return new CacheKey(sort, toNullableMap(values));
|
||||
}
|
||||
|
||||
static Map<Integer, Nulled> toNullableMap(Object[] args) {
|
||||
|
||||
Map<Integer, Nulled> paramMap = new HashMap<>(args.length);
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES);
|
||||
}
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
CacheKey cacheKey = (CacheKey) o;
|
||||
return sort.equals(cacheKey.sort) && params.equals(cacheKey.params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sort, params);
|
||||
}
|
||||
}
|
||||
|
||||
enum Nulled {
|
||||
YES, NO
|
||||
}
|
||||
|
||||
}
|
||||
@@ -896,7 +896,7 @@ public abstract class QueryUtils {
|
||||
* @param attribute the attribute name to check.
|
||||
* @return true if the attribute has already been inner joined
|
||||
*/
|
||||
private static boolean isAlreadyInnerJoined(From<?, ?> from, String attribute) {
|
||||
static boolean isAlreadyInnerJoined(From<?, ?> from, String attribute) {
|
||||
|
||||
for (Fetch<?, ?> fetch : from.getFetches()) {
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.AbstractJpqlQuery;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Entity;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Expression;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Join;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.OrderExpression;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Predicate;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.RenderContext;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.SelectStep;
|
||||
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.WhereStep;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class JpqlQueryBuilderUnitTests {
|
||||
|
||||
@Test
|
||||
void placeholdersRenderCorrectly() {
|
||||
|
||||
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1");
|
||||
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.named("arg1")).render(RenderContext.EMPTY))
|
||||
.isEqualTo(":arg1");
|
||||
assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeholdersErrorOnInvaludInput() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> JpqlQueryBuilder.parameter((String) null));
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringLiteralRendersAsQuotedString() {
|
||||
|
||||
assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
|
||||
|
||||
/* JPA Spec - 4.6.1 Literals:
|
||||
> A string literal that includes a single quote is represented by two single quotes--for example: 'literal''s'. */
|
||||
assertThat(JpqlQueryBuilder.stringLiteral("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void entity() {
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(Order.class);
|
||||
assertThat(entity.alias()).isEqualTo("o");
|
||||
assertThat(entity.entity()).isEqualTo(Order.class.getName());
|
||||
assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); // TODO: this really confusing
|
||||
assertThat(entity.simpleName()).isEqualTo(Order.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void literalExpressionRendersAsIs() {
|
||||
Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, ‘, ’, person.firstName))");
|
||||
assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))");
|
||||
}
|
||||
|
||||
@Test
|
||||
void xxx() {
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(Order.class);
|
||||
PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date");
|
||||
|
||||
String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity));
|
||||
|
||||
assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}");
|
||||
|
||||
// JpqlQueryBuilder.where(PathAndOrigin)
|
||||
}
|
||||
|
||||
@Test
|
||||
void predicateRendering() {
|
||||
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(Order.class);
|
||||
WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country"));
|
||||
|
||||
assertThat(where.between("'AT'", "'DE'").render(ctx(entity))).isEqualTo("o.country BETWEEN 'AT' AND 'DE'");
|
||||
assertThat(where.eq("'AT'").render(ctx(entity))).isEqualTo("o.country = 'AT'");
|
||||
assertThat(where.eq(JpqlQueryBuilder.stringLiteral("AT")).render(ctx(entity))).isEqualTo("o.country = 'AT'");
|
||||
assertThat(where.gt("'AT'").render(ctx(entity))).isEqualTo("o.country > 'AT'");
|
||||
assertThat(where.gte("'AT'").render(ctx(entity))).isEqualTo("o.country >= 'AT'");
|
||||
// TODO: that is really really bad
|
||||
// lange namen
|
||||
assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')");
|
||||
|
||||
// 1 in age - cleanup what is not used - remove everything eles
|
||||
// assertThat(where.inMultivalued("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); //
|
||||
assertThat(where.isEmpty().render(ctx(entity))).isEqualTo("o.country IS EMPTY");
|
||||
assertThat(where.isNotEmpty().render(ctx(entity))).isEqualTo("o.country IS NOT EMPTY");
|
||||
assertThat(where.isTrue().render(ctx(entity))).isEqualTo("o.country = TRUE");
|
||||
assertThat(where.isFalse().render(ctx(entity))).isEqualTo("o.country = FALSE");
|
||||
assertThat(where.isNull().render(ctx(entity))).isEqualTo("o.country IS NULL");
|
||||
assertThat(where.isNotNull().render(ctx(entity))).isEqualTo("o.country IS NOT NULL");
|
||||
assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
|
||||
.isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'");
|
||||
assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
|
||||
.isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'");
|
||||
assertThat(where.lt("'AT'").render(ctx(entity))).isEqualTo("o.country < 'AT'");
|
||||
assertThat(where.lte("'AT'").render(ctx(entity))).isEqualTo("o.country <= 'AT'");
|
||||
assertThat(where.memberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' MEMBER OF o.country");
|
||||
// TODO: can we have this where.value(foo).memberOf(pathAndOrigin);
|
||||
assertThat(where.notMemberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' NOT MEMBER OF o.country");
|
||||
assertThat(where.neq("'AT'").render(ctx(entity))).isEqualTo("o.country != 'AT'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectRendering() {
|
||||
|
||||
// make sure things are immutable
|
||||
SelectStep select = JpqlQueryBuilder.selectFrom(Order.class); // the select step is mutable - not sure i like it
|
||||
// hm, I somehow exepect this to render only the selection part
|
||||
assertThat(select.count().render()).startsWith("SELECT COUNT(o)");
|
||||
assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o ");
|
||||
assertThat(select.distinct().count().render()).startsWith("SELECT COUNT(DISTINCT o) ");
|
||||
assertThat(JpqlQueryBuilder.selectFrom(Order.class).select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render())
|
||||
.startsWith("SELECT o.country ");
|
||||
}
|
||||
|
||||
// @Test
|
||||
// void sorting() {
|
||||
//
|
||||
// JpqlQueryBuilder.orderBy(new OrderExpression() , Sort.Order.asc("country"));
|
||||
//
|
||||
// Entity entity = JpqlQueryBuilder.entity(Order.class);
|
||||
//
|
||||
// AbstractJpqlQuery query = JpqlQueryBuilder.selectFrom(Order.class)
|
||||
// .entity()
|
||||
// .orderBy()
|
||||
// .where(context -> "1 = 1");
|
||||
//
|
||||
// }
|
||||
|
||||
@Test
|
||||
void joins() {
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
|
||||
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
|
||||
Join li_pr2 = JpqlQueryBuilder.innerJoin(entity, "product2");
|
||||
|
||||
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
|
||||
PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name");
|
||||
|
||||
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
|
||||
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity));
|
||||
|
||||
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void x2() {
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
|
||||
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
|
||||
Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person");
|
||||
|
||||
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
|
||||
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
|
||||
|
||||
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
|
||||
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
|
||||
|
||||
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void x3() {
|
||||
|
||||
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
|
||||
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
|
||||
Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person");
|
||||
|
||||
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
|
||||
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
|
||||
|
||||
// JpqlQueryBuilder.and("x = y", "a = b"); -> x = y AND a = b
|
||||
|
||||
// JpqlQueryBuilder.nested(JpqlQueryBuilder.and("x = y", "a = b")) (x = y AND a = b)
|
||||
|
||||
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
|
||||
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
|
||||
|
||||
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
|
||||
}
|
||||
|
||||
static RenderContext ctx(Entity... entities) {
|
||||
Map<Origin, String> aliases = new LinkedHashMap<>(entities.length);
|
||||
for (Entity entity : entities) {
|
||||
aliases.put(entity, entity.alias());
|
||||
}
|
||||
|
||||
return new RenderContext(aliases);
|
||||
}
|
||||
|
||||
@jakarta.persistence.Entity
|
||||
static class Order {
|
||||
|
||||
@Id Long id;
|
||||
Date date;
|
||||
String country;
|
||||
|
||||
@OneToMany List<LineItem> lineItems;
|
||||
}
|
||||
|
||||
@jakarta.persistence.Entity
|
||||
static class LineItem {
|
||||
|
||||
@Id Long id;
|
||||
|
||||
@ManyToOne Product product;
|
||||
@ManyToOne Product product2;
|
||||
@ManyToOne Product person;
|
||||
|
||||
}
|
||||
|
||||
@jakarta.persistence.Entity
|
||||
static class Person {
|
||||
@Id Long id;
|
||||
String name;
|
||||
}
|
||||
|
||||
@jakarta.persistence.Entity
|
||||
static class Product {
|
||||
|
||||
@Id Long id;
|
||||
|
||||
String name;
|
||||
String productType;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class PartTreeJpaQueryIntegrationTests {
|
||||
|
||||
Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) }));
|
||||
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY)))
|
||||
.contains("firstname %s :".formatted(criteria.endsWith("Not") ? "<>" : "="));
|
||||
.contains("firstname %s ?".formatted(criteria.endsWith("Not") ? "!=" : "="));
|
||||
|
||||
query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) }));
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.assertThat;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.FieldSource;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class PartTreeQueryCacheUnitTests {
|
||||
|
||||
PartTreeQueryCache cache;
|
||||
|
||||
static Supplier<Stream<Arguments>> cacheInput = () -> Stream.of(
|
||||
Arguments.arguments(Sort.unsorted(), StubJpaParameterParameterAccessor.accessor()), //
|
||||
Arguments.arguments(Sort.by(Direction.ASC, "one"), StubJpaParameterParameterAccessor.accessor()), //
|
||||
Arguments.arguments(Sort.by(Direction.DESC, "one"), StubJpaParameterParameterAccessor.accessor()), //
|
||||
Arguments.arguments(Sort.unsorted(),
|
||||
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), //
|
||||
Arguments.arguments(Sort.unsorted(),
|
||||
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null })), //
|
||||
Arguments.arguments(Sort.by(Direction.ASC, "one"),
|
||||
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), //
|
||||
Arguments.arguments(Sort.by(Direction.ASC, "one"),
|
||||
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null })));
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new PartTreeQueryCache();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@FieldSource("cacheInput")
|
||||
void getReturnsNullForEmptyCache(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
assertThat(cache.get(sort, accessor)).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@FieldSource("cacheInput")
|
||||
void getReturnsCachedInstance(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
|
||||
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
|
||||
|
||||
assertThat(cache.put(sort, accessor, queryCreator)).isNull();
|
||||
assertThat(cache.get(sort, accessor)).isSameAs(queryCreator);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@FieldSource("cacheInput")
|
||||
void cacheGetWithSort(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
|
||||
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
|
||||
assertThat(cache.put(Sort.by("not-in-cache"), accessor, queryCreator)).isNull();
|
||||
|
||||
assertThat(cache.get(sort, accessor)).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@FieldSource("cacheInput")
|
||||
void cacheGetWithccessor(Sort sort, JpaParametersParameterAccessor accessor) {
|
||||
|
||||
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
|
||||
assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull();
|
||||
|
||||
assertThat(cache.get(sort, accessor)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cachesOnNullableNotArgumentType() {
|
||||
|
||||
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
|
||||
Sort sort = Sort.unsorted();
|
||||
assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull();
|
||||
|
||||
assertThat(cache.get(sort,
|
||||
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "spring", null)))
|
||||
.isNull();
|
||||
|
||||
assertThat(cache.get(sort,
|
||||
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, null, "data"))).isNull();
|
||||
|
||||
assertThat(cache.get(sort,
|
||||
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring")))
|
||||
.isSameAs(queryCreator);
|
||||
|
||||
assertThat(cache.get(Sort.by("not-in-cache"),
|
||||
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring")))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class StubJpaParameterParameterAccessor extends JpaParametersParameterAccessor {
|
||||
|
||||
private StubJpaParameterParameterAccessor(JpaParameters parameters, Object[] values) {
|
||||
super(parameters, values);
|
||||
}
|
||||
|
||||
static JpaParametersParameterAccessor accessor(Object... values) {
|
||||
|
||||
Class<?>[] parameterTypes = Arrays.stream(values).map(it -> it != null ? it.getClass() : Object.class)
|
||||
.toArray(Class<?>[]::new);
|
||||
return accessor(parameterTypes, values);
|
||||
}
|
||||
|
||||
static JpaParametersParameterAccessor accessor(Class<?>... parameterTypes) {
|
||||
return accessor(parameterTypes, new Object[parameterTypes.length]);
|
||||
}
|
||||
|
||||
static AccessorBuilder accessorFor(Class<?>... parameterTypes) {
|
||||
return arguments -> accessor(parameterTypes, arguments);
|
||||
|
||||
}
|
||||
|
||||
interface AccessorBuilder {
|
||||
JpaParametersParameterAccessor withValues(Object... arguments);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
static JpaParametersParameterAccessor accessor(Class<?>[] parameterTypes, Object... parameters) {
|
||||
|
||||
List<JpaParameter> parametersList = new ArrayList<>(parameterTypes.length);
|
||||
List<Object> valueList = new ArrayList<>(parameterTypes.length);
|
||||
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
|
||||
if (i < parameters.length) {
|
||||
valueList.add(parameters[i]);
|
||||
}
|
||||
|
||||
Class<?> parameterType = parameterTypes[i];
|
||||
MethodParameter mock = Mockito.mock(MethodParameter.class);
|
||||
when(mock.getParameterType()).thenReturn((Class) parameterType);
|
||||
JpaParameter parameter = new JpaParameter(mock, TypeInformation.of(parameterType));
|
||||
parametersList.add(parameter);
|
||||
}
|
||||
|
||||
return new StubJpaParameterParameterAccessor(new JpaParameters(parametersList), valueList.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
List<String> parameters = new ArrayList<>(getParameters().getNumberOfParameters());
|
||||
|
||||
for (int i = 0; i < getParameters().getNumberOfParameters(); i++) {
|
||||
Object value = getValue(i);
|
||||
if (value == null) {
|
||||
value = "null";
|
||||
}
|
||||
parameters.add("%s: %s (%s)".formatted(i, value, getParameters().getParameter(i).getType().getSimpleName()));
|
||||
}
|
||||
return "%s".formatted(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.metamodel.EmbeddableType;
|
||||
import jakarta.persistence.metamodel.EntityType;
|
||||
import jakarta.persistence.metamodel.ManagedType;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
import jakarta.persistence.spi.ClassTransformer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl;
|
||||
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.instrument.classloading.SimpleThrowawayClassLoader;
|
||||
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class TestMetaModel implements Metamodel {
|
||||
|
||||
private final String persistenceUnit;
|
||||
private final Set<Class<?>> managedTypes;
|
||||
private final Lazy<EntityManagerFactory> entityManagerFactory = Lazy.of(this::init);
|
||||
private final Lazy<Metamodel> metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel());
|
||||
private Lazy<EntityManager> enityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager());
|
||||
|
||||
TestMetaModel(Set<Class<?>> managedTypes) {
|
||||
this("dynamic-tests", managedTypes);
|
||||
}
|
||||
|
||||
TestMetaModel(String persistenceUnit, Set<Class<?>> managedTypes) {
|
||||
this.persistenceUnit = persistenceUnit;
|
||||
this.managedTypes = managedTypes;
|
||||
}
|
||||
|
||||
public static TestMetaModel hibernateModel(Class<?>... types) {
|
||||
return new TestMetaModel(Set.of(types));
|
||||
}
|
||||
|
||||
public static TestMetaModel hibernateModel(String persistenceUnit, Class<?>... types) {
|
||||
return new TestMetaModel(persistenceUnit, Set.of(types));
|
||||
}
|
||||
|
||||
public <X> EntityType<X> entity(Class<X> cls) {
|
||||
return metamodel.get().entity(cls);
|
||||
}
|
||||
|
||||
public <X> ManagedType<X> managedType(Class<X> cls) {
|
||||
return metamodel.get().managedType(cls);
|
||||
}
|
||||
|
||||
public <X> EmbeddableType<X> embeddable(Class<X> cls) {
|
||||
return metamodel.get().embeddable(cls);
|
||||
}
|
||||
|
||||
public Set<ManagedType<?>> getManagedTypes() {
|
||||
return metamodel.get().getManagedTypes();
|
||||
}
|
||||
|
||||
public Set<EntityType<?>> getEntities() {
|
||||
return metamodel.get().getEntities();
|
||||
}
|
||||
|
||||
public Set<EmbeddableType<?>> getEmbeddables() {
|
||||
return metamodel.get().getEmbeddables();
|
||||
}
|
||||
|
||||
public EntityManager entityManager() {
|
||||
return enityManager.get();
|
||||
}
|
||||
|
||||
EntityManagerFactory init() {
|
||||
|
||||
MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() {
|
||||
@Override
|
||||
public ClassLoader getNewTempClassLoader() {
|
||||
return new SimpleThrowawayClassLoader(this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransformer(ClassTransformer classTransformer) {
|
||||
// just ingnore it
|
||||
}
|
||||
};
|
||||
|
||||
persistenceUnitInfo.setPersistenceUnitName(persistenceUnit);
|
||||
this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName);
|
||||
|
||||
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
|
||||
|
||||
return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) {
|
||||
@Override
|
||||
public List<String> getManagedClassNames() {
|
||||
return persistenceUnitInfo.getManagedClassNames();
|
||||
}
|
||||
}, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build();
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,14 @@
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
|
||||
<persistence-unit name="dynamic-tests">
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
<properties>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
|
||||
<!-- Custom PUs for metadata tests -->
|
||||
|
||||
<persistence-unit name="metadata">
|
||||
|
||||
Reference in New Issue
Block a user