Skip to content

Commit 6465f79

Browse files
author
euiyoung
committed
Add validation warning for invalid Set<T> in @MappedCollection (GH-2061)
- Add validation in RelationalMappingContext.createPersistentEntity - Add unit tests for validation scenarios
1 parent f8f7612 commit 6465f79

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package org.springframework.data.relational.core.mapping;
1717

1818
import java.util.Map;
19+
import java.util.Set;
1920
import java.util.concurrent.ConcurrentHashMap;
2021

22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
2124
import org.jspecify.annotations.Nullable;
22-
2325
import org.springframework.beans.BeansException;
2426
import org.springframework.context.ApplicationContext;
2527
import org.springframework.core.env.Environment;
@@ -45,6 +47,8 @@
4547
public class RelationalMappingContext
4648
extends AbstractMappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> {
4749

50+
private static final Logger logger = LoggerFactory.getLogger(RelationalMappingContext.class);
51+
4852
private final NamingStrategy namingStrategy;
4953
private final Map<AggregatePathCacheKey, AggregatePath> aggregatePathCache = new ConcurrentHashMap<>();
5054

@@ -142,6 +146,9 @@ protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformati
142146
this.namingStrategy, this.sqlIdentifierExpressionEvaluator);
143147
entity.setForceQuote(isForceQuote());
144148

149+
// Validate Set<T> properties in @MappedCollection context
150+
validateSetMappedCollectionProperties(entity);
151+
145152
return entity;
146153
}
147154

@@ -219,6 +226,78 @@ public AggregatePath getAggregatePath(RelationalPersistentEntity<?> type) {
219226
return aggregatePath;
220227
}
221228

229+
/**
230+
* Validates Set<T> properties in nested @MappedCollection scenarios.
231+
*
232+
* @param entity the entity to validate
233+
*/
234+
private <T> void validateSetMappedCollectionProperties(RelationalPersistentEntity<T> entity) {
235+
for (RelationalPersistentProperty property : entity) {
236+
if (isSetMappedCollection(property)) {
237+
validateSetMappedCollectionProperty(property);
238+
}
239+
}
240+
}
241+
242+
/**
243+
* Checks if a property is a Set with @MappedCollection annotation.
244+
*/
245+
private boolean isSetMappedCollection(RelationalPersistentProperty property) {
246+
return property.isCollectionLike()
247+
&& Set.class.isAssignableFrom(property.getType())
248+
&& property.isAnnotationPresent(MappedCollection.class);
249+
}
250+
251+
/**
252+
* Validates a Set<T> property in @MappedCollection context.
253+
*
254+
* @param property the Set property to validate
255+
*/
256+
private void validateSetMappedCollectionProperty(RelationalPersistentProperty property) {
257+
Class<?> elementType = property.getComponentType();
258+
if (elementType == null) {
259+
return;
260+
}
261+
262+
RelationalPersistentEntity<?> elementEntity = getPersistentEntity(elementType);
263+
if (elementEntity == null) {
264+
return;
265+
}
266+
267+
boolean hasId = elementEntity.hasIdProperty();
268+
boolean hasEntityOrCollectionReferences = hasEntityOrCollectionReferences(elementEntity);
269+
270+
if (!hasId && hasEntityOrCollectionReferences) {
271+
String message = String.format(
272+
"Invalid @MappedCollection usage: Set<%s> in %s.%s. " +
273+
"Set elements without @Id must not contain entity or collection references. " +
274+
"Consider using List instead or add @Id to %s.",
275+
elementType.getSimpleName(),
276+
property.getOwner().getType().getSimpleName(),
277+
property.getName(),
278+
elementType.getSimpleName()
279+
);
280+
281+
logger.warn(message);
282+
}
283+
}
284+
285+
/**
286+
* Checks if an entity has any properties that are entities or collections.
287+
*/
288+
private boolean hasEntityOrCollectionReferences(RelationalPersistentEntity<?> entity) {
289+
for (RelationalPersistentProperty prop : entity) {
290+
if (prop.isIdProperty() || prop.isVersionProperty()) {
291+
continue;
292+
}
293+
294+
if (prop.isEntity() || prop.isCollectionLike()) {
295+
return true;
296+
}
297+
}
298+
return false;
299+
}
300+
222301
private record AggregatePathCacheKey(RelationalPersistentEntity<?> root,
223302
@Nullable PersistentPropertyPath<? extends RelationalPersistentProperty> path) {
224303

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.util.HashSet;
2121
import java.util.List;
22+
import java.util.Set;
2223
import java.util.UUID;
2324

2425
import org.junit.jupiter.api.BeforeEach;
@@ -152,4 +153,74 @@ static class Base {
152153
static class Inherit1 extends Base {}
153154

154155
static class Inherit2 extends Base {}
156+
157+
// GH-2061 - Tests for Set<T> validation in @MappedCollection context
158+
159+
@Test // GH-2061
160+
void doesNotThrowExceptionForInvalidSetUsage() {
161+
context = new RelationalMappingContext();
162+
context.setSimpleTypeHolder(holder);
163+
164+
// Should not throw exception, just log warning
165+
assertThatCode(() -> context.getPersistentEntity(AggregateWithInvalidSet.class))
166+
.doesNotThrowAnyException();
167+
}
168+
169+
@Test // GH-2061
170+
void doesNotThrowExceptionWhenSetElementHasId() {
171+
context = new RelationalMappingContext();
172+
context.setSimpleTypeHolder(holder);
173+
174+
assertThatCode(() -> context.getPersistentEntity(AggregateWithValidSetHavingId.class))
175+
.doesNotThrowAnyException();
176+
}
177+
178+
@Test // GH-2061
179+
void doesNotThrowExceptionWhenSetElementWithoutIdHasNoReferences() {
180+
context = new RelationalMappingContext();
181+
context.setSimpleTypeHolder(holder);
182+
183+
assertThatCode(() -> context.getPersistentEntity(AggregateWithValidSetWithoutReferences.class))
184+
.doesNotThrowAnyException();
185+
}
186+
187+
// Test entities for GH-2061
188+
static class AggregateWithInvalidSet {
189+
@Id Long id;
190+
@MappedCollection(idColumn = "aggregate_id", keyColumn = "idx")
191+
Set<InvalidElement> elements;
192+
}
193+
194+
static class InvalidElement {
195+
String name;
196+
OtherEntity reference;
197+
}
198+
199+
static class OtherEntity {
200+
@Id Long id;
201+
String value;
202+
}
203+
204+
static class AggregateWithValidSetHavingId {
205+
@Id Long id;
206+
@MappedCollection(idColumn = "aggregate_id")
207+
Set<ElementWithId> elements;
208+
}
209+
210+
static class ElementWithId {
211+
@Id Long id;
212+
String name;
213+
OtherEntity reference;
214+
}
215+
216+
static class AggregateWithValidSetWithoutReferences {
217+
@Id Long id;
218+
@MappedCollection(idColumn = "aggregate_id", keyColumn = "idx")
219+
Set<SimpleElement> elements;
220+
}
221+
222+
static class SimpleElement {
223+
String name;
224+
int value;
225+
}
155226
}

0 commit comments

Comments
 (0)