Skip to content

Commit 1e49675

Browse files
committed
Polishing.
Introduce ExpressionFactory to reduce code duplications. Unify JpqlUtils and QueryUtils expression creation to reduce code duplications. Add Eclipselink tests. Many thanks to @academey for design ideas. See #3349 Original pull request: #3922 See also: #3970
1 parent 054b7f4 commit 1e49675

File tree

10 files changed

+564
-365
lines changed

10 files changed

+564
-365
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2025 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 jakarta.persistence.metamodel.Attribute.PersistentAttributeType.*;
19+
20+
import jakarta.persistence.ManyToOne;
21+
import jakarta.persistence.OneToOne;
22+
import jakarta.persistence.metamodel.Attribute;
23+
import jakarta.persistence.metamodel.Bindable;
24+
import jakarta.persistence.metamodel.ManagedType;
25+
import jakarta.persistence.metamodel.PluralAttribute;
26+
import jakarta.persistence.metamodel.SingularAttribute;
27+
28+
import java.lang.annotation.Annotation;
29+
import java.lang.reflect.AnnotatedElement;
30+
import java.lang.reflect.Member;
31+
import java.util.Collections;
32+
import java.util.HashMap;
33+
import java.util.Map;
34+
35+
import org.jspecify.annotations.Nullable;
36+
37+
import org.springframework.core.annotation.AnnotationUtils;
38+
import org.springframework.data.mapping.PropertyPath;
39+
import org.springframework.util.StringUtils;
40+
41+
/**
42+
* Support class to build expression factories for JPA query creation.
43+
*
44+
* @author Mark Paluch
45+
* @since 4.0
46+
*/
47+
class ExpressionFactorySupport {
48+
49+
static final Map<Attribute.PersistentAttributeType, @Nullable Class<? extends Annotation>> ASSOCIATION_TYPES;
50+
51+
static {
52+
Map<Attribute.PersistentAttributeType, @Nullable Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
53+
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
54+
persistentAttributeTypes.put(ONE_TO_MANY, null);
55+
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
56+
persistentAttributeTypes.put(MANY_TO_MANY, null);
57+
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
58+
59+
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
60+
}
61+
62+
/**
63+
* Checks if this attribute requires an outer join. This is the case e.g. if it hadn't already been fetched with an
64+
* inner join and if it's an optional association, and if previous paths has already required outer joins. It also
65+
* ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999).
66+
*
67+
* @param resolver the {@link ModelPathResolver} to check for the model.
68+
* @param property the property path
69+
* @param isForSelection is the property navigated for the selection or ordering part of the query? if true, we need
70+
* to generate an explicit outer join in order to prevent Hibernate to use an inner join instead. see
71+
* https://hibernate.atlassian.net/browse/HHH-12999
72+
* @param hasRequiredOuterJoin has a parent already required an outer join?
73+
* @param isLeafProperty is leaf property
74+
* @param isRelationshipId whether property path refers to relationship id
75+
* @return whether an outer join is to be used for integrating this attribute in a query.
76+
*/
77+
public boolean requiresOuterJoin(ModelPathResolver resolver, PropertyPath property, boolean isForSelection,
78+
boolean hasRequiredOuterJoin, boolean isLeafProperty, boolean isRelationshipId) {
79+
80+
Bindable<?> propertyPathModel = resolver.resolve(property);
81+
82+
if (!(propertyPathModel instanceof Attribute<?, ?> attribute)) {
83+
return false;
84+
}
85+
86+
// not a persistent attribute type association (@OneToOne, @ManyToOne)
87+
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
88+
return false;
89+
}
90+
91+
boolean isCollection = attribute.isCollection();
92+
// if this path is an optional one to one attribute navigated from the not owning side we also need an
93+
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
94+
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
95+
boolean isInverseOptionalOneToOne = ONE_TO_ONE == attribute.getPersistentAttributeType()
96+
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
97+
98+
if ((isLeafProperty || isRelationshipId) && !isForSelection && !isCollection && !isInverseOptionalOneToOne
99+
&& !hasRequiredOuterJoin) {
100+
return false;
101+
}
102+
103+
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
104+
}
105+
106+
/**
107+
* Checks if this property path is referencing to relationship id.
108+
*
109+
* @param resolver the {@link ModelPathResolver resolver}.
110+
* @param property the property path.
111+
* @return whether in a query is relationship id.
112+
*/
113+
public boolean isRelationshipId(ModelPathResolver resolver, PropertyPath property) {
114+
115+
if (!property.hasNext()) {
116+
return false;
117+
}
118+
119+
Bindable<?> bindable = resolver.resolveNext(property);
120+
return bindable instanceof SingularAttribute<?, ?> sa && sa.isId();
121+
}
122+
123+
@SuppressWarnings("unchecked")
124+
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
125+
126+
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
127+
128+
if (associationAnnotation == null) {
129+
return defaultValue;
130+
}
131+
132+
Member member = attribute.getJavaMember();
133+
134+
if (!(member instanceof AnnotatedElement annotatedMember)) {
135+
return defaultValue;
136+
}
137+
138+
Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation);
139+
if (annotation == null) {
140+
return defaultValue;
141+
}
142+
143+
T value = (T) AnnotationUtils.getValue(annotation, propertyName);
144+
return value != null ? value : defaultValue;
145+
}
146+
147+
/**
148+
* Required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join regardless of which
149+
* join operation is specified next
150+
*
151+
* @see <a href=
152+
* "https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892">https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892</a>
153+
* @param model
154+
* @return
155+
*/
156+
static @Nullable ManagedType<?> getManagedTypeForModel(@Nullable Object model) {
157+
158+
if (model instanceof ManagedType<?> managedType) {
159+
return managedType;
160+
}
161+
162+
if (model instanceof PluralAttribute<?, ?, ?> pa) {
163+
return pa.getElementType() instanceof ManagedType<?> managedType ? managedType : null;
164+
}
165+
166+
if (!(model instanceof SingularAttribute<?, ?> singularAttribute)) {
167+
return null;
168+
}
169+
170+
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
171+
}
172+
173+
public interface ModelPathResolver {
174+
175+
/**
176+
* Resolve the {@link Bindable} for the given {@link PropertyPath}.
177+
*
178+
* @param propertyPath
179+
* @return
180+
*/
181+
@Nullable
182+
Bindable<?> resolve(PropertyPath propertyPath);
183+
184+
/**
185+
* Resolve the next {@link Bindable} for the given {@link PropertyPath}. Requires the {@link PropertyPath#hasNext()
186+
* to have a next item}.
187+
*
188+
* @param propertyPath
189+
* @return
190+
*/
191+
@Nullable
192+
Bindable<?> resolveNext(PropertyPath propertyPath);
193+
194+
}
195+
196+
}

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,16 +1019,6 @@ public interface Origin {
10191019

10201020
}
10211021

1022-
/**
1023-
* An origin that is used to select data from. selection origins are used with paths to define where a path is
1024-
* anchored.
1025-
*/
1026-
public interface Bindable {
1027-
1028-
boolean isRoot();
1029-
1030-
}
1031-
10321022
/**
10331023
* The root entity.
10341024
*/

0 commit comments

Comments
 (0)