Skip to content

Commit ed0bf90

Browse files
committed
Merge upstream main and add @Any annotation support
- Resolved conflicts with new FromExpressionFactory structure - Added isAnyAnnotatedProperty() method to detect @Any annotations - Modified getModelForPath() to handle @Any properties gracefully - Updated ExpressionFactorySupport to treat null models as outer joins - Integrated tests into QueryUtilsIntegrationTests Signed-off-by: Hyunjoon Park <[email protected]> Signed-off-by: academey <[email protected]>
2 parents 6e28117 + b93c238 commit ed0bf90

27 files changed

+1011
-404
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory
332332
} else if (AopUtils.isAopProxy(unwrapped)) {
333333
unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped);
334334
}
335+
336+
if (unwrapped == null) {
337+
throw new IllegalStateException(
338+
"Unwrapping EntityManagerFactory from '%s' failed resulting in null".formatted(emf));
339+
}
335340
}
336341

337342
Class<?> entityManagerType = unwrapped.getClass();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ default <S extends T, R> R findBy(PredicateSpecification<T> spec,
240240
* @since 3.0
241241
* @throws InvalidDataAccessApiUsageException if the query function returns the {@link FluentQuery} instance.
242242
*/
243-
<S extends T, R> R findBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction);
243+
<S extends T, R extends @Nullable Object> R findBy(Specification<T> spec,
244+
Function<? super SpecificationFluentQuery<S>, R> queryFunction);
244245

