Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f0b944d

Browse files
committedMar 17, 2025
Polishing.
Remove method overloads accepting pure strings. Use switch-expressions. Correctly navigate nested joins. Introduce PathExpression interface, refine naming. See #3588 Original pull request: #3653
1 parent 69e2a88 commit f0b944d

File tree

11 files changed

+493
-486
lines changed

11 files changed

+493
-486
lines changed
 

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.springframework.data.repository.query.parser.Part.Type.IS_NOT_EMPTY;
19-
import static org.springframework.data.repository.query.parser.Part.Type.NOT_CONTAINING;
20-
import static org.springframework.data.repository.query.parser.Part.Type.NOT_LIKE;
21-
import static org.springframework.data.repository.query.parser.Part.Type.SIMPLE_PROPERTY;
18+
import static org.springframework.data.repository.query.parser.Part.Type.*;
2219

2320
import jakarta.persistence.EntityManager;
2421
import jakarta.persistence.criteria.CriteriaQuery;
@@ -39,7 +36,6 @@
3936
import org.springframework.data.domain.Sort;
4037
import org.springframework.data.jpa.domain.JpaSort;
4138
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
42-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
4339
import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding;
4440
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
4541
import org.springframework.data.mapping.PropertyPath;
@@ -183,8 +179,8 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
183179
QueryUtils.checkSortExpression(order);
184180

185181
try {
186-
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
187-
PropertyPath.from(order.getProperty(), entityType.getJavaType())));
182+
expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
183+
PropertyPath.from(order.getProperty(), entityType.getJavaType()));
188184
} catch (PropertyReferenceException e) {
189185

190186
if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
@@ -227,7 +223,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
227223
requiredSelection = getRequiredSelection(sort, returnedType);
228224
}
229225

230-
List<PathAndOrigin> paths = new ArrayList<>(requiredSelection.size());
226+
List<JpqlQueryBuilder.PathExpression> paths = new ArrayList<>(requiredSelection.size());
231227
for (String selection : requiredSelection) {
232228
paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
233229
PropertyPath.from(selection, returnedType.getDomainType()), true));
@@ -251,7 +247,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
251247

252248
} else {
253249

254-
List<PathAndOrigin> paths = entityType.getIdClassAttributes().stream()//
250+
List<JpqlQueryBuilder.PathExpression> paths = entityType.getIdClassAttributes().stream()//
255251
.map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
256252
PropertyPath.from(it.getName(), returnedType.getDomainType()), true))
257253
.toList();
@@ -320,7 +316,7 @@ public JpqlQueryBuilder.Predicate build() {
320316
PropertyPath property = part.getProperty();
321317
Type type = part.getType();
322318

323-
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
319+
JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
324320
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas);
325321
JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas));
326322

@@ -385,7 +381,7 @@ public JpqlQueryBuilder.Predicate build() {
385381
return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull();
386382
}
387383

388-
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata));
384+
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(simple));
389385
return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression);
390386
case IS_EMPTY:
391387
case IS_NOT_EMPTY:
@@ -420,8 +416,8 @@ private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.O
420416
* @param path must not be {@literal null}.
421417
* @return
422418
*/
423-
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) {
424-
return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path));
419+
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.PathExpression path) {
420+
return potentiallyIgnoreCase(path.getPropertyPath(), path);
425421
}
426422

