diff --git a/src/main/java/org/springframework/data/annotation/Reference.java b/src/main/java/org/springframework/data/annotation/Reference.java index 0b2bfd3be..f87dc97bd 100644 --- a/src/main/java/org/springframework/data/annotation/Reference.java +++ b/src/main/java/org/springframework/data/annotation/Reference.java @@ -21,6 +21,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * Meta-annotation to be used to annotate annotations that mark references to other objects. * @@ -30,4 +32,22 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(value = { FIELD, METHOD, ANNOTATION_TYPE }) public @interface Reference { + + /** + * Explicitly define the target type of the reference. Used in case the annotated property is not the target type but + * rather an identifier and/or if that identifier type is not uniquely identifying the target entity. + * + * @return + */ + @AliasFor(attribute = "to") + Class> value() default Class.class; + + /** + * Explicitly define the target type of the reference. Used in case the annotated property is not the target type but + * rather an identifier and/or if that identifier type is not uniquely identifying the target entity. + * + * @return + */ + @AliasFor(attribute = "value") + Class> to() default Class.class; } diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index cc5be87f5..2814bf36b 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -333,4 +333,15 @@ public interface PersistentProperty
> {
return AnnotatedElementUtils.hasAnnotation(getActualType(), annotationType);
}
+
+ /**
+ * Return the type the property refers to in case it's an association.
+ *
+ * @return the type the property refers to in case it's an association or {@literal null} in case it's not an
+ * association, the target entity type is not explicitly defined (either explicitly or through the property
+ * type itself).
+ * @since 2.1
+ */
+ @Nullable
+ Class> getAssociationTargetType();
}
diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
index 3a0c864bf..176bcf8b9 100644
--- a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
+++ b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
@@ -15,6 +15,8 @@
*/
package org.springframework.data.mapping.context;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.function.BiFunction;
@@ -24,6 +26,7 @@ import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -44,9 +47,23 @@ public class PersistentEntities implements Streamable getAssociationTargetType() {
+
+ Reference reference = findAnnotation(Reference.class);
+
+ if (reference == null) {
+ return isEntity() ? getActualType() : null;
+ }
+
+ Class> targetType = reference.to();
+
+ return Class.class.equals(targetType) //
+ ? isEntity() ? getActualType() : null //
+ : targetType;
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString()
diff --git a/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java b/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java
index c05f6a509..ddf9ae07a 100755
--- a/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java
@@ -25,6 +25,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.util.ClassTypeInformation;
/**
@@ -72,10 +75,100 @@ public class PersistentEntitiesUnitTests {
assertThat(entities.getManagedTypes()).contains(ClassTypeInformation.from(Sample.class));
assertThat(entities.getPersistentEntity(Sample.class)).hasValueSatisfying(it -> assertThat(entities).contains(it));
+ }
+ @Test // DATACMNS-1318
+ public void detectsReferredToEntity() {
+
+ SampleMappingContext context = new SampleMappingContext();
+ context.getPersistentEntity(Sample.class);
+
+ SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class)//
+ .getPersistentProperty("sampleId");
+
+ PersistentEntity, ?> referredToEntity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
+
+ assertThat(referredToEntity).isNotNull();
+ assertThat(referredToEntity.getType()).isEqualTo(Sample.class);
+ }
+
+ @Test // DATACMNS-1318
+ public void rejectsAmbiguousIdentifierType() {
+
+ SampleMappingContext context = new SampleMappingContext();
+ context.getPersistentEntity(FirstWithLongId.class);
+ context.getPersistentEntity(SecondWithLongId.class);
+
+ SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
+ .getPersistentProperty("longId");
+
+ PersistentEntities entities = PersistentEntities.of(context);
+
+ assertThatExceptionOfType(IllegalStateException.class)//
+ .isThrownBy(() -> entities.getEntityUltimatelyReferredToBy(property)) //
+ .withMessageContaining(FirstWithLongId.class.getName()) //
+ .withMessageContaining(SecondWithLongId.class.getName()) //
+ .withMessageContaining(Reference.class.getSimpleName());
+ }
+
+ @Test // DATACMNS-1318
+ public void allowsExplicitlyQualifiedReference() {
+
+ SampleMappingContext context = new SampleMappingContext();
+ context.getPersistentEntity(FirstWithLongId.class);
+ context.getPersistentEntity(SecondWithLongId.class);
+
+ SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
+ .getPersistentProperty("qualifiedLongId");
+
+ PersistentEntity, ?> entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
+
+ assertThat(entity).isNotNull();
+ assertThat(entity.getType()).isEqualTo(FirstWithLongId.class);
+ }
+
+ @Test // DATACMNS-1318
+ public void allowsGenericReference() {
+
+ SampleMappingContext context = new SampleMappingContext();
+ context.getPersistentEntity(FirstWithGenericId.class);
+ context.getPersistentEntity(SecondWithGenericId.class);
+
+ SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
+ .getPersistentProperty("generic");
+
+ PersistentEntity, ?> entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
+
+ assertThat(entity).isNotNull();
+ assertThat(entity.getType()).isEqualTo(SecondWithGenericId.class);
}
static class Sample {
-
+ @Id String id;
}
+
+ static class WithReference {
+ @Reference String sampleId;
+ @Reference Long longId;
+ @Reference(FirstWithLongId.class) Long qualifiedLongId;
+ @Reference Identifier property.getRequiredAnnotation(Transient.class)).isInstanceOf(IllegalStateException.class);
}
+ @Test // DATACMNS-1318
+ public void detectsUltimateAssociationTargetClass() {
+
+ Stream.of("toSample", "toSample2", "sample", "withoutAnnotation").forEach(it -> {
+ assertThat(getProperty(WithReferences.class, it).getAssociationTargetType()).isEqualTo(Sample.class);
+ });
+ }
+
@SuppressWarnings("unchecked")
private Map