Skip to content

Commit 6e28117

Browse files
committed
Refactor @Any annotation support to align with project conventions
- Move tests from separate file to QueryUtilsIntegrationTests - Move isAnyAnnotatedProperty method to appropriate location - Follow PR spring-projects#3922 pattern for test structure - Simplify code and remove unnecessary comments Signed-off-by: Hyunjoon Park <[email protected]> Signed-off-by: academey <[email protected]>
1 parent 376410c commit 6e28117

File tree

4 files changed

+108
-254
lines changed

4 files changed

+108
-254
lines changed

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

Lines changed: 39 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -746,50 +746,6 @@ private static Nulls toNulls(Sort.NullHandling nullHandling) {
746746
};
747747
}
748748

749-
/**
750-
* Checks if the given property path represents a property annotated with Hibernate's @Any annotation.
751-
* This is necessary because @Any associations are not present in the JPA metamodel.
752-
*
753-
* @param from the root from which to resolve the property
754-
* @param property the property path to check
755-
* @return true if the property is annotated with @Any, false otherwise
756-
* @since 4.0
757-
*/
758-
private static boolean isAnyAnnotatedProperty(From<?, ?> from, PropertyPath property) {
759-
try {
760-
// Get the Java type of the from clause
761-
Class<?> javaType = from.getJavaType();
762-
String propertyName = property.getSegment();
763-
764-
// Try to find the field
765-
Member member = null;
766-
try {
767-
member = javaType.getDeclaredField(propertyName);
768-
} catch (NoSuchFieldException ex) {
769-
// Try to find a getter method
770-
String capitalizedProperty = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
771-
try {
772-
member = javaType.getDeclaredMethod("get" + capitalizedProperty);
773-
} catch (NoSuchMethodException ex2) {
774-
// Property not found
775-
return false;
776-
}
777-
}
778-
779-
if (member instanceof AnnotatedElement annotatedElement) {
780-
// Check for Hibernate @Any annotation using reflection to avoid compile-time dependency
781-
for (Annotation annotation : annotatedElement.getAnnotations()) {
782-
if (annotation.annotationType().getName().equals("org.hibernate.annotations.Any")) {
783-
return true;
784-
}
785-
}
786-
}
787-
} catch (Exception ex) {
788-
// If anything goes wrong, assume it's not an @Any property
789-
}
790-
return false;
791-
}
792-
793749
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
794750
return toExpressionRecursively(from, property, false);
795751
}
@@ -938,6 +894,45 @@ static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyNam
938894
return value != null ? value : defaultValue;
939895
}
940896

