Skip to content

Commit ab1122e

Browse files
academeyclaude
andcommitted
Support Hibernate @Any annotation in query derivation.
Added special handling for Hibernate's @Any annotation in QueryUtils to prevent IllegalArgumentException when deriving queries for properties annotated with @Any. Since @Any associations are not part of the JPA metamodel, they require special detection and handling using reflection. The fix: - Detects @Any annotated properties using reflection to avoid compile-time Hibernate dependency - Returns the property path directly without metamodel validation for @Any properties - Treats @Any associations as optional (requiring outer joins) - Handles metamodel lookup failures gracefully when encountering @Any properties This change allows Spring Data JPA repositories to use query derivation methods for entities containing @Any associations without throwing exceptions. Fixes #2318 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d5d9332 commit ab1122e

File tree

9 files changed

+520
-1
lines changed

9 files changed

+520
-1
lines changed

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,13 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
773773

774774
boolean isLeafProperty = !property.hasNext();
775775

776+
// Check if this is a Hibernate @Any annotated property
777+
if (isAnyAnnotatedProperty(from, property)) {
778+
// For @Any associations, we need to handle them specially since they're not in the metamodel
779+
// Simply return the path expression without further processing
780+
return from.get(segment);
781+
}
782+
776783
boolean requiresOuterJoin = requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin);
777784

778785
// if it does not require an outer join and is a leaf, simply get the segment
@@ -816,6 +823,12 @@ static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean
816823
return false;
817824
}
818825

826+
// Check if this is a @Any annotated property
827+
if (isAnyAnnotatedProperty(from, property)) {
828+
// @Any associations should be treated as optional associations
829+
return true;
830+
}
831+
819832
Bindable<?> model = from.getModel();
820833
ManagedType<?> managedType = getManagedTypeForModel(model);
821834
Bindable<?> propertyPathModel = getModelForPath(property, managedType, from);
@@ -827,6 +840,12 @@ static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean
827840
return true;
828841
}
829842

843+
// If propertyPathModel is null, it might be a @Any association
844+
if (propertyPathModel == null) {
845+
// For @Any associations or other non-metamodel properties, default to outer join
846+
return true;
847+
}
848+
830849
if (!(propertyPathModel instanceof Attribute<?, ?> attribute)) {
831850
return false;
832851
}
@@ -969,10 +988,17 @@ static void checkSortExpression(Order order) {
969988
return (Bindable<?>) managedType.getAttribute(segment);
970989
} catch (IllegalArgumentException ex) {
971990
// ManagedType may be erased for some vendor if the attribute is declared as generic
991+
// or the attribute is not part of the metamodel (e.g., @Any annotation)
972992
}
973993
}
974994

975-
return fallback.get(segment).getModel();
995+
try {
996+
return fallback.get(segment).getModel();
997+
} catch (IllegalArgumentException ex) {
998+
// This can happen with @Any annotated properties as they're not in the metamodel
999+
// Return null to indicate the property cannot be resolved through the metamodel
1000+
return null;
1001+
}
9761002
}
9771003

