Browse Source

Add validation warning for invalid Set<T> in @MappedCollection (GH-2061)

- Add validation in RelationalMappingContext.createPersistentEntity
- Add unit tests for validation scenarios

Signed-off-by: euiyoung <euiyoung1403@gmail.com>
pull/2204/head
euiyoung 1 month ago committed by euiyoung
parent
commit
1dbac6cdf0
  1. 81
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java
  2. 71
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java

81
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java

@ -16,10 +16,12 @@ @@ -16,10 +16,12 @@
package org.springframework.data.relational.core.mapping;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
@ -45,6 +47,8 @@ import org.springframework.util.Assert; @@ -45,6 +47,8 @@ import org.springframework.util.Assert;
public class RelationalMappingContext
extends AbstractMappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> {
private static final Logger logger = LoggerFactory.getLogger(RelationalMappingContext.class);
private final NamingStrategy namingStrategy;
private final Map<AggregatePathCacheKey, AggregatePath> aggregatePathCache = new ConcurrentHashMap<>();
@ -142,6 +146,9 @@ public class RelationalMappingContext @@ -142,6 +146,9 @@ public class RelationalMappingContext
this.namingStrategy, this.sqlIdentifierExpressionEvaluator);
entity.setForceQuote(isForceQuote());
// Validate Set<T> properties in @MappedCollection context
validateSetMappedCollectionProperties(entity);
return entity;
}
@ -219,6 +226,78 @@ public class RelationalMappingContext @@ -219,6 +226,78 @@ public class RelationalMappingContext
return aggregatePath;
}
/**
* Validates Set<T> properties in nested @MappedCollection scenarios.
*
* @param entity the entity to validate
*/
private <T> void validateSetMappedCollectionProperties(RelationalPersistentEntity<T> entity) {
for (RelationalPersistentProperty property : entity) {
if (isSetMappedCollection(property)) {
validateSetMappedCollectionProperty(property);
}
}
}
/**
* Checks if a property is a Set with @MappedCollection annotation.
*/
private boolean isSetMappedCollection(RelationalPersistentProperty property) {
return property.isCollectionLike()
&& Set.class.isAssignableFrom(property.getType())
&& property.isAnnotationPresent(MappedCollection.class);
}
/**
* Validates a Set<T> property in @MappedCollection context.
*
* @param property the Set property to validate
*/
private void validateSetMappedCollectionProperty(RelationalPersistentProperty property) {
Class<?> elementType = property.getComponentType();
if (elementType == null) {
return;
}
RelationalPersistentEntity<?> elementEntity = getPersistentEntity(elementType);
if (elementEntity == null) {
return;
}
boolean hasId = elementEntity.hasIdProperty();
boolean hasEntityOrCollectionReferences = hasEntityOrCollectionReferences(elementEntity);
if (!hasId && hasEntityOrCollectionReferences) {
String message = String.format(
"Invalid @MappedCollection usage: Set<%s> in %s.%s. " +
"Set elements without @Id must not contain entity or collection references. " +
"Consider using List instead or add @Id to %s.",
elementType.getSimpleName(),
property.getOwner().getType().getSimpleName(),
property.getName(),
elementType.getSimpleName()
);
logger.warn(message);
}
}
/**
* Checks if an entity has any properties that are entities or collections.
*/
private boolean hasEntityOrCollectionReferences(RelationalPersistentEntity<?> entity) {
for (RelationalPersistentProperty prop : entity) {
if (prop.isIdProperty() || prop.isVersionProperty()) {
continue;
}
if (prop.isEntity() || prop.isCollectionLike()) {
return true;
}
}
return false;
}
private record AggregatePathCacheKey(RelationalPersistentEntity<?> root,
@Nullable PersistentPropertyPath<? extends RelationalPersistentProperty> path) {

71
spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java

@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
@ -152,4 +153,74 @@ public class RelationalMappingContextUnitTests { @@ -152,4 +153,74 @@ public class RelationalMappingContextUnitTests {
static class Inherit1 extends Base {}
static class Inherit2 extends Base {}
// GH-2061 - Tests for Set<T> validation in @MappedCollection context
@Test // GH-2061
void doesNotThrowExceptionForInvalidSetUsage() {
context = new RelationalMappingContext();
context.setSimpleTypeHolder(holder);
// Should not throw exception, just log warning
assertThatCode(() -> context.getPersistentEntity(AggregateWithInvalidSet.class))
.doesNotThrowAnyException();
}
@Test // GH-2061
void doesNotThrowExceptionWhenSetElementHasId() {
context = new RelationalMappingContext();
context.setSimpleTypeHolder(holder);
assertThatCode(() -> context.getPersistentEntity(AggregateWithValidSetHavingId.class))
.doesNotThrowAnyException();
}
@Test // GH-2061
void doesNotThrowExceptionWhenSetElementWithoutIdHasNoReferences() {
context = new RelationalMappingContext();
context.setSimpleTypeHolder(holder);
assertThatCode(() -> context.getPersistentEntity(AggregateWithValidSetWithoutReferences.class))
.doesNotThrowAnyException();
}
// Test entities for GH-2061
static class AggregateWithInvalidSet {
@Id Long id;
@MappedCollection(idColumn = "aggregate_id", keyColumn = "idx")
Set<InvalidElement> elements;
}
static class InvalidElement {
String name;
OtherEntity reference;
}
static class OtherEntity {
@Id Long id;
String value;
}
static class AggregateWithValidSetHavingId {
@Id Long id;
@MappedCollection(idColumn = "aggregate_id")
Set<ElementWithId> elements;
}
static class ElementWithId {
@Id Long id;
String name;
OtherEntity reference;
}
static class AggregateWithValidSetWithoutReferences {
@Id Long id;
@MappedCollection(idColumn = "aggregate_id", keyColumn = "idx")
Set<SimpleElement> elements;
}
static class SimpleElement {
String name;
int value;
}
}

Loading…
Cancel
Save