Skip to content

Commit be30491

Browse files
mp911deschauder
authored andcommitted
Allow using embedded Id's without Embedded annotation.
1 parent 75c4783 commit be30491

File tree

12 files changed

+89
-15
lines changed

12 files changed

+89
-15
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,14 @@ private static Function<AggregatePath, Object> getWrappedValueProvider(Function<
454454
AggregatePath aggregatePath) {
455455

456456
AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath();
457+
458+
if (!idDefiningParentPath.hasIdProperty()) {
459+
return ap -> {
460+
throw new IllegalStateException(
461+
"AggregatePath %s does not define an identifier property".formatted(idDefiningParentPath));
462+
};
463+
}
464+
457465
RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty();
458466
AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath;
459467

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentExtractorSupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2727
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2828
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
29+
import org.springframework.data.relational.core.mapping.RelationalPredicates;
2930
import org.springframework.data.relational.core.sql.SqlIdentifier;
3031
import org.springframework.data.relational.domain.RowDocument;
3132
import org.springframework.lang.Nullable;
@@ -235,7 +236,7 @@ private void readEntity(RS row, RowDocument document, AggregatePath basePath,
235236

236237
AggregatePath path = basePath.append(property);
237238

238-
if (property.isEntity() && !property.isEmbedded() && (property.isCollectionLike() || property.isQualified())) {
239+
if (RelationalPredicates.isRelation(property) && (property.isCollectionLike() || property.isQualified())) {
239240

240241
readerState.put(property, new ContainerSink<>(aggregateContext, property, path));
241242
continue;

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3434
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3535
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
36+
import org.springframework.data.relational.core.mapping.RelationalPredicates;
3637
import org.springframework.data.relational.core.sql.SqlIdentifier;
3738
import org.springframework.lang.Nullable;
3839

@@ -257,12 +258,14 @@ private <S, T> SqlIdentifierParameterSource getParameterSource(@Nullable S insta
257258
PersistentPropertyAccessor<S> propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance)
258259
: NoValuePropertyAccessor.instance();
259260

261+
260262
persistentEntity.doWithAll(property -> {
261263

262264
if (skipProperty.test(property) || !property.isWritable()) {
263265
return;
264266
}
265-
if (property.isEntity() && !property.isEmbedded()) {
267+
268+
if (RelationalPredicates.isRelation(property)) {
266269
return;
267270
}
268271

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3434
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3535
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
36+
import org.springframework.data.relational.core.mapping.RelationalPredicates;
3637
import org.springframework.data.relational.core.sql.SqlIdentifier;
3738
import org.springframework.lang.Nullable;
3839
import org.springframework.util.Assert;
@@ -68,7 +69,7 @@ public static Tables from(Stream<? extends RelationalPersistentEntity<?>> persis
6869

6970
for (RelationalPersistentProperty property : entity) {
7071

71-
if (property.isEntity() && !property.isEmbedded()) {
72+
if (RelationalPredicates.isRelation(property)) {
7273
foreignKeyMetadataList.add(createForeignKeyMetadata(entity, property, context, sqlTypeMapping));
7374
continue;
7475
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ private record WrappedPk(Long id) {
255255
}
256256

257257
private record SimpleEntity( //
258-
@Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, //
258+
@Id WrappedPk wrappedPk, //
259259
String name //
260260
) {
261261
}
@@ -272,7 +272,7 @@ private record EmbeddedPk(Long one, String two) {
272272
}
273273

274274
private record SimpleEntityWithEmbeddedPk( //
275-
@Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, //
275+
@Id EmbeddedPk embeddedPk, //
276276
String name //
277277
) {
278278
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@
4444
import org.springframework.data.mapping.PersistentPropertyAccessor;
4545
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
4646
import org.springframework.data.mapping.context.MappingContext;
47-
import org.springframework.data.mapping.model.*;
47+
import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory;
48+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
49+
import org.springframework.data.mapping.model.EntityInstantiator;
50+
import org.springframework.data.mapping.model.ParameterValueProvider;
51+
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
52+
import org.springframework.data.mapping.model.PropertyValueProvider;
53+
import org.springframework.data.mapping.model.SimpleTypeHolder;
54+
import org.springframework.data.mapping.model.SpELContext;
55+
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
56+
import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider;
4857
import org.springframework.data.projection.EntityProjection;
4958
import org.springframework.data.projection.EntityProjectionIntrospector;
5059
import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate;
@@ -578,6 +587,10 @@ private Object readEmbedded(ConversionContext conversionContext, RelationalPrope
578587
private boolean shouldReadEmbeddable(ConversionContext context, RelationalPersistentProperty property,
579588
RelationalPersistentEntity<?> unwrappedEntity, RelationalPropertyValueProvider propertyValueProvider) {
580589

590+
if (property.isIdProperty() && !property.isAnnotationPresent(Embedded.class)) {
591+
return true;
592+
}
593+
581594
OnEmpty onEmpty = property.getRequiredAnnotation(Embedded.class).onEmpty();
582595

583596
if (onEmpty.equals(OnEmpty.USE_EMPTY)) {

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.mapping.PersistentPropertyPath;
2525
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2626
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
27+
import org.springframework.data.relational.core.mapping.RelationalPredicates;
2728
import org.springframework.lang.Nullable;
2829
import org.springframework.util.Assert;
2930

@@ -124,7 +125,7 @@ private List<DbAction<?>> deleteReferencedEntities(Object id, AggregateChange<?>
124125
private void forAllTableRepresentingPaths(Class<?> entityType,
125126
Consumer<PersistentPropertyPath<RelationalPersistentProperty>> pathConsumer) {
126127

127-
context.findPersistentPropertyPaths(entityType, property -> property.isEntity() && !property.isEmbedded()) //
128+
context.findPersistentPropertyPaths(entityType, RelationalPredicates.isRelation()) //
128129
.filter(path -> context.getAggregatePath(path).isWritable()) //
129130
.forEach(pathConsumer);
130131
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2828
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2929
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
30+
import org.springframework.data.relational.core.mapping.RelationalPredicates;
3031
import org.springframework.data.util.Pair;
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.util.Assert;
@@ -61,7 +62,7 @@ class WritingContext<T> {
6162
this.aggregateChange = aggregateChange;
6263
this.rootIdValueSource = IdValueSource.forInstance(root,
6364
context.getRequiredPersistentEntity(aggregateChange.getEntityType()));
64-
this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()) //
65+
this.paths = context.findPersistentPropertyPaths(entityType, RelationalPredicates::isRelation) //
6566
.filter(ppp -> context.getAggregatePath(ppp).isWritable()).toList();
6667
}
6768

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ public boolean isOrdered() {
250250

251251
@Override
252252
public boolean isEmbedded() {
253-
return isEmbedded;
253+
return isEmbedded || (isIdProperty() && isEntity());
254254
}
255255

256256
@Override
@@ -263,7 +263,8 @@ public boolean shouldCreateEmptyEmbedded() {
263263

264264
Embedded findAnnotation = findAnnotation(Embedded.class);
265265

266-
return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty());
266+
return (findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty()))
267+
|| (isIdProperty() && isEntity());
267268
}
268269

269270
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.mapping;
17+
18+
import java.util.function.Predicate;
19+
20+
/**
21+
* Collection of relational predicates.
22+
*
23+
* @author Mark Paluch
24+
* @since 4.0
25+
*/
26+
public class RelationalPredicates {
27+
28+
/**
29+
* Predicate to determine whether a property is a relation (i.e. it is an entity, not an identifier property, and not
30+
* an embedded property).
31+
*
32+
* @return a predicate that tests if the given property is a relation.
33+
*/
34+
public static Predicate<? super RelationalPersistentProperty> isRelation() {
35+
return RelationalPredicates::isRelation;
36+
}
37+
38+
/**
39+
* Determine whether a property is a relation (i.e. it is an entity, not an identifier property, and not an embedded
40+
* property).
41+
*
42+
* @return {@literal true} if the property is a relation; {@literal false} otherwise.
43+
*/
44+
public static boolean isRelation(RelationalPersistentProperty property) {
45+
return !property.isIdProperty() && property.isEntity() && !property.isEmbedded();
46+
}
47+
}

src/main/antora/modules/ROOT/partials/mapping-annotations.adoc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
The `RelationalConverter` can use metadata to drive the mapping of objects to rows.
22
The following annotations are available:
33

4-
* `@Embedded`: an entity with this annotation will be mapped to the table of the parent entity, instead of a separate table.
4+
* `@Embedded`: a property with this annotation will be mapped to the table of the parent entity, instead of a separate table.
55
Allows to specify if the resulting columns should have a common prefix.
66
If all columns resulting from such an entity are `null` either the annotated entity will be `null` or _empty_, i.e. all of its properties will be `null`, depending on the value of `@Embedded.onEmpty()`
77
May be combined with `@Id` to form a composite id.
@@ -32,5 +32,3 @@ However, this is not recommended, since it may cause problems with other tools.
3232
The value is `null` (`zero` for primitive types) is considered as marker for entities to be new.
3333
The initially stored value is `zero` (`one` for primitive types).
3434
The version gets incremented automatically on every update.
35-
36-

src/main/antora/modules/ROOT/partials/mapping.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ endif::[]
151151
[[entity-persistence.embedded-ids]]
152152
=== Embedded Ids
153153

154-
Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side.
154+
The identifier property can be annotated with `@Embedded` allowing to use composite ids.
155155
The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements.
156156
Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate.
157157

@@ -189,7 +189,7 @@ CREATE TABLE PERSON_WITH_COMPOSITE_ID (
189189
<2> `pk` is marked as id and embedded
190190
<3> the two columns from the embedded `Name` entity make up the primary key in the database.
191191
192-
Details of the create tables will depend on the database used.
192+
Details of table creation depends on the used database.
193193
====
194194

195195
[[entity-persistence.read-only-properties]]

0 commit comments

Comments
 (0)