9781004
/**
@@ -996,4 +1022,48 @@ static void checkSortExpression(Order order) {
9961022

9971023
return singularAttribute.getType() instanceof ManagedType<?> managedType ? managedType : null;
9981024
}
1025+
1026+
/**
1027+
* Checks if the given property path represents a property annotated with Hibernate's @Any annotation.
1028+
* This is necessary because @Any associations are not present in the JPA metamodel.
1029+
*
1030+
* @param from the root from which to resolve the property
1031+
* @param property the property path to check
1032+
* @return true if the property is annotated with @Any, false otherwise
1033+
* @since 4.0
1034+
*/
1035+
private static boolean isAnyAnnotatedProperty(From<?, ?> from, PropertyPath property) {
1036+
try {
1037+
// Get the Java type of the from clause
1038+
Class<?> javaType = from.getJavaType();
1039+
String propertyName = property.getSegment();
1040+
1041+
// Try to find the field
1042+
Member member = null;
1043+
try {
1044+
member = javaType.getDeclaredField(propertyName);
1045+
} catch (NoSuchFieldException ex) {
1046+
// Try to find a getter method
1047+
String capitalizedProperty = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
1048+
try {
1049+
member = javaType.getDeclaredMethod("get" + capitalizedProperty);
1050+
} catch (NoSuchMethodException ex2) {
1051+
// Property not found
1052+
return false;
1053+
}
1054+
}
1055+
1056+
if (member instanceof AnnotatedElement annotatedElement) {
1057+
// Check for Hibernate @Any annotation using reflection to avoid compile-time dependency
1058+
for (Annotation annotation : annotatedElement.getAnnotations()) {
1059+
if (annotation.annotationType().getName().equals("org.hibernate.annotations.Any")) {
1060+
return true;
1061+
}
1062+
}
1063+
}
1064+
} catch (Exception ex) {
1065+
// If anything goes wrong, assume it's not an @Any property
1066+
}
1067+
return false;
1068+
}
9991069
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.domain.sample;
17+
18+
import jakarta.persistence.Entity;
19+
20+
/**
21+
* Concrete implementation of MonitorObject for @Any annotation testing.
22+
*/
23+
@Entity
24+
public class ConcreteMonitor1 extends MonitorObject {
25+
26+
private String property1;
27+
28+
public String getProperty1() {
29+
return property1;
30+
}
31+
32+
public void setProperty1(String property1) {
33+
this.property1 = property1;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.domain.sample;
17+
18+
import jakarta.persistence.Entity;
19+
20+
/**
21+
* Concrete implementation of MonitorObject for @Any annotation testing.
22+
*/
23+
@Entity
24+
public class ConcreteMonitor2 extends MonitorObject {
25+
26+
private String property2;
27+
28+
public String getProperty2() {
29+
return property2;
30+
}
31+
32+
public void setProperty2(String property2) {
33+
this.property2 = property2;
34+
}
35+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.domain.sample;
17+
18+
import jakarta.persistence.Column;
19+
import jakarta.persistence.Entity;
20+
import jakarta.persistence.FetchType;
21+
import jakarta.persistence.GeneratedValue;
22+
import jakarta.persistence.GenerationType;
23+
import jakarta.persistence.Id;
24+
import jakarta.persistence.JoinColumn;
25+
26+
import org.hibernate.annotations.Any;
27+
import org.hibernate.annotations.AnyDiscriminator;
28+
import org.hibernate.annotations.AnyDiscriminatorValue;
29+
import org.hibernate.annotations.AnyKeyJavaClass;
30+
31+
/**
32+
* Entity using @Any annotation for polymorphic association.
33+
*/
34+
@Entity
35+
public class EntityWithAny {
36+
37+
@Id
38+
@GeneratedValue(strategy = GenerationType.IDENTITY)
39+
private Long id;
40+
41+
@Any(fetch = FetchType.LAZY)
42+
@AnyDiscriminator(org.hibernate.annotations.DiscriminatorType.INTEGER)
43+
@AnyDiscriminatorValue(discriminator = "1", entity = ConcreteMonitor1.class)
44+
@AnyDiscriminatorValue(discriminator = "2", entity = ConcreteMonitor2.class)
45+
@AnyKeyJavaClass(String.class)
46+
@Column(name = "monitor_type")
47+
@JoinColumn(name = "monitor_name")
48+
private MonitorObject monitorObject;
49+
50+
private String description;
51+
52+
public Long getId() {
53+
return id;
54+
}
55+
56+
public void setId(Long id) {
57+
this.id = id;
58+
}
59+
60+
public MonitorObject getMonitorObject() {
61+
return monitorObject;
62+
}
63+
64+
public void setMonitorObject(MonitorObject monitorObject) {
65+
this.monitorObject = monitorObject;
66+
}
67+
68+
public String getDescription() {
69+
return description;
70+
}
71+
72+
public void setDescription(String description) {
73+
this.description = description;
74+
}
75+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.domain.sample;
17+
18+
import jakarta.persistence.Id;
19+
import jakarta.persistence.MappedSuperclass;
20+
21+
/**
22+
* Base class for monitor objects used in @Any annotation testing.
23+
*/
24+
@MappedSuperclass
25+
public abstract class MonitorObject {
26+
27+
@Id
28+
protected String name;
29+
30+
public String getName() {
31+
return name;
32+
}
33+
34+
public void setName(String name) {
35+
this.name = name;
36+
}
37+
}

0 commit comments

Comments
 (0)