897+
/**
898+
* Checks if the given property path represents a property annotated with Hibernate's @Any annotation.
899+
* This is necessary because @Any associations are not present in the JPA metamodel.
900+
*
901+
* @param from the root from which to resolve the property
902+
* @param property the property path to check
903+
* @return true if the property is annotated with @Any, false otherwise
904+
*/
905+
private static boolean isAnyAnnotatedProperty(From<?, ?> from, PropertyPath property) {
906+
907+
try {
908+
Class<?> javaType = from.getJavaType();
909+
String propertyName = property.getSegment();
910+
911+
Member member = null;
912+
try {
913+
member = javaType.getDeclaredField(propertyName);
914+
} catch (NoSuchFieldException ex) {
915+
String capitalizedProperty = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
916+
try {
917+
member = javaType.getDeclaredMethod("get" + capitalizedProperty);
918+
} catch (NoSuchMethodException ex2) {
919+
return false;
920+
}
921+
}
922+
923+
if (member instanceof AnnotatedElement annotatedElement) {
924+
for (Annotation annotation : annotatedElement.getAnnotations()) {
925+
if (annotation.annotationType().getName().equals("org.hibernate.annotations.Any")) {
926+
return true;
927+
}
928+
}
929+
}
930+
} catch (Exception ex) {
931+
// If anything goes wrong, assume it's not an @Any property
932+
}
933+
return false;
934+
}
935+
941936
/**
942937
* Returns an existing (fetch) join for the given attribute if one already exists or creates a new one if not.
943938
*

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

Lines changed: 0 additions & 210 deletions
This file was deleted.

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
import java.util.function.Consumer;
4747
import java.util.stream.Collectors;
4848

49+
import org.hibernate.annotations.Any;
50+
import org.hibernate.annotations.AnyDiscriminator;
51+
import org.hibernate.annotations.AnyDiscriminatorValue;
52+
import org.hibernate.annotations.AnyKeyJavaClass;
4953
import org.junit.jupiter.api.Test;
5054
import org.junit.jupiter.api.extension.ExtendWith;
5155
import org.mockito.Mockito;
@@ -370,6 +374,36 @@ void queryUtilsConsidersNullPrecedence() {
370374
}
371375
}
372376

377+
@Test // GH-2318
378+
void handlesHibernateAnyAnnotationWithoutThrowingException() {
379+
380+
doInMerchantContext((emf) -> {
381+
382+
CriteriaBuilder builder = emf.createEntityManager().getCriteriaBuilder();
383+
CriteriaQuery<EntityWithAny> query = builder.createQuery(EntityWithAny.class);
384+
Root<EntityWithAny> root = query.from(EntityWithAny.class);
385+
386+
// This would throw IllegalArgumentException without the fix
387+
PropertyPath monitorObjectPath = PropertyPath.from("monitorObject", EntityWithAny.class);
388+
assertThatNoException().isThrownBy(() -> QueryUtils.toExpressionRecursively(root, monitorObjectPath));
389+
});
390+
}
391+
392+
@Test // GH-2318
393+
void doesNotCreateJoinForAnyAnnotatedProperty() {
394+
395+
doInMerchantContext((emf) -> {
396+
397+
CriteriaBuilder builder = emf.createEntityManager().getCriteriaBuilder();
398+
CriteriaQuery<EntityWithAny> query = builder.createQuery(EntityWithAny.class);
399+
Root<EntityWithAny> root = query.from(EntityWithAny.class);
400+
401+
QueryUtils.toExpressionRecursively(root, PropertyPath.from("monitorObject", EntityWithAny.class));
402+
403+
assertThat(root.getJoins()).isEmpty();
404+
});
405+
}
406+
373407
/**
374408
* This test documents an ambiguity in the JPA spec (or it's implementation in Hibernate vs EclipseLink) that we have
375409
* to work around in the test {@link #doesNotCreateJoinForOptionalAssociationWithoutFurtherNavigation()}. See also:
@@ -434,6 +468,38 @@ static class Credential {
434468
String uid;
435469
}
436470

471+
@Entity
472+
@SuppressWarnings("unused")
473+
static class EntityWithAny {
474+
475+
@Id String id;
476+
477+
@Any
478+
@AnyDiscriminator // Default is STRING type
479+
@AnyDiscriminatorValue(discriminator = "monitorable", entity = MonitorableEntity.class)
480+
@AnyDiscriminatorValue(discriminator = "another", entity = AnotherMonitorableEntity.class)
481+
@AnyKeyJavaClass(String.class)
482+
@jakarta.persistence.JoinColumn(name = "monitor_object_id")
483+
@jakarta.persistence.Column(name = "monitor_object_type")
484+
Object monitorObject;
485+
}
486+
487+
@Entity
488+
@SuppressWarnings("unused")
489+
static class MonitorableEntity {
490+
491+
@Id String id;
492+
String name;
493+
}
494+
495+
@Entity
496+
@SuppressWarnings("unused")
497+
static class AnotherMonitorableEntity {
498+
499+
@Id String id;
500+
String code;
501+
}
502+
437503
/**
438504
* A {@link PersistenceProviderResolver} that returns only a Hibernate {@link PersistenceProvider} and ignores others.
439505
*

0 commit comments

Comments
 (0)