427423
/**

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java

Lines changed: 294 additions & 180 deletions
Large diffs are not rendered by default.

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java

Lines changed: 16 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,16 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ELEMENT_COLLECTION;
19-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_MANY;
20-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE;
21-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_MANY;
22-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE;
23-
24-
import jakarta.persistence.ManyToOne;
25-
import jakarta.persistence.OneToOne;
2618
import jakarta.persistence.criteria.From;
27-
import jakarta.persistence.criteria.Join;
28-
import jakarta.persistence.criteria.JoinType;
2919
import jakarta.persistence.metamodel.Attribute;
3020
import jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
3121
import jakarta.persistence.metamodel.Bindable;
3222
import jakarta.persistence.metamodel.ManagedType;
3323
import jakarta.persistence.metamodel.Metamodel;
3424
import jakarta.persistence.metamodel.PluralAttribute;
35-
import jakarta.persistence.metamodel.SingularAttribute;
36-
37-
import java.lang.annotation.Annotation;
38-
import java.lang.reflect.AnnotatedElement;
39-
import java.lang.reflect.Member;
40-
import java.util.Collections;
41-
import java.util.HashMap;
42-
import java.util.Map;
25+
4326
import java.util.Objects;
4427

45-
import org.springframework.core.annotation.AnnotationUtils;
4628
import org.springframework.data.mapping.PropertyPath;
4729
import org.springframework.lang.Nullable;
4830
import org.springframework.util.StringUtils;
@@ -52,25 +34,12 @@
5234
*/
5335
class JpqlUtils {
5436

55-
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
56-
57-
static {
58-
Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
59-
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
60-
persistentAttributeTypes.put(ONE_TO_MANY, null);
61-
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
62-
persistentAttributeTypes.put(MANY_TO_MANY, null);
63-
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
64-
65-
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
66-
}
67-
68-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
37+
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
6938
Bindable<?> from, PropertyPath property) {
7039
return toExpressionRecursively(metamodel, source, from, property, false);
7140
}
7241

73-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
42+
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
7443
Bindable<?> from, PropertyPath property, boolean isForSelection) {
7544
return toExpressionRecursively(metamodel, source, from, property, isForSelection, false);
7645
}
@@ -84,16 +53,13 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode
8453
* @param hasRequiredOuterJoin has a parent already required an outer join?
8554
* @return the expression
8655
*/
87-
@SuppressWarnings("unchecked")
88-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
56+
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
8957
Bindable<?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
9058

9159
String segment = property.getSegment();
9260

9361
boolean isLeafProperty = !property.hasNext();
94-
95-
boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection,
96-
hasRequiredOuterJoin);
62+
boolean requiresOuterJoin = requiresOuterJoin(metamodel, from, property, isForSelection, hasRequiredOuterJoin);
9763

9864
// if it does not require an outer join and is a leaf, simply get the segment
9965
if (!requiresOuterJoin && isLeafProperty) {
@@ -103,22 +69,19 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode
10369
// get or create the join
10470
JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment)
10571
: JpqlQueryBuilder.innerJoin(source, segment);
106-
// JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER;
107-
// Join<?, ?> join = QueryUtils.getOrCreateJoin(from, segment, joinType);
10872

109-
//
11073
// if it's a leaf, return the join
11174
if (isLeafProperty) {
11275
return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true);
11376
}
11477

11578
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
11679

117-
// ManagedType<?> managedType = ;
118-
Bindable<?> managedTypeForModel = (Bindable<?>) getManagedTypeForModel(from);
119-
// Attribute<?, ?> joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null);
120-
// recurse with the next property
121-
return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin);
80+
ManagedType<?> managedTypeForModel = QueryUtils.getManagedTypeForModel(from);
81+
Attribute<?, ?> nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from);
82+
83+
return toExpressionRecursively(metamodel, joinSource, (Bindable<?>) nextAttribute, nextProperty, isForSelection,
84+
requiresOuterJoin);
12285
}
12386

12487
/**
@@ -127,25 +90,24 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode
12790
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999)
12891
*
12992
* @param metamodel
130-
* @param source
13193
* @param bindable
13294
* @param propertyPath
13395
* @param isForSelection
13496
* @param hasRequiredOuterJoin
13597
* @return
13698
*/
137-
static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable<?> bindable,
138-
PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) {
99+
static boolean requiresOuterJoin(Metamodel metamodel, Bindable<?> bindable, PropertyPath propertyPath,
100+
boolean isForSelection, boolean hasRequiredOuterJoin) {
139101

140-
ManagedType<?> managedType = getManagedTypeForModel(bindable);
102+
ManagedType<?> managedType = QueryUtils.getManagedTypeForModel(bindable);
141103
Attribute<?, ?> attribute = getModelForPath(metamodel, propertyPath, managedType, bindable);
142104

143105
boolean isPluralAttribute = bindable instanceof PluralAttribute;
144106
if (attribute == null) {
145107
return isPluralAttribute;
146108
}
147109

148-
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
110+
if (!QueryUtils.ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
149111
return false;
150112
}
151113

@@ -155,47 +117,14 @@ static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin so
155117
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
156118
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
157119
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
158-
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
120+
&& StringUtils.hasText(QueryUtils.getAnnotationProperty(attribute, "mappedBy", ""));
159121

160122
boolean isLeafProperty = !propertyPath.hasNext();
161123
if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) {
162124
return false;
163125
}
164126

165-
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
166-
}
167-
168-
@Nullable
169-
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
170-
171-
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
172-
173-
if (associationAnnotation == null) {
174-
return defaultValue;
175-
}
176-
177-
Member member = attribute.getJavaMember();
178-
179-
if (!(member instanceof AnnotatedElement annotatedMember)) {
180-
return defaultValue;
181-
}
182-
183-
Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation);
184-
return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
185-
}
186-
187-
@Nullable
188-
private static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
189-
190-
if (model instanceof ManagedType<?> managedType) {
191-
return managedType;
192-
}
193-
194-
if (!(model instanceof SingularAttribute<?, ?> singularAttribute)) {
195-
return null;
196-
}
197-
198-
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
127+
return hasRequiredOuterJoin || QueryUtils.getAnnotationProperty(attribute, "optional", true);
199128
}
200129

