|
16 | 16 | package org.springframework.data.relational.core.mapping; |
17 | 17 |
|
18 | 18 | import java.util.Map; |
| 19 | +import java.util.Set; |
19 | 20 | import java.util.concurrent.ConcurrentHashMap; |
20 | 21 |
|
| 22 | +import org.slf4j.Logger; |
| 23 | +import org.slf4j.LoggerFactory; |
21 | 24 | import org.jspecify.annotations.Nullable; |
22 | | - |
23 | 25 | import org.springframework.beans.BeansException; |
24 | 26 | import org.springframework.context.ApplicationContext; |
25 | 27 | import org.springframework.core.env.Environment; |
|
45 | 47 | public class RelationalMappingContext |
46 | 48 | extends AbstractMappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> { |
47 | 49 |
|
| 50 | + private static final Logger logger = LoggerFactory.getLogger(RelationalMappingContext.class); |
| 51 | + |
48 | 52 | private final NamingStrategy namingStrategy; |
49 | 53 | private final Map<AggregatePathCacheKey, AggregatePath> aggregatePathCache = new ConcurrentHashMap<>(); |
50 | 54 |
|
@@ -142,6 +146,9 @@ protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformati |
142 | 146 | this.namingStrategy, this.sqlIdentifierExpressionEvaluator); |
143 | 147 | entity.setForceQuote(isForceQuote()); |
144 | 148 |
|
| 149 | + // Validate Set<T> properties in @MappedCollection context |
| 150 | + validateSetMappedCollectionProperties(entity); |
| 151 | + |
145 | 152 | return entity; |
146 | 153 | } |
147 | 154 |
|
@@ -219,6 +226,78 @@ public AggregatePath getAggregatePath(RelationalPersistentEntity<?> type) { |
219 | 226 | return aggregatePath; |
220 | 227 | } |
221 | 228 |
|
| 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 | + |
222 | 301 | private record AggregatePathCacheKey(RelationalPersistentEntity<?> root, |
223 | 302 | @Nullable PersistentPropertyPath<? extends RelationalPersistentProperty> path) { |
224 | 303 |
|
|
0 commit comments