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 363b946

Browse files
christophstroblmp911de
authored andcommittedJan 14, 2025
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
1 parent e14e382 commit 363b946

17 files changed

+2085
-90
lines changed
 

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuil
7979
JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> {
8080

8181
syntheticBindings.add(provider.nextSynthetic(value, scrollPosition));
82-
return JpqlQueryBuilder.expression(render(counter.incrementAndGet()));
82+
return placeholder(counter.incrementAndGet());
8383
});
8484
JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate);
8585

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ protected JpaParameters(ParametersSource parametersSource,
6363
super(parametersSource, parameterFactory);
6464
}
6565

66-
private JpaParameters(List<JpaParameter> parameters) {
66+
JpaParameters(List<JpaParameter> parameters) {
6767
super(parameters);
6868
}
6969

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

+51-43
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,30 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.springframework.data.repository.query.parser.Part.Type.*;
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;
1922

2023
import jakarta.persistence.EntityManager;
2124
import jakarta.persistence.criteria.CriteriaQuery;
2225
import jakarta.persistence.criteria.Expression;
23-
import jakarta.persistence.criteria.From;
2426
import jakarta.persistence.criteria.Predicate;
27+
import jakarta.persistence.metamodel.Attribute;
28+
import jakarta.persistence.metamodel.Bindable;
2529
import jakarta.persistence.metamodel.EntityType;
30+
import jakarta.persistence.metamodel.Metamodel;
2631
import jakarta.persistence.metamodel.SingularAttribute;
2732

2833
import java.util.ArrayList;
2934
import java.util.Collection;
3035
import java.util.Iterator;
3136
import java.util.List;
37+
import java.util.stream.Collectors;
3238

3339
import org.springframework.data.domain.Sort;
3440
import org.springframework.data.jpa.domain.JpaSort;
41+
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
3542
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin;
3643
import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding;
3744
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
@@ -56,6 +63,7 @@
5663
* @author Moritz Becker
5764
* @author Andrey Kovalev
5865
* @author Greg Turnquist
66+
* @author Christoph Strobl
5967
* @author Jinmyeong Kim
6068
*/
6169
class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
@@ -66,8 +74,8 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
6674
private final PartTree tree;
6775
private final EscapeCharacter escape;
6876
private final EntityType<?> entityType;
69-
private final From<?, ?> from;
7077
private final JpqlQueryBuilder.Entity entity;
78+
private final Metamodel metamodel;
7179

7280
/**
7381
* Create a new {@link JpaQueryCreator}.
@@ -88,12 +96,12 @@ public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvid
8896
this.templates = templates;
8997
this.escape = provider.getEscape();
9098
this.entityType = em.getMetamodel().entity(type.getDomainType());
91-
this.from = em.getCriteriaBuilder().createQuery().from(type.getDomainType());
9299
this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType());
100+
this.metamodel = em.getMetamodel();
93101
}
94102

95-
From<?, ?> getFrom() {
96-
return from;
103+
Bindable<?> getFrom() {
104+
return entityType;
97105
}
98106

99107
JpqlQueryBuilder.Entity getEntity() {
@@ -175,7 +183,7 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
175183
QueryUtils.checkSortExpression(order);
176184

177185
try {
178-
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from,
186+
expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
179187
PropertyPath.from(order.getProperty(), entityType.getJavaType())));
180188
} catch (PropertyReferenceException e) {
181189

@@ -210,12 +218,19 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
210218

211219
if (returnedType.needsCustomConstruction()) {
212220

213-
Collection<String> requiredSelection = getRequiredSelection(sort, returnedType);
221+
Collection<String> requiredSelection = null;
222+
if (returnedType.getReturnedType().getPackageName().startsWith("java.util")
223+
|| returnedType.getReturnedType().getPackageName().startsWith("jakarta.persistence")) {
224+
requiredSelection = metamodel.managedType(returnedType.getDomainType()).getAttributes().stream()
225+
.map(Attribute::getName).collect(Collectors.toList());
226+
} else {
227+
requiredSelection = getRequiredSelection(sort, returnedType);
228+
}
214229

215230
List<PathAndOrigin> paths = new ArrayList<>(requiredSelection.size());
216231
for (String selection : requiredSelection) {
217-
paths.add(
218-
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(selection, from.getJavaType()), true));
232+
paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
233+
PropertyPath.from(selection, returnedType.getDomainType()), true));
219234
}
220235

221236
if (useTupleQuery()) {
@@ -231,14 +246,14 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
231246
if (entityType.hasSingleIdAttribute()) {
232247

233248
SingularAttribute<?, ?> id = entityType.getId(entityType.getIdType().getJavaType());
234-
return selectStep.select(
235-
JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(id.getName(), from.getJavaType()), true));
249+
return selectStep.select(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
250+
PropertyPath.from(id.getName(), returnedType.getDomainType()), true));
236251

237252
} else {
238253

239254
List<PathAndOrigin> paths = entityType.getIdClassAttributes().stream()//
240-
.map(it -> JpqlUtils.toExpressionRecursively(entity, from,
241-
PropertyPath.from(it.getName(), from.getJavaType()), true))
255+
.map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
256+
PropertyPath.from(it.getName(), returnedType.getDomainType()), true))
242257
.toList();
243258
return selectStep.select(paths);
244259
}
@@ -255,12 +270,12 @@ Collection<String> getRequiredSelection(Sort sort, ReturnedType returnedType) {
255270
return returnedType.getInputProperties();
256271
}
257272

258-
String render(ParameterBinding binding) {
259-
return render(binding.getRequiredPosition());
273+
JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) {
274+
return placeholder(binding.getRequiredPosition());
260275
}
261276

262-
String render(int position) {
263-
return "?" + position;
277+
JpqlQueryBuilder.Expression placeholder(int position) {
278+
return JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(position));
264279
}
265280

266281
/**
@@ -305,33 +320,33 @@ public JpqlQueryBuilder.Predicate build() {
305320
PropertyPath property = part.getProperty();
306321
Type type = part.getType();
307322

308-
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(entity, from, property);
323+
PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property);
309324
JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas);
310325
JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas));
311326

312327
switch (type) {
313328
case BETWEEN:
314329
PartTreeParameterBinding first = provider.next(part);
315330
ParameterBinding second = provider.next(part);
316-
return where.between(render(first), render(second));
331+
return where.between(placeholder(first), placeholder(second));
317332
case AFTER:
318333
case GREATER_THAN:
319-
return where.gt(render(provider.next(part)));
334+
return where.gt(placeholder(provider.next(part)));
320335
case GREATER_THAN_EQUAL:
321-
return where.gte(render(provider.next(part)));
336+
return where.gte(placeholder(provider.next(part)));
322337
case BEFORE:
323338
case LESS_THAN:
324-
return where.lt(render(provider.next(part)));
339+
return where.lt(placeholder(provider.next(part)));
325340
case LESS_THAN_EQUAL:
326-
return where.lte(render(provider.next(part)));
341+
return where.lte(placeholder(provider.next(part)));
327342
case IS_NULL:
328343
return where.isNull();
329344
case IS_NOT_NULL:
330345
return where.isNotNull();
331346
case NOT_IN:
332-
return whereIgnoreCase.notIn(render(provider.next(part, Collection.class)));
347+
return whereIgnoreCase.notIn(placeholder(provider.next(part, Collection.class)));
333348
case IN:
334-
return whereIgnoreCase.in(render(provider.next(part, Collection.class)));
349+
return whereIgnoreCase.in(placeholder(provider.next(part, Collection.class)));
335350
case STARTING_WITH:
336351
case ENDING_WITH:
337352
case CONTAINING:
@@ -340,16 +355,16 @@ public JpqlQueryBuilder.Predicate build() {
340355
if (property.getLeafProperty().isCollection()) {
341356
where = JpqlQueryBuilder.where(entity, property);
342357

343-
return type.equals(NOT_CONTAINING) ? where.notMemberOf(render(provider.next(part)))
344-
: where.memberOf(render(provider.next(part)));
358+
return type.equals(NOT_CONTAINING) ? where.notMemberOf(placeholder(provider.next(part)))
359+
: where.memberOf(placeholder(provider.next(part)));
345360
}
346361

347362
case LIKE:
348363
case NOT_LIKE:
349364

350365
PartTreeParameterBinding parameter = provider.next(part, String.class);
351366
JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(),
352-
JpqlQueryBuilder.parameter(render(parameter)));
367+
placeholder(parameter));
353368
// Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
354369
String escapeChar = Character.toString(escape.getEscapeCharacter());
355370
return
@@ -362,23 +377,16 @@ public JpqlQueryBuilder.Predicate build() {
362377
case FALSE:
363378
return where.isFalse();
364379
case SIMPLE_PROPERTY:
365-
PartTreeParameterBinding simple = provider.next(part);
366-
367-
if (simple.isIsNullParameter()) {
368-
return where.isNull();
369-
}
370-
371-
return whereIgnoreCase.eq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(simple))));
372380
case NEGATING_SIMPLE_PROPERTY:
373381

374-
PartTreeParameterBinding negating = provider.next(part);
382+
PartTreeParameterBinding simple = provider.next(part);
375383

376-
if (negating.isIsNullParameter()) {
377-
return where.isNotNull();
384+
if (simple.isIsNullParameter()) {
385+
return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull();
378386
}
379387

380-
return whereIgnoreCase
381-
.neq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(negating))));
388+
JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata));
389+
return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression);
382390
case IS_EMPTY:
383391
case IS_NOT_EMPTY:
384392

@@ -412,8 +420,8 @@ private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.O
412420
* @param path must not be {@literal null}.
413421
* @return
414422
*/
415-
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin pas) {
416-
return potentiallyIgnoreCase(pas.path(), JpqlQueryBuilder.expression(pas));
423+
private <T> JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) {
424+
return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path));
417425
}
418426