201130
@Nullable

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
import jakarta.persistence.criteria.From;
2222
import jakarta.persistence.criteria.Predicate;
2323
import jakarta.persistence.criteria.Root;
24+
import jakarta.persistence.metamodel.Bindable;
25+
import jakarta.persistence.metamodel.Metamodel;
2426

2527
import java.util.List;
2628

27-
import jakarta.persistence.metamodel.Bindable;
28-
import jakarta.persistence.metamodel.Metamodel;
2929
import org.springframework.data.domain.KeysetScrollPosition;
3030
import org.springframework.data.domain.Sort;
3131
import org.springframework.data.domain.Sort.Order;
@@ -147,7 +147,7 @@ public JpqlStrategy(Metamodel metamodel, Bindable<?> from, JpqlQueryBuilder.Enti
147147
public JpqlQueryBuilder.Expression createExpression(String property) {
148148

149149
PropertyPath path = PropertyPath.from(property, from.getBindableJavaType());
150-
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path));
150+
return JpqlUtils.toExpressionRecursively(metamodel, entity, from, path);
151151
}
152152

153153
@Override

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,17 +242,12 @@ public Object prepare(@Nullable Object value) {
242242

243243
if (String.class.equals(parameterType) && !noWildcards) {
244244

245-
switch (type) {
246-
case STARTING_WITH:
247-
return String.format("%s%%", escape.escape(value.toString()));
248-
case ENDING_WITH:
249-
return String.format("%%%s", escape.escape(value.toString()));
250-
case CONTAINING:
251-
case NOT_CONTAINING:
252-
return String.format("%%%s%%", escape.escape(value.toString()));
253-
default:
254-
return value;
255-
}
245+
return switch (type) {
246+
case STARTING_WITH -> String.format("%s%%", escape.escape(value.toString()));
247+
case ENDING_WITH -> String.format("%%%s", escape.escape(value.toString()));
248+
case CONTAINING, NOT_CONTAINING -> String.format("%%%s%%", escape.escape(value.toString()));
249+
default -> value;
250+
};
256251
}
257252

258253
return Collection.class.isAssignableFrom(parameterType) //
@@ -710,7 +705,7 @@ static MethodInvocationArgument ofParameter(BindingIdentifier identifier) {
710705
boolean isExpression();
711706

712707
/**
713-
* @return {@code true} if the origin is an expression.
708+
* @return {@code true} if the origin is synthetic (contributed by e.g. KeysetPagination)
714709
*/
715710
boolean isSynthetic();
716711
}

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
import jakarta.persistence.TypedQuery;
2323
import jakarta.persistence.criteria.CriteriaQuery;
2424

25-
import java.util.LinkedHashMap;
2625
import java.util.List;
27-
import java.util.Map;
26+
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2829

2930
import org.springframework.data.domain.KeysetScrollPosition;
3031
import org.springframework.data.domain.OffsetScrollPosition;
@@ -57,6 +58,7 @@
5758
*/
5859
public class PartTreeJpaQuery extends AbstractJpaQuery {
5960

61+
private static final Logger log = LoggerFactory.getLogger(PartTreeJpaQuery.class);
6062
private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
6163

6264
private final PartTree tree;
@@ -201,7 +203,6 @@ private static boolean expectsCollection(Type type) {
201203
return type == Type.IN || type == Type.NOT_IN;
202204
}
203205

204-
205206
/**
206207
* Query preparer to create {@link CriteriaQuery} instances and potentially cache them.
207208
*
@@ -222,6 +223,11 @@ public Query createQuery(JpaParametersParameterAccessor accessor) {
222223
String jpql = creator.createQuery(sort);
223224
Query query;
224225

226+
if (log.isDebugEnabled()) {
227+
log.debug(String.format("%s: Derived query for query method [%s]: '%s'", getClass().getSimpleName(),
228+
getQueryMethod(), jpql));
229+
}
230+
225231
try {
226232
query = creator.useTupleQuery() ? em.createQuery(jpql, Tuple.class) : em.createQuery(jpql);
227233
} catch (Exception e) {
@@ -273,11 +279,14 @@ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPositio
273279

274280
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
275281

282+
JpqlQueryCreator jpqlQueryCreator;
276283
synchronized (cache) {
277-
JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties
278-
if (jpqlQueryCreator != null) {
279-
return jpqlQueryCreator;
280-
}
284+
jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for
285+
// simple properties
286+
}
287+
288+
if (jpqlQueryCreator != null) {
289+
return jpqlQueryCreator;
281290
}
282291

283292
EntityManager entityManager = getEntityManager();

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import java.util.HashMap;
18+
import java.util.BitSet;
1919
import java.util.LinkedHashMap;
2020
import java.util.Map;
2121
import java.util.Objects;
@@ -25,11 +25,13 @@
2525
import org.springframework.util.ObjectUtils;
2626

2727
/**
28+
* Cache for PartTree queries.
29+
*
2830
* @author Christoph Strobl
2931
*/
3032
class PartTreeQueryCache {
3133

32-
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<CacheKey, JpqlQueryCreator>() {
34+
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<>() {
3335
@Override
3436
protected boolean removeEldestEntry(Map.Entry<CacheKey, JpqlQueryCreator> eldest) {
3537
return size() > 256;
@@ -49,30 +51,37 @@ JpqlQueryCreator put(Sort sort, JpaParametersParameterAccessor accessor, JpqlQue
4951
static class CacheKey {
5052

5153
private final Sort sort;
52-
private final Map<Integer, Nulled> params;
5354

54-
public CacheKey(Sort sort, Map<Integer, Nulled> params) {
55+
/**
56+
* Bitset of null/non-null parameter values. A 0 bit means the parameter value is {@code null}, a 1 bit means the
57+
* parameter is not {@code null}.
58+
*/
59+
private final BitSet params;
60+
61+
public CacheKey(Sort sort, BitSet params) {
5562
this.sort = sort;
5663
this.params = params;
5764
}
5865

5966
static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) {
6067

6168
Object[] values = accessor.getValues();
69+
6270
if (ObjectUtils.isEmpty(values)) {
63-
return new CacheKey(sort, Map.of());
71+
return new CacheKey(sort, new BitSet());
6472
}
6573

6674
return new CacheKey(sort, toNullableMap(values));
6775
}
6876

69-
static Map<Integer, Nulled> toNullableMap(Object[] args) {
77+
static BitSet toNullableMap(Object[] args) {
7078

71-
Map<Integer, Nulled> paramMap = new HashMap<>(args.length);
79+
BitSet bitSet = new BitSet(args.length);
7280
for (int i = 0; i < args.length; i++) {
73-
paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES);
81+
bitSet.set(i, args[i] != null);
7482
}
75-
return paramMap;
83+
84+
return bitSet;
7685
}
7786

7887
@Override
@@ -93,8 +102,4 @@ public int hashCode() {
93102
}
94103
}
95104

96-
enum Nulled {
97-
YES, NO
98-
}
99-
100105
}

‎spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public abstract class QueryUtils {
130130

131131
private static final Pattern CONSTRUCTOR_EXPRESSION;
132132

133-
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
133+
static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
134134

135135
private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
136136
private static final int VARIABLE_NAME_GROUP_INDEX = 4;
@@ -837,8 +837,7 @@ static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean
837837
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
838838
}
839839

840-
@Nullable
841-
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
840+
static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
842841

843842
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
844843

@@ -967,7 +966,7 @@ private static Bindable<?> getModelForPath(PropertyPath path, @Nullable ManagedT
967966
* @return
968967
*/
969968
@Nullable
970-
private static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
969+
static ManagedType<?> getManagedTypeForModel(Bindable<?> model) {
971970

972971
if (model instanceof ManagedType<?> managedType) {
973972
return managedType;

‎spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java

Lines changed: 63 additions & 61 deletions
Large diffs are not rendered by default.

‎spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java

Lines changed: 54 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.assertj.core.api.Assertions.assertThat;
19-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.springframework.data.jpa.repository.query.JpqlQueryBuilder.*;
2020

2121
import jakarta.persistence.Id;
2222
import jakarta.persistence.ManyToOne;
@@ -28,26 +28,15 @@
2828
import java.util.Map;
2929

3030
import org.junit.jupiter.api.Test;
31-
import org.springframework.data.domain.Sort;
32-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.AbstractJpqlQuery;
33-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Entity;
34-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Expression;
35-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Join;
36-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.OrderExpression;
37-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin;
38-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
39-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
40-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Predicate;
41-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.RenderContext;
42-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.SelectStep;
43-
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.WhereStep;
4431

4532
/**
33+
* Unit tests for {@link JpqlQueryBuilder}.
34+
*
4635
* @author Christoph Strobl
4736
*/
4837
class JpqlQueryBuilderUnitTests {
4938

50-
@Test
39+
@Test // GH-3588
5140
void placeholdersRenderCorrectly() {
5241

5342
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1");
@@ -56,89 +45,88 @@ void placeholdersRenderCorrectly() {
5645
assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1");
5746
}
5847

59-
@Test
60-
void placeholdersErrorOnInvaludInput() {
48+
@Test // GH-3588
49+
void placeholdersErrorOnInvalidInput() {
6150
assertThatExceptionOfType(IllegalArgumentException.class)
6251
.isThrownBy(() -> JpqlQueryBuilder.parameter((String) null));
6352
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter(""));
6453
}
6554

66-
@Test
55+
@Test // GH-3588
6756
void stringLiteralRendersAsQuotedString() {
6857

69-
assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
58+
assertThat(literal("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
7059

7160
/* JPA Spec - 4.6.1 Literals:
7261
> A string literal that includes a single quote is represented by two single quotes--for example: 'literal''s'. */
73-
assertThat(JpqlQueryBuilder.stringLiteral("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'");
62+
assertThat(literal("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'");
7463
}
7564

76-
@Test
65+
@Test // GH-3588
7766
void entity() {
7867

7968
Entity entity = JpqlQueryBuilder.entity(Order.class);
80-
assertThat(entity.alias()).isEqualTo("o");
81-
assertThat(entity.entity()).isEqualTo(Order.class.getName());
82-
assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); // TODO: this really confusing
83-
assertThat(entity.simpleName()).isEqualTo(Order.class.getSimpleName());
69+
assertThat(entity.getAlias()).isEqualTo("o");
70+
assertThat(entity.getEntity()).isEqualTo(Order.class.getName());
71+
assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName());
8472
}
8573

86-
@Test
74+
@Test // GH-3588
8775
void literalExpressionRendersAsIs() {
88-
Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, ‘, ’, person.firstName))");
76+
Expression expression = expression("CONCAT(person.lastName, ‘, ’, person.firstName))");
8977
assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))");
9078
}
9179

92-
@Test
80+
@Test // GH-3588
9381
void xxx() {
9482

9583
Entity entity = JpqlQueryBuilder.entity(Order.class);
9684
PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date");
9785

98-
String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity));
86+
String fragment = JpqlQueryBuilder.where(orderDate).eq(expression("{d '2024-11-05'}")).render(ctx(entity));
9987

10088
assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}");
101-
102-
// JpqlQueryBuilder.where(PathAndOrigin)
10389
}
10490

105-
@Test
91+
@Test // GH-3588
10692
void predicateRendering() {
10793

108-
10994
Entity entity = JpqlQueryBuilder.entity(Order.class);
11095
WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country"));
96+
RenderContext context = ctx(entity);
97+
98+
assertThat(where.between(expression("'AT'"), expression("'DE'")).render(context))
99+
.isEqualTo("o.country BETWEEN 'AT' AND 'DE'");
100+
assertThat(where.eq(expression("'AT'")).render(context)).isEqualTo("o.country = 'AT'");
101+
assertThat(where.eq(literal("AT")).render(context)).isEqualTo("o.country = 'AT'");
102+
assertThat(where.gt(expression("'AT'")).render(context)).isEqualTo("o.country > 'AT'");
103+
assertThat(where.gte(expression("'AT'")).render(context)).isEqualTo("o.country >= 'AT'");
111104

112-
assertThat(where.between("'AT'", "'DE'").render(ctx(entity))).isEqualTo("o.country BETWEEN 'AT' AND 'DE'");
113-
assertThat(where.eq("'AT'").render(ctx(entity))).isEqualTo("o.country = 'AT'");
114-
assertThat(where.eq(JpqlQueryBuilder.stringLiteral("AT")).render(ctx(entity))).isEqualTo("o.country = 'AT'");
115-
assertThat(where.gt("'AT'").render(ctx(entity))).isEqualTo("o.country > 'AT'");
116-
assertThat(where.gte("'AT'").render(ctx(entity))).isEqualTo("o.country >= 'AT'");
117105
// TODO: that is really really bad
118106
// lange namen
119-
assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')");
107+
assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')");
120108

121109
// 1 in age - cleanup what is not used - remove everything eles
122110
// assertThat(where.inMultivalued("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); //
123-
assertThat(where.isEmpty().render(ctx(entity))).isEqualTo("o.country IS EMPTY");
124-
assertThat(where.isNotEmpty().render(ctx(entity))).isEqualTo("o.country IS NOT EMPTY");
125-
assertThat(where.isTrue().render(ctx(entity))).isEqualTo("o.country = TRUE");
126-
assertThat(where.isFalse().render(ctx(entity))).isEqualTo("o.country = FALSE");
127-
assertThat(where.isNull().render(ctx(entity))).isEqualTo("o.country IS NULL");
128-
assertThat(where.isNotNull().render(ctx(entity))).isEqualTo("o.country IS NOT NULL");
129-
assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
111+
assertThat(where.isEmpty().render(context)).isEqualTo("o.country IS EMPTY");
112+
assertThat(where.isNotEmpty().render(context)).isEqualTo("o.country IS NOT EMPTY");
113+
assertThat(where.isTrue().render(context)).isEqualTo("o.country = TRUE");
114+
assertThat(where.isFalse().render(context)).isEqualTo("o.country = FALSE");
115+
assertThat(where.isNull().render(context)).isEqualTo("o.country IS NULL");
116+
assertThat(where.isNotNull().render(context)).isEqualTo("o.country IS NOT NULL");
117+
assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context))
130118
.isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'");
131-
assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
119+
assertThat(where.notLike(expression("'\\_%'"), "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context))
132120
.isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'");
133-
assertThat(where.lt("'AT'").render(ctx(entity))).isEqualTo("o.country < 'AT'");
134-
assertThat(where.lte("'AT'").render(ctx(entity))).isEqualTo("o.country <= 'AT'");
135-
assertThat(where.memberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' MEMBER OF o.country");
121+
assertThat(where.lt(expression("'AT'")).render(context)).isEqualTo("o.country < 'AT'");
122+
assertThat(where.lte(expression("'AT'")).render(context)).isEqualTo("o.country <= 'AT'");
123+
assertThat(where.memberOf(expression("'AT'")).render(context)).isEqualTo("'AT' MEMBER OF o.country");
136124
// TODO: can we have this where.value(foo).memberOf(pathAndOrigin);
137-
assertThat(where.notMemberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' NOT MEMBER OF o.country");
138-
assertThat(where.neq("'AT'").render(ctx(entity))).isEqualTo("o.country != 'AT'");
125+
assertThat(where.notMemberOf(expression("'AT'")).render(context)).isEqualTo("'AT' NOT MEMBER OF o.country");
126+
assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'");
139127
}
140128

141-
@Test
129+
@Test // GH-3588
142130
void selectRendering() {
143131

144132
// make sure things are immutable
@@ -147,25 +135,12 @@ void selectRendering() {
147135
assertThat(select.count().render()).startsWith("SELECT COUNT(o)");
148136
assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o ");
149137
assertThat(select.distinct().count().render()).startsWith("SELECT COUNT(DISTINCT o) ");
150-
assertThat(JpqlQueryBuilder.selectFrom(Order.class).select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render())
151-
.startsWith("SELECT o.country ");
138+
assertThat(JpqlQueryBuilder.selectFrom(Order.class)
139+
.select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render())
140+
.startsWith("SELECT o.country ");
152141
}
153142

154-
// @Test
155-
// void sorting() {
156-
//
157-
// JpqlQueryBuilder.orderBy(new OrderExpression() , Sort.Order.asc("country"));
158-
//
159-
// Entity entity = JpqlQueryBuilder.entity(Order.class);
160-
//
161-
// AbstractJpqlQuery query = JpqlQueryBuilder.selectFrom(Order.class)
162-
// .entity()
163-
// .orderBy()
164-
// .where(context -> "1 = 1");
165-
//
166-
// }
167-
168-
@Test
143+
@Test // GH-3588
169144
void joins() {
170145

171146
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
@@ -175,14 +150,14 @@ void joins() {
175150
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
176151
PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name");
177152

178-
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
179-
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity));
153+
String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30"))
154+
.and(JpqlQueryBuilder.where(personName).eq(literal("ex40"))).render(ctx(entity));
180155

181156
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'");
182157
}
183158

184-
@Test
185-
void x2() {
159+
@Test // GH-3588
160+
void joinOnPaths() {
186161

187162
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
188163
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
@@ -191,36 +166,17 @@ void x2() {
191166
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
192167
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
193168

194-
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
195-
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
196-
197-
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
198-
}
199-
200-
@Test
201-
void x3() {
202-
203-
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
204-
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
205-
Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person");
206-
207-
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
208-
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
209-
210-
// JpqlQueryBuilder.and("x = y", "a = b"); -> x = y AND a = b
211-
212-
// JpqlQueryBuilder.nested(JpqlQueryBuilder.and("x = y", "a = b")) (x = y AND a = b)
213-
214-
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
215-
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity));
169+
String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30"))
170+
.and(JpqlQueryBuilder.where(personName).eq(literal("cstrobl"))).render(ctx(entity));
216171

217172
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
218173
}
219174

220175
static RenderContext ctx(Entity... entities) {
176+
221177
Map<Origin, String> aliases = new LinkedHashMap<>(entities.length);
222178
for (Entity entity : entities) {
223-
aliases.put(entity, entity.alias());
179+
aliases.put(entity, entity.getAlias());
224180
}
225181

226182
return new RenderContext(aliases);

‎spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,26 @@
4848
class ParameterMetadataProviderIntegrationTests {
4949

5050
@PersistenceContext EntityManager em;
51-
/* TODO
51+
5252
@Test // DATAJPA-758
53-
void forwardsParameterNameIfTransparentlyNamed() throws Exception {
53+
void usesIndexedParametersForExplicityNamedParameters() throws Exception {
5454

5555
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class));
56-
ParameterMetadata<Object> metadata = provider.next(new Part("firstname", User.class));
56+
ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("firstname", User.class));
5757

58-
assertThat(metadata.getName()).isEqualTo("name");
58+
assertThat(metadata.getName()).isNull();
59+
assertThat(metadata.getPosition()).isEqualTo(1);
5960
}
6061

6162
@Test // DATAJPA-758
62-
void forwardsParameterNameIfExplicitlyAnnotated() throws Exception {
63+
void usesIndexedParameters() throws Exception {
6364

6465
ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByLastname", String.class));
65-
ParameterMetadata<Object> metadata = provider.next(new Part("lastname", User.class));
66+
ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("lastname", User.class));
6667

67-
assertThat(metadata.getExpression().getName()).isNull();
68-
} */
68+
assertThat(metadata.getName()).isNull();
69+
assertThat(metadata.getPosition()).isEqualTo(1);
70+
}
6971

7072
@Test // DATAJPA-772
7173
void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception {

0 commit comments

Comments
 (0)
Please sign in to comment.