245246
/**
246247
* Extension to {@link FetchableFluentQuery} allowing slice results and pagination with a custom count

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public void addTransformer(ClassTransformer classTransformer) {
9191

9292
this.entityManagerFactory = init(() -> {
9393

94-
managedTypes.stream().forEach(persistenceUnitInfo::addManagedClassName);
94+
managedTypes.forEach(persistenceUnitInfo::addManagedClassName);
9595

9696
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
9797

@@ -104,7 +104,7 @@ public List<String> getManagedClassNames() {
104104

105105
@Override
106106
public URL getPersistenceUnitRootUrl() {
107-
return persistenceUnitRootUrl;
107+
return persistenceUnitRootUrl != null ? persistenceUnitRootUrl : super.getPersistenceUnitRootUrl();
108108
}
109109

110110
};

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe
281281
return builder.build();
282282
}
283283

284-
private CodeBlock applyLimits(boolean exists, String pageable) {
284+
private CodeBlock applyLimits(boolean exists, @Nullable String pageable) {
285+
286+
Assert.notNull(queries, "Queries must not be null");
285287

286288
Builder builder = CodeBlock.builder();
287289

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Arrays;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Objects;
2829
import java.util.Optional;
2930
import java.util.Properties;
3031
import java.util.function.Function;
@@ -99,7 +100,7 @@ private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource con
99100
PropertiesBasedNamedQueriesFactoryBean factoryBean = new PropertiesBasedNamedQueriesFactoryBean();
100101
factoryBean.setLocations(resolver.getResources(location));
101102
factoryBean.afterPropertiesSet();
102-
return factoryBean.getObject();
103+
return Objects.requireNonNull(factoryBean.getObject());
103104
} catch (IOException e) {
104105
throw new RuntimeException(e);
105106
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInform
9797

9898
@Override
9999
public PreprocessedQuery getQuery() {
100-
return null;
100+
throw new UnsupportedOperationException();
101101
}
102102

103103
@Override
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 is null, it might be a @Any association
83+
if (propertyPathModel == null) {
84+
// For @Any associations or other non-metamodel properties, default to outer join
85+
return true;
86+
}
87+
88+
if (!(propertyPathModel instanceof Attribute<?, ?> attribute)) {
89+
return false;
90+
}
91+
92+
// not a persistent attribute type association (@OneToOne, @ManyToOne)
93+
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
94+
return false;
95+
}
96+
97+
boolean isCollection = attribute.isCollection();
98+
// if this path is an optional one to one attribute navigated from the not owning side we also need an
99+
// explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712
100+
// and https://github.com/eclipse-ee4j/jpa-api/issues/170
101+
boolean isInverseOptionalOneToOne = ONE_TO_ONE == attribute.getPersistentAttributeType()
102+
&& StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
103+
104+
if ((isLeafProperty || isRelationshipId) && !isForSelection && !isCollection && !isInverseOptionalOneToOne
105+
&& !hasRequiredOuterJoin) {
106+
return false;
107+
}
108+
109+
return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true);
110+
}
111+
112+
/**
113+
* Checks if this property path is referencing to relationship id.
114+
*
115+
* @param resolver the {@link ModelPathResolver resolver}.
116+
* @param property the property path.
117+
* @return whether in a query is relationship id.
118+
*/
119+
public boolean isRelationshipId(ModelPathResolver resolver, PropertyPath property) {
120+
121+
if (!property.hasNext()) {
122+
return false;
123+
}
124+
125+
Bindable<?> bindable = resolver.resolveNext(property);
126+
return bindable instanceof SingularAttribute<?, ?> sa && sa.isId();
127+
}
128+
129+
@SuppressWarnings("unchecked")
130+
private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
131+
132+
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
133+
134+
if (associationAnnotation == null) {
135+
return defaultValue;
136+
}
137+
138+
Member member = attribute.getJavaMember();
139+
140+
if (!(member instanceof AnnotatedElement annotatedMember)) {
141+
return defaultValue;
142+
}
143+
144+
Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation);
145+
if (annotation == null) {
146+
return defaultValue;
147+
}
148+
149+
T value = (T) AnnotationUtils.getValue(annotation, propertyName);
150+
return value != null ? value : defaultValue;
151+
}
152+
153+
/**
154+
* Required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join regardless of which
155+
* join operation is specified next
156+
*
157+
* @see <a href=
158+
* "https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892">https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892</a>
159+
* @param model
160+
* @return
161+
*/
162+
static @Nullable ManagedType<?> getManagedTypeForModel(@Nullable Object model) {
163+
164+
if (model instanceof ManagedType<?> managedType) {
165+
return managedType;
166+
}
167+
168+
if (model instanceof PluralAttribute<?, ?, ?> pa) {
169+
return pa.getElementType() instanceof ManagedType<?> managedType ? managedType : null;
170+
}
171+
172+
if (!(model instanceof SingularAttribute<?, ?> singularAttribute)) {
173+
return null;
174+
}
175+
176+
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
177+
}
178+
179+
public interface ModelPathResolver {
180+
181+
/**
182+
* Resolve the {@link Bindable} for the given {@link PropertyPath}.
183+
*
184+
* @param propertyPath
185+
* @return
186+
*/
187+
@Nullable
188+
Bindable<?> resolve(PropertyPath propertyPath);
189+
190+
/**
191+
* Resolve the next {@link Bindable} for the given {@link PropertyPath}. Requires the {@link PropertyPath#hasNext()
192+
* to have a next item}.
193+
*
194+
* @param propertyPath
195+
* @return
196+
*/
197+
@Nullable
198+
Bindable<?> resolveNext(PropertyPath propertyPath);
199+
200+
}
201+
202+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext
142142
QueryTokenStream tokens = super.visitJoinFunctionCall(ctx);
143143

144144
if (ctx.variable() != null && !tokens.isEmpty()) {
145-
transformerSupport.registerAlias(tokens.getLast());
145+
transformerSupport.registerAlias(tokens.getRequiredLast());
146146
}
147147

148148
return tokens;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
357357
}
358358
}
359359

360-
private JpqlQueryBuilder.Expression getDistanceExpression() {
360+
private JpqlQueryBuilder.@Nullable Expression getDistanceExpression() {
361361

362362
DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction());
363363

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)