419427
/**

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

+115-12
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
18+
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_ASC;
19+
import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DESC;
1920

2021
import java.util.ArrayList;
2122
import java.util.Arrays;
@@ -32,7 +33,9 @@
3233
import org.springframework.data.util.Predicates;
3334
import org.springframework.lang.Nullable;
3435
import org.springframework.util.Assert;
36+
import org.springframework.util.ClassUtils;
3537
import org.springframework.util.ObjectUtils;
38+
import org.springframework.util.StringUtils;
3639

3740
/**
3841
* A Domain-Specific Language to build JPQL queries using Java code.
@@ -189,7 +192,7 @@ public static Expression expression(PathAndOrigin pas) {
189192
}
190193

191194
/**
192-
* Create a simple expression from a string.
195+
* Create a simple expression from a string as is.
193196
*
194197
* @param expression
195198
* @return
@@ -201,11 +204,19 @@ public static Expression expression(String expression) {
201204
return new LiteralExpression(expression);
202205
}
203206

207+
public static Expression stringLiteral(String literal) {
208+
return new StringLiteralExpression(literal);
209+
}
210+
204211
public static Expression parameter(String parameter) {
205212

206213
Assert.hasText(parameter, "Parameter must not be empty or null");
207214

208-
return new ParameterExpression(parameter);
215+
return new ParameterExpression(new ParameterPlaceholder(parameter));
216+
}
217+
218+
public static Expression parameter(ParameterPlaceholder placeholder) {
219+
return new ParameterExpression(placeholder);
209220
}
210221

211222
public static Expression orderBy(Expression sortExpression, Sort.Order order) {
@@ -279,12 +290,12 @@ public Predicate isNotNull() {
279290

280291
@Override
281292
public Predicate isTrue() {
282-
return new LhsPredicate(rhs, "IS TRUE");
293+
return new LhsPredicate(rhs, "= TRUE");
283294
}
284295

285296
@Override
286297
public Predicate isFalse() {
287-
return new LhsPredicate(rhs, "IS FALSE");
298+
return new LhsPredicate(rhs, "= FALSE");
288299
}
289300

290301
@Override
@@ -309,7 +320,7 @@ public Predicate notIn(Expression value) {
309320

310321
@Override
311322
public Predicate inMultivalued(Expression value) {
312-
return new MemberOfPredicate(rhs, "IN", value);
323+
return new MemberOfPredicate(rhs, "IN", value); // TODO: that does not line up in my head - ahahah
313324
}
314325

315326
@Override
@@ -466,6 +477,42 @@ public String toString() {
466477
}
467478
}
468479

480+
static PathAndOrigin path(Origin origin, String path) {
481+
482+
if(origin instanceof Entity entity) {
483+
484+
try {
485+
PropertyPath from = PropertyPath.from(path, ClassUtils.forName(entity.entity, Entity.class.getClassLoader()));
486+
return new PathAndOrigin(from, entity, false);
487+
} catch (ClassNotFoundException e) {
488+
throw new RuntimeException(e);
489+
}
490+
}
491+
if(origin instanceof Join join) {
492+
493+
Origin parent = join.source;
494+
List<String> segments = new ArrayList<>();
495+
segments.add(join.path);
496+
while(!(parent instanceof Entity)) {
497+
if(parent instanceof Join pj) {
498+
parent = pj.source;
499+
segments.add(pj.path);
500+
} else {
501+
parent = null;
502+
}
503+
}
504+
505+
if(parent instanceof Entity entity) {
506+
Collections.reverse(segments);
507+
segments.add(path);
508+
PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, "."));
509+
return new PathAndOrigin(path1.path().getLeafProperty(), origin, false);
510+
}
511+
}
512+
throw new IllegalStateException(" oh no ");
513+
514+
}
515+
469516
/**
470517
* Entity selection.
471518
*
@@ -513,7 +560,9 @@ record ConstructorExpression(String resultType, Multiselect multiselect) impleme
513560

514561
@Override
515562
public String render(RenderContext context) {
516-
return "new %s(%s)".formatted(resultType, multiselect.render(context));
563+
564+
565+
return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context)));
517566
}
518567

519568
@Override
@@ -542,7 +591,9 @@ public String render(RenderContext context) {
542591
}
543592

544593
builder.append(PathExpression.render(path, context));
545-
builder.append(" ").append(path.path().getSegment());
594+
if(!context.isConstructorContext()) {
595+
builder.append(" ").append(path.path().getSegment());
596+
}
546597
}
547598

548599
return builder.toString();
@@ -583,7 +634,7 @@ default Predicate or(Predicate other) {
583634
* @param other
584635
* @return a composed predicate combining this and {@code other} using the AND operator.
585636
*/
586-
default Predicate and(Predicate other) {
637+
default Predicate and(Predicate other) { // don't like the structuring of this and the nest() thing
587638
return new AndPredicate(this, other);
588639
}
589640

@@ -799,6 +850,22 @@ public String prefixWithAlias(Origin source, String fragment) {
799850
String alias = getAlias(source);
800851
return ObjectUtils.isEmpty(source) ? fragment : alias + "." + fragment;
801852
}
853+
854+
public boolean isConstructorContext() {
855+
return false;
856+
}
857+
}
858+
859+
static class ConstructorContext extends RenderContext {
860+
861+
ConstructorContext(RenderContext rootContext) {
862+
super(rootContext.aliases);
863+
}
864+
865+
@Override
866+
public boolean isConstructorContext() {
867+
return true;
868+
}
802869
}
803870

804871
/**
@@ -807,7 +874,7 @@ public String prefixWithAlias(Origin source, String fragment) {
807874
*/
808875
public interface Origin {
809876

810-
String getName();
877+
String getName(); // TODO: mainly used along records - shoule we call this just name()?
811878
}
812879

813880
/**
@@ -1051,11 +1118,28 @@ public String toString() {
10511118
}
10521119
}
10531120

1054-
record ParameterExpression(String parameter) implements Expression {
1121+
record StringLiteralExpression(String literal) implements Expression {
10551122

10561123
@Override
10571124
public String render(RenderContext context) {
1058-
return parameter;
1125+
return "'%s'".formatted(literal.replaceAll("'", "''"));
1126+
}
1127+
1128+
public String raw() {
1129+
return literal;
1130+
}
1131+
1132+
@Override
1133+
public String toString() {
1134+
return render(RenderContext.EMPTY);
1135+
}
1136+
}
1137+
1138+
record ParameterExpression(ParameterPlaceholder parameter) implements Expression {
1139+
1140+
@Override
1141+
public String render(RenderContext context) {
1142+
return parameter.placeholder;
10591143
}
10601144

10611145
@Override
@@ -1158,6 +1242,8 @@ record InPredicate(Expression path, String operator, Expression predicate) imple
11581242

11591243
@Override
11601244
public String render(RenderContext context) {
1245+
1246+
//TODO: should we rather wrap it with nested or check if its a nested predicate before we call render
11611247
return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context));
11621248
}
11631249

@@ -1216,4 +1302,21 @@ public String toString() {
12161302
public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) {
12171303

12181304
}
1305+
1306+
public record ParameterPlaceholder(String placeholder) {
1307+
1308+
public ParameterPlaceholder {
1309+
Assert.hasText(placeholder, "Placeholder must not be null nor empty");
1310+
}
1311+
1312+
public static ParameterPlaceholder indexed(int index) {
1313+
return new ParameterPlaceholder("?%s".formatted(index));
1314+
}
1315+
1316+
public static ParameterPlaceholder named(String name) {
1317+
1318+
Assert.hasText(name, "Placeholder name must not be empty");
1319+
return new ParameterPlaceholder(":%s".formatted(name));
1320+
}
1321+
}
12191322
}

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

+154-13
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,64 @@
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;
1826
import jakarta.persistence.criteria.From;
1927
import jakarta.persistence.criteria.Join;
2028
import jakarta.persistence.criteria.JoinType;
29+
import jakarta.persistence.metamodel.Attribute;
30+
import jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
31+
import jakarta.persistence.metamodel.Bindable;
32+
import jakarta.persistence.metamodel.ManagedType;
33+
import jakarta.persistence.metamodel.Metamodel;
34+
import jakarta.persistence.metamodel.PluralAttribute;
35+
import jakarta.persistence.metamodel.SingularAttribute;
2136

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;
2243
import java.util.Objects;
2344

45+
import org.springframework.core.annotation.AnnotationUtils;
2446
import org.springframework.data.mapping.PropertyPath;
47+
import org.springframework.lang.Nullable;
48+
import org.springframework.util.StringUtils;
2549

2650
/**
2751
* @author Mark Paluch
2852
*/
2953
class JpqlUtils {
3054

31-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
32-
PropertyPath property) {
33-
return toExpressionRecursively(source, from, property, false);
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);
3466
}
3567

36-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
37-
PropertyPath property, boolean isForSelection) {
38-
return toExpressionRecursively(source, from, property, isForSelection, false);
68+
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
69+
Bindable<?> from, PropertyPath property) {
70+
return toExpressionRecursively(metamodel, source, from, property, false);
71+
}
72+
73+
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
74+
Bindable<?> from, PropertyPath property, boolean isForSelection) {
75+
return toExpressionRecursively(metamodel, source, from, property, isForSelection, false);
3976
}
4077

4178
/**
@@ -45,18 +82,18 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.O
4582
* @param property the property path
4683
* @param isForSelection is the property navigated for the selection or ordering part of the query?
4784
* @param hasRequiredOuterJoin has a parent already required an outer join?
48-
* @param <T> the type of the expression
4985
* @return the expression
5086
*/
5187
@SuppressWarnings("unchecked")
52-
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From<?, ?> from,
53-
PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
88+
static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
89+
Bindable<?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
5490

5591
String segment = property.getSegment();
5692

5793
boolean isLeafProperty = !property.hasNext();
5894

59-
boolean requiresOuterJoin = QueryUtils.requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin);
95+
boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection,
96+
hasRequiredOuterJoin);
6097

6198
// if it does not require an outer join and is a leaf, simply get the segment
6299
if (!requiresOuterJoin && isLeafProperty) {
@@ -66,17 +103,121 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.O
66103
// get or create the join
67104
JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment)
68105
: JpqlQueryBuilder.innerJoin(source, segment);
69-
JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER;
70-
Join<?, ?> join = QueryUtils.getOrCreateJoin(from, segment, joinType);
106+
// JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER;
107+
// Join<?, ?> join = QueryUtils.getOrCreateJoin(from, segment, joinType);
71108

109+
//
72110
// if it's a leaf, return the join
73111
if (isLeafProperty) {
74112
return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true);
75113
}
76114

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

117+
// ManagedType<?> managedType = ;
118+
Bindable<?> managedTypeForModel = (Bindable<?>) getManagedTypeForModel(from);
119+
// Attribute<?, ?> joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null);
79120
// recurse with the next property
80-
return toExpressionRecursively(joinSource, join, nextProperty, isForSelection, requiresOuterJoin);
121+
return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin);
122+
}
123+
124+
/**
125+
* Checks if this attribute requires an outer join. This is the case e.g. if it hadn't already been fetched with an
126+
* inner join and if it's an optional association, and if previous paths has already required outer joins. It also
127+
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999)
128+
*
129+
* @param metamodel
130+
* @param source
131+
* @param bindable
132+
* @param propertyPath
133+
* @param isForSelection
134+
* @param hasRequiredOuterJoin
135+
* @return
136+
*/
137+
static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable<?> bindable,
138+
PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) {
139+
140+
ManagedType<?> managedType = getManagedTypeForModel(bindable);
141+
Attribute<?, ?> attribute = getModelForPath(metamodel, propertyPath, managedType, bindable);
142+
143+
boolean isPluralAttribute = bindable instanceof PluralAttribute;
144+
if (attribute == null) {
145+
return isPluralAttribute;
146+
}
147+
148+
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
149+
return false;
150+
}
151+
152+
boolean isCollection = attribute.isCollection();
153+
154+
// if this path is an optional one to one attribute navigated from the not owning side we also need an
155+
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
156+
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
157+
boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
158+
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
159+
160+
boolean isLeafProperty = !propertyPath.hasNext();
161+
if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) {
162+
return false;
163+
}
164+
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;
199+
}
200+
201+
@Nullable
202+
private static Attribute<?, ?> getModelForPath(Metamodel metamodel, PropertyPath path,
203+
@Nullable ManagedType<?> managedType, Bindable<?> fallback) {
204+
205+
String segment = path.getSegment();
206+
if (managedType != null) {
207+
try {
208+
return managedType.getAttribute(segment);
209+
} catch (IllegalArgumentException ex) {
210+
// ManagedType may be erased for some vendor if the attribute is declared as generic
211+
}
212+
}
213+
214+
Class<?> fallbackType = fallback.getBindableJavaType();
215+
try {
216+
return metamodel.managedType(fallbackType).getAttribute(segment);
217+
} catch (IllegalArgumentException e) {
218+
219+
}
220+
221+
return null;
81222
}
82223
}

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

+10-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
import java.util.List;
2626

27+
import jakarta.persistence.metamodel.Bindable;
28+
import jakarta.persistence.metamodel.Metamodel;
2729
import org.springframework.data.domain.KeysetScrollPosition;
2830
import org.springframework.data.domain.Sort;
2931
import org.springframework.data.domain.Sort.Order;
@@ -77,11 +79,11 @@ public Predicate createPredicate(Root<?> root, CriteriaBuilder criteriaBuilder)
7779
}
7880

7981
@Nullable
80-
public JpqlQueryBuilder.Predicate createJpqlPredicate(From<?, ?> from, JpqlQueryBuilder.Entity entity,
82+
public JpqlQueryBuilder.Predicate createJpqlPredicate(Bindable<?> from, JpqlQueryBuilder.Entity entity,
8183
ParameterFactory factory) {
8284

8385
KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection());
84-
return delegate.createPredicate(position, sort, new JpqlStrategy(from, entity, factory));
86+
return delegate.createPredicate(position, sort, new JpqlStrategy(null, from, entity, factory));
8587
}
8688

8789
@SuppressWarnings("rawtypes")
@@ -128,22 +130,24 @@ public Predicate or(List<Predicate> intermediate) {
128130

129131
private static class JpqlStrategy implements QueryStrategy<JpqlQueryBuilder.Expression, JpqlQueryBuilder.Predicate> {
130132

131-
private final From<?, ?> from;
133+
private final Bindable<?> from;
132134
private final JpqlQueryBuilder.Entity entity;
133135
private final ParameterFactory factory;
136+
private final Metamodel metamodel;
134137

135-
public JpqlStrategy(From<?, ?> from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) {
138+
public JpqlStrategy(Metamodel metamodel, Bindable<?> from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) {
136139

137140
this.from = from;
138141
this.entity = entity;
139142
this.factory = factory;
143+
this.metamodel = metamodel;
140144
}
141145

142146
@Override
143147
public JpqlQueryBuilder.Expression createExpression(String property) {
144148

145-
PropertyPath path = PropertyPath.from(property, from.getJavaType());
146-
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, path));
149+
PropertyPath path = PropertyPath.from(property, from.getBindableJavaType());
150+
return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path));
147151
}
148152

149153
@Override

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
*
4343
* @author Thomas Darimont
4444
* @author Mark Paluch
45+
* @author Christoph Strobl
4546
*/
4647
class ParameterBinding {
4748

@@ -213,7 +214,10 @@ public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin or
213214
this.templates = templates;
214215
this.escape = escape;
215216

216-
this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType();
217+
this.type = value == null
218+
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
219+
? Type.IS_NULL
220+
: part.getType();
217221
this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase());
218222
this.noWildcards = part.getProperty().getLeafProperty().isCollection();
219223
}

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

+6-11
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
6262
private final PartTree tree;
6363
private final JpaParameters parameters;
6464

65-
private final QueryPreparer query;
65+
private final QueryPreparer queryPreparer;
6666
private final QueryPreparer countQuery;
6767
private final EntityManager em;
6868
private final EscapeCharacter escape;
@@ -102,7 +102,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
102102
this.tree = new PartTree(method.getName(), domainClass);
103103
validate(tree, parameters, method.toString());
104104
this.countQuery = new CountQueryPreparer();
105-
this.query = tree.isCountProjection() ? countQuery : new QueryPreparer();
105+
this.queryPreparer = tree.isCountProjection() ? countQuery : new QueryPreparer();
106106

107107
} catch (Exception o_O) {
108108
throw new IllegalArgumentException(
@@ -112,7 +112,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
112112

113113
@Override
114114
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
115-
return query.createQuery(accessor);
115+
return queryPreparer.createQuery(accessor);
116116
}
117117

118118
@Override
@@ -210,12 +210,7 @@ private static boolean expectsCollection(Type type) {
210210
*/
211211
private class QueryPreparer {
212212

213-
private final Map<Sort, JpqlQueryCreator> cache = new LinkedHashMap<Sort, JpqlQueryCreator>() {
214-
@Override
215-
protected boolean removeEldestEntry(Map.Entry<Sort, JpqlQueryCreator> eldest) {
216-
return size() > 256;
217-
}
218-
};
213+
private final PartTreeQueryCache cache = new PartTreeQueryCache();
219214

220215
/**
221216
* Creates a new {@link Query} for the given parameter values.
@@ -279,7 +274,7 @@ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPositio
279274
protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) {
280275

281276
synchronized (cache) {
282-
JpqlQueryCreator jpqlQueryCreator = cache.get(sort);
277+
JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties
283278
if (jpqlQueryCreator != null) {
284279
return jpqlQueryCreator;
285280
}
@@ -304,7 +299,7 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess
304299
}
305300

306301
synchronized (cache) {
307-
cache.put(sort, creator);
302+
cache.put(sort, accessor, creator);
308303
}
309304

310305
return creator;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import java.util.HashMap;
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import java.util.Objects;
22+
23+
import org.springframework.data.domain.Sort;
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.ObjectUtils;
26+
27+
/**
28+
* @author Christoph Strobl
29+
*/
30+
class PartTreeQueryCache {
31+
32+
private final Map<CacheKey, JpqlQueryCreator> cache = new LinkedHashMap<CacheKey, JpqlQueryCreator>() {
33+
@Override
34+
protected boolean removeEldestEntry(Map.Entry<CacheKey, JpqlQueryCreator> eldest) {
35+
return size() > 256;
36+
}
37+
};
38+
39+
@Nullable
40+
JpqlQueryCreator get(Sort sort, JpaParametersParameterAccessor accessor) {
41+
return cache.get(CacheKey.of(sort, accessor));
42+
}
43+
44+
@Nullable
45+
JpqlQueryCreator put(Sort sort, JpaParametersParameterAccessor accessor, JpqlQueryCreator creator) {
46+
return cache.put(CacheKey.of(sort, accessor), creator);
47+
}
48+
49+
static class CacheKey {
50+
51+
private final Sort sort;
52+
private final Map<Integer, Nulled> params;
53+
54+
public CacheKey(Sort sort, Map<Integer, Nulled> params) {
55+
this.sort = sort;
56+
this.params = params;
57+
}
58+
59+
static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) {
60+
61+
Object[] values = accessor.getValues();
62+
if (ObjectUtils.isEmpty(values)) {
63+
return new CacheKey(sort, Map.of());
64+
}
65+
66+
return new CacheKey(sort, toNullableMap(values));
67+
}
68+
69+
static Map<Integer, Nulled> toNullableMap(Object[] args) {
70+
71+
Map<Integer, Nulled> paramMap = new HashMap<>(args.length);
72+
for (int i = 0; i < args.length; i++) {
73+
paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES);
74+
}
75+
return paramMap;
76+
}
77+
78+
@Override
79+
public boolean equals(Object o) {
80+
if (o == this) {
81+
return true;
82+
}
83+
if (o == null || getClass() != o.getClass()) {
84+
return false;
85+
}
86+
CacheKey cacheKey = (CacheKey) o;
87+
return sort.equals(cacheKey.sort) && params.equals(cacheKey.params);
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
return Objects.hash(sort, params);
93+
}
94+
}
95+
96+
enum Nulled {
97+
YES, NO
98+
}
99+
100+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String pro
915915
* @param attribute the attribute name to check.
916916
* @return true if the attribute has already been inner joined
917917
*/
918-
private static boolean isAlreadyInnerJoined(From<?, ?> from, String attribute) {
918+
static boolean isAlreadyInnerJoined(From<?, ?> from, String attribute) {
919919

920920
for (Fetch<?, ?> fetch : from.getFetches()) {
921921

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

+1,039
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
21+
import jakarta.persistence.Id;
22+
import jakarta.persistence.ManyToOne;
23+
import jakarta.persistence.OneToMany;
24+
25+
import java.util.Date;
26+
import java.util.LinkedHashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
30+
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;
44+
45+
/**
46+
* @author Christoph Strobl
47+
*/
48+
class JpqlQueryBuilderUnitTests {
49+
50+
@Test
51+
void placeholdersRenderCorrectly() {
52+
53+
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1");
54+
assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.named("arg1")).render(RenderContext.EMPTY))
55+
.isEqualTo(":arg1");
56+
assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1");
57+
}
58+
59+
@Test
60+
void placeholdersErrorOnInvaludInput() {
61+
assertThatExceptionOfType(IllegalArgumentException.class)
62+
.isThrownBy(() -> JpqlQueryBuilder.parameter((String) null));
63+
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter(""));
64+
}
65+
66+
@Test
67+
void stringLiteralRendersAsQuotedString() {
68+
69+
assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'");
70+
71+
/* JPA Spec - 4.6.1 Literals:
72+
> 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'");
74+
}
75+
76+
@Test
77+
void entity() {
78+
79+
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());
84+
}
85+
86+
@Test
87+
void literalExpressionRendersAsIs() {
88+
Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, ‘, ’, person.firstName))");
89+
assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))");
90+
}
91+
92+
@Test
93+
void xxx() {
94+
95+
Entity entity = JpqlQueryBuilder.entity(Order.class);
96+
PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date");
97+
98+
String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity));
99+
100+
assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}");
101+
102+
// JpqlQueryBuilder.where(PathAndOrigin)
103+
}
104+
105+
@Test
106+
void predicateRendering() {
107+
108+
109+
Entity entity = JpqlQueryBuilder.entity(Order.class);
110+
WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country"));
111+
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'");
117+
// TODO: that is really really bad
118+
// lange namen
119+
assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')");
120+
121+
// 1 in age - cleanup what is not used - remove everything eles
122+
// 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)))
130+
.isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'");
131+
assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity)))
132+
.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");
136+
// 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'");
139+
}
140+
141+
@Test
142+
void selectRendering() {
143+
144+
// make sure things are immutable
145+
SelectStep select = JpqlQueryBuilder.selectFrom(Order.class); // the select step is mutable - not sure i like it
146+
// hm, I somehow exepect this to render only the selection part
147+
assertThat(select.count().render()).startsWith("SELECT COUNT(o)");
148+
assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o ");
149+
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 ");
152+
}
153+
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
169+
void joins() {
170+
171+
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
172+
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
173+
Join li_pr2 = JpqlQueryBuilder.innerJoin(entity, "product2");
174+
175+
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
176+
PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name");
177+
178+
String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30"))
179+
.and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity));
180+
181+
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'");
182+
}
183+
184+
@Test
185+
void x2() {
186+
187+
Entity entity = JpqlQueryBuilder.entity(LineItem.class);
188+
Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product");
189+
Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person");
190+
191+
PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name");
192+
PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name");
193+
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));
216+
217+
assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'");
218+
}
219+
220+
static RenderContext ctx(Entity... entities) {
221+
Map<Origin, String> aliases = new LinkedHashMap<>(entities.length);
222+
for (Entity entity : entities) {
223+
aliases.put(entity, entity.alias());
224+
}
225+
226+
return new RenderContext(aliases);
227+
}
228+
229+
@jakarta.persistence.Entity
230+
static class Order {
231+
232+
@Id Long id;
233+
Date date;
234+
String country;
235+
236+
@OneToMany List<LineItem> lineItems;
237+
}
238+
239+
@jakarta.persistence.Entity
240+
static class LineItem {
241+
242+
@Id Long id;
243+
244+
@ManyToOne Product product;
245+
@ManyToOne Product product2;
246+
@ManyToOne Product person;
247+
248+
}
249+
250+
@jakarta.persistence.Entity
251+
static class Person {
252+
@Id Long id;
253+
String name;
254+
}
255+
256+
@jakarta.persistence.Entity
257+
static class Product {
258+
259+
@Id Long id;
260+
261+
String name;
262+
String productType;
263+
264+
}
265+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ void recreatesQueryIfNullValueIsGiven(String criteria) throws Exception {
112112

113113
Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) }));
114114
assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY)))
115-
.contains("firstname %s :".formatted(criteria.endsWith("Not") ? "<>" : "="));
115+
.contains("firstname %s ?".formatted(criteria.endsWith("Not") ? "!=" : "="));
116116

117117
query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) }));
118118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.function.Supplier;
21+
import java.util.stream.Stream;
22+
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.FieldSource;
28+
import org.mockito.Mockito;
29+
import org.springframework.data.domain.Sort;
30+
import org.springframework.data.domain.Sort.Direction;
31+
32+
/**
33+
* @author Christoph Strobl
34+
*/
35+
public class PartTreeQueryCacheUnitTests {
36+
37+
PartTreeQueryCache cache;
38+
39+
static Supplier<Stream<Arguments>> cacheInput = () -> Stream.of(
40+
Arguments.arguments(Sort.unsorted(), StubJpaParameterParameterAccessor.accessor()), //
41+
Arguments.arguments(Sort.by(Direction.ASC, "one"), StubJpaParameterParameterAccessor.accessor()), //
42+
Arguments.arguments(Sort.by(Direction.DESC, "one"), StubJpaParameterParameterAccessor.accessor()), //
43+
Arguments.arguments(Sort.unsorted(),
44+
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), //
45+
Arguments.arguments(Sort.unsorted(),
46+
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null })), //
47+
Arguments.arguments(Sort.by(Direction.ASC, "one"),
48+
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), //
49+
Arguments.arguments(Sort.by(Direction.ASC, "one"),
50+
StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null })));
51+
52+
@BeforeEach
53+
void beforeEach() {
54+
cache = new PartTreeQueryCache();
55+
}
56+
57+
@ParameterizedTest
58+
@FieldSource("cacheInput")
59+
void getReturnsNullForEmptyCache(Sort sort, JpaParametersParameterAccessor accessor) {
60+
assertThat(cache.get(sort, accessor)).isNull();
61+
}
62+
63+
@ParameterizedTest
64+
@FieldSource("cacheInput")
65+
void getReturnsCachedInstance(Sort sort, JpaParametersParameterAccessor accessor) {
66+
67+
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
68+
69+
assertThat(cache.put(sort, accessor, queryCreator)).isNull();
70+
assertThat(cache.get(sort, accessor)).isSameAs(queryCreator);
71+
}
72+
73+
@ParameterizedTest
74+
@FieldSource("cacheInput")
75+
void cacheGetWithSort(Sort sort, JpaParametersParameterAccessor accessor) {
76+
77+
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
78+
assertThat(cache.put(Sort.by("not-in-cache"), accessor, queryCreator)).isNull();
79+
80+
assertThat(cache.get(sort, accessor)).isNull();
81+
}
82+
83+
@ParameterizedTest
84+
@FieldSource("cacheInput")
85+
void cacheGetWithccessor(Sort sort, JpaParametersParameterAccessor accessor) {
86+
87+
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
88+
assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull();
89+
90+
assertThat(cache.get(sort, accessor)).isNull();
91+
}
92+
93+
@Test
94+
void cachesOnNullableNotArgumentType() {
95+
96+
JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class);
97+
Sort sort = Sort.unsorted();
98+
assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull();
99+
100+
assertThat(cache.get(sort,
101+
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "spring", null)))
102+
.isNull();
103+
104+
assertThat(cache.get(sort,
105+
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, null, "data"))).isNull();
106+
107+
assertThat(cache.get(sort,
108+
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring")))
109+
.isSameAs(queryCreator);
110+
111+
assertThat(cache.get(Sort.by("not-in-cache"),
112+
StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring")))
113+
.isNull();
114+
}
115+
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.mockito.Mockito.when;
19+
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
24+
import org.mockito.Mockito;
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
27+
import org.springframework.data.util.TypeInformation;
28+
29+
/**
30+
* @author Christoph Strobl
31+
*/
32+
public class StubJpaParameterParameterAccessor extends JpaParametersParameterAccessor {
33+
34+
private StubJpaParameterParameterAccessor(JpaParameters parameters, Object[] values) {
35+
super(parameters, values);
36+
}
37+
38+
static JpaParametersParameterAccessor accessor(Object... values) {
39+
40+
Class<?>[] parameterTypes = Arrays.stream(values).map(it -> it != null ? it.getClass() : Object.class)
41+
.toArray(Class<?>[]::new);
42+
return accessor(parameterTypes, values);
43+
}
44+
45+
static JpaParametersParameterAccessor accessor(Class<?>... parameterTypes) {
46+
return accessor(parameterTypes, new Object[parameterTypes.length]);
47+
}
48+
49+
static AccessorBuilder accessorFor(Class<?>... parameterTypes) {
50+
return arguments -> accessor(parameterTypes, arguments);
51+
52+
}
53+
54+
interface AccessorBuilder {
55+
JpaParametersParameterAccessor withValues(Object... arguments);
56+
}
57+
58+
@SuppressWarnings({ "rawtypes", "unchecked" })
59+
static JpaParametersParameterAccessor accessor(Class<?>[] parameterTypes, Object... parameters) {
60+
61+
List<JpaParameter> parametersList = new ArrayList<>(parameterTypes.length);
62+
List<Object> valueList = new ArrayList<>(parameterTypes.length);
63+
64+
for (int i = 0; i < parameterTypes.length; i++) {
65+
66+
if (i < parameters.length) {
67+
valueList.add(parameters[i]);
68+
}
69+
70+
Class<?> parameterType = parameterTypes[i];
71+
MethodParameter mock = Mockito.mock(MethodParameter.class);
72+
when(mock.getParameterType()).thenReturn((Class) parameterType);
73+
JpaParameter parameter = new JpaParameter(mock, TypeInformation.of(parameterType));
74+
parametersList.add(parameter);
75+
}
76+
77+
return new StubJpaParameterParameterAccessor(new JpaParameters(parametersList), valueList.toArray());
78+
}
79+
80+
@Override
81+
public String toString() {
82+
List<String> parameters = new ArrayList<>(getParameters().getNumberOfParameters());
83+
84+
for (int i = 0; i < getParameters().getNumberOfParameters(); i++) {
85+
Object value = getValue(i);
86+
if (value == null) {
87+
value = "null";
88+
}
89+
parameters.add("%s: %s (%s)".formatted(i, value, getParameters().getParameter(i).getType().getSimpleName()));
90+
}
91+
return "%s".formatted(parameters);
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.util;
17+
18+
import jakarta.persistence.EntityManager;
19+
import jakarta.persistence.EntityManagerFactory;
20+
import jakarta.persistence.metamodel.EmbeddableType;
21+
import jakarta.persistence.metamodel.EntityType;
22+
import jakarta.persistence.metamodel.ManagedType;
23+
import jakarta.persistence.metamodel.Metamodel;
24+
import jakarta.persistence.spi.ClassTransformer;
25+
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Set;
29+
30+
import org.hibernate.jpa.HibernatePersistenceProvider;
31+
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl;
32+
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor;
33+
import org.springframework.data.util.Lazy;
34+
import org.springframework.instrument.classloading.SimpleThrowawayClassLoader;
35+
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
36+
37+
/**
38+
* @author Christoph Strobl
39+
*/
40+
public class TestMetaModel implements Metamodel {
41+
42+
private final String persistenceUnit;
43+
private final Set<Class<?>> managedTypes;
44+
private final Lazy<EntityManagerFactory> entityManagerFactory = Lazy.of(this::init);
45+
private final Lazy<Metamodel> metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel());
46+
private Lazy<EntityManager> enityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager());
47+
48+
TestMetaModel(Set<Class<?>> managedTypes) {
49+
this("dynamic-tests", managedTypes);
50+
}
51+
52+
TestMetaModel(String persistenceUnit, Set<Class<?>> managedTypes) {
53+
this.persistenceUnit = persistenceUnit;
54+
this.managedTypes = managedTypes;
55+
}
56+
57+
public static TestMetaModel hibernateModel(Class<?>... types) {
58+
return new TestMetaModel(Set.of(types));
59+
}
60+
61+
public static TestMetaModel hibernateModel(String persistenceUnit, Class<?>... types) {
62+
return new TestMetaModel(persistenceUnit, Set.of(types));
63+
}
64+
65+
public <X> EntityType<X> entity(Class<X> cls) {
66+
return metamodel.get().entity(cls);
67+
}
68+
69+
public <X> ManagedType<X> managedType(Class<X> cls) {
70+
return metamodel.get().managedType(cls);
71+
}
72+
73+
public <X> EmbeddableType<X> embeddable(Class<X> cls) {
74+
return metamodel.get().embeddable(cls);
75+
}
76+
77+
public Set<ManagedType<?>> getManagedTypes() {
78+
return metamodel.get().getManagedTypes();
79+
}
80+
81+
public Set<EntityType<?>> getEntities() {
82+
return metamodel.get().getEntities();
83+
}
84+
85+
public Set<EmbeddableType<?>> getEmbeddables() {
86+
return metamodel.get().getEmbeddables();
87+
}
88+
89+
public EntityManager entityManager() {
90+
return enityManager.get();
91+
}
92+
93+
EntityManagerFactory init() {
94+
95+
MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() {
96+
@Override
97+
public ClassLoader getNewTempClassLoader() {
98+
return new SimpleThrowawayClassLoader(this.getClass().getClassLoader());
99+
}
100+
101+
@Override
102+
public void addTransformer(ClassTransformer classTransformer) {
103+
// just ingnore it
104+
}
105+
};
106+
107+
persistenceUnitInfo.setPersistenceUnitName(persistenceUnit);
108+
this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName);
109+
110+
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
111+
112+
return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) {
113+
@Override
114+
public List<String> getManagedClassNames() {
115+
return persistenceUnitInfo.getManagedClassNames();
116+
}
117+
}, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build();
118+
}
119+
}

‎spring-data-jpa/src/test/resources/META-INF/persistence.xml

+8
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@
102102
</properties>
103103
</persistence-unit>
104104

105+
<persistence-unit name="dynamic-tests">
106+
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
107+
<exclude-unlisted-classes>true</exclude-unlisted-classes>
108+
<properties>
109+
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
110+
</properties>
111+
</persistence-unit>
112+
105113
<!-- Custom PUs for metadata tests -->
106114

107115
<persistence-unit name="metadata">

0 commit comments

Comments
 (0)
Please sign in to comment.