From 75917cf7c1e42dc50f6492f38f37192b10c471bd Mon Sep 17 00:00:00 2001 From: Yanin Vyacheslav Date: Sun, 28 Jun 2026 16:07:57 +0300 Subject: [PATCH] GH-2303: Support suffix attribute for @Embedded annotations Introduce the 'suffix' attribute within the @Embedded annotation and its associated shortcuts (@Embedded.Nullable, @Embedded.Empty) to mirror the existing prefix functionality. Update RelationalPersistentProperty, BasicRelationalPersistentProperty, and EmbeddedContext to propagate and accumulate column name suffixes recursively during embeddable nesting. Update EmbeddedRelationalPersistentProperty to transform the final database column identifier with the computed suffix chain. --- .../BasicRelationalPersistentProperty.java | 9 +++++ .../relational/core/mapping/Embedded.java | 17 ++++++++ .../core/mapping/EmbeddedContext.java | 10 +++++ .../EmbeddedRelationalPersistentProperty.java | 9 ++++- .../mapping/RelationalPersistentProperty.java | 7 ++++ ...RelationalPersistentPropertyUnitTests.java | 39 ++++++++++++++----- .../RelationalMappingContextUnitTests.java | 10 +++-- 7 files changed, 87 insertions(+), 14 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index d74246986b..1711b388f7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -61,6 +61,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent private final @Nullable ValueExpression collectionKeyColumnNameExpression; private final boolean isEmbedded; private final String embeddedPrefix; + private final String embeddedSuffix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; @@ -89,6 +90,9 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity> collectionIdColumnName = null; Lazy collectionKeyColumnName = Lazy @@ -269,6 +273,11 @@ public String getEmbeddedPrefix() { return isEmbedded() ? embeddedPrefix : ""; } + @Override + public String getEmbeddedSuffix() { + return isEmbedded() ? embeddedSuffix : ""; + } + @Override public boolean shouldCreateEmptyEmbedded() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Embedded.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Embedded.java index 9ffbfa63be..8738a75b7d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Embedded.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Embedded.java @@ -56,6 +56,11 @@ */ String prefix() default ""; + /** + * @return suffix for columns in the embedded value object. An empty {@link String} by default. + */ + String suffix() default ""; + /** * Load strategy to be used {@link Embedded#onEmpty()}. * @@ -104,6 +109,12 @@ enum OnEmpty { */ @AliasFor(annotation = Embedded.class, attribute = "prefix") String prefix() default ""; + + /** + * @return suffix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "suffix") + String suffix() default ""; } /** @@ -144,5 +155,11 @@ enum OnEmpty { */ @AliasFor(annotation = Embedded.class, attribute = "prefix") String prefix() default ""; + + /** + * @return suffix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "suffix") + String suffix() default ""; } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java index 589c403bca..b7b88a75bc 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java @@ -32,4 +32,14 @@ public String withEmbeddedPrefix(String name) { String embeddedPrefix = ownerProperty.getEmbeddedPrefix(); return embeddedPrefix + name; } + + public String withEmbeddedSuffix(String name) { + + if (!ownerProperty.isEmbedded()) { + return name; + } + + String embeddedSuffix = ownerProperty.getEmbeddedSuffix(); + return name + embeddedSuffix; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java index 3006b0dd14..6a31ee97df 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java @@ -54,9 +54,16 @@ public String getEmbeddedPrefix() { return context.withEmbeddedPrefix(delegate.getEmbeddedPrefix()); } + @Override + public String getEmbeddedSuffix() { + return context.withEmbeddedSuffix(delegate.getEmbeddedSuffix()); + } + @Override public SqlIdentifier getColumnName() { - return delegate.getColumnName().transform(context::withEmbeddedPrefix); + return delegate.getColumnName() + .transform(context::withEmbeddedPrefix) + .transform(context::withEmbeddedSuffix); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index ad61ba4a6a..8e4870dbe7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -83,6 +83,13 @@ default String getEmbeddedPrefix() { return ""; } + /** + * @return Suffix for embedded columns. If the column is not embedded the return value is empty. + */ + default String getEmbeddedSuffix() { + return ""; + } + /** * Returns whether an empty embedded object is supposed to be created for this property. */ diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java index c3349186dd..35506d4613 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java @@ -93,7 +93,7 @@ void shouldEvaluateMappedCollectionExpressions() { assertThat(property.getKeyColumn()).isEqualTo(quoted("key_col")); } - @Test // DATAJDBC-111 + @Test // DATAJDBC-111, GH-2303 void detectsEmbeddedEntity() { final RelationalPersistentEntity requiredPersistentEntity = context @@ -101,11 +101,12 @@ void detectsEmbeddedEntity() { SoftAssertions softly = new SoftAssertions(); - BiConsumer checkEmbedded = (name, prefix) -> { - + BiConsumer checkEmbedded = (name, args) -> { + String expectedPrefix = args[0]; + String expectedSuffix = args[1]; RelationalPersistentProperty property = requiredPersistentEntity.getRequiredPersistentProperty(name); - if (!prefix.isEmpty()) { + if (!expectedPrefix.isEmpty() || !expectedSuffix.isEmpty()) { softly.assertThat(property.isEmbedded()) // .describedAs(name + " is embedded") // .isTrue(); @@ -113,13 +114,19 @@ void detectsEmbeddedEntity() { softly.assertThat(property.getEmbeddedPrefix()) // .describedAs(name + " prefix") // - .isEqualTo(prefix); + .isEqualTo(expectedPrefix); + + softly.assertThat(property.getEmbeddedSuffix()) // + .describedAs(name + " suffix") // + .isEqualTo(expectedSuffix); }; - checkEmbedded.accept("someList", ""); - checkEmbedded.accept("id", ""); - checkEmbedded.accept("embeddableEntity", ""); - checkEmbedded.accept("prefixedEmbeddableEntity", "prefix"); + checkEmbedded.accept("someList", new String[]{"", ""}); + checkEmbedded.accept("id", new String[]{"", ""}); + checkEmbedded.accept("embeddableEntity", new String[]{"", ""}); + + checkEmbedded.accept("prefixedEmbeddableEntity", new String[]{"prefix", ""}); + checkEmbedded.accept("suffixedEmbeddableEntity", new String[]{"", "_suffixed"}); softly.assertAll(); } @@ -190,6 +197,18 @@ void determineSequenceNameWithSchemaSpecified() { .isEqualTo(SqlIdentifier.from(SqlIdentifier.quoted("public"), SqlIdentifier.quoted("my_seq"))); } + @Test // GH-2303 + void detectsEmbeddedEntitySuffix() { + final RelationalPersistentEntity requiredPersistentEntity = context + .getRequiredPersistentEntity(DummyEntity.class); + + RelationalPersistentProperty property = requiredPersistentEntity + .getRequiredPersistentProperty("suffixedEmbeddableEntity"); + + assertThat(property.isEmbedded()).isTrue(); + assertThat(property.getEmbeddedSuffix()).isEqualTo("_suffixed"); + } + @SuppressWarnings("unused") static class DummyEntity { @@ -230,6 +249,8 @@ static class DummyEntity { // DATAJDBC-111 private @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix") EmbeddableEntity prefixedEmbeddableEntity; + private @Embedded(onEmpty = OnEmpty.USE_NULL, suffix = "_suffixed") EmbeddableEntity suffixedEmbeddableEntity; + public DummyEntity(Long id, SomeEnum someEnum, LocalDateTime localDateTime, ZonedDateTime zonedDateTime, List listOfString, String[] arrayOfString, List listOfEntity, OtherEntity[] arrayOfEntity) { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java index 46e943f69c..5e65c34b71 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java @@ -90,7 +90,7 @@ public void rootAggregatePathsGetCached() { assertThat((Object) one).isSameAs(two); } - @Test // GH-1586 + @Test // GH-1586, GH-2303 void correctlyCascadesPrefix() { RelationalPersistentEntity entity = context.getRequiredPersistentEntity(WithEmbedded.class); @@ -102,8 +102,10 @@ void correctlyCascadesPrefix() { RelationalPersistentProperty name = childEntity.getRequiredPersistentProperty("name"); assertThat(parent.getEmbeddedPrefix()).isEqualTo("prnt_"); + assertThat(parent.getEmbeddedSuffix()).isEqualTo("_prnt"); assertThat(child.getEmbeddedPrefix()).isEqualTo("prnt_chld_"); - assertThat(name.getColumnName()).isEqualTo(SqlIdentifier.quoted("PRNT_CHLD_NAME")); + assertThat(child.getEmbeddedSuffix()).isEqualTo("_chld_prnt"); + assertThat(name.getColumnName()).isEqualTo(SqlIdentifier.quoted("PRNT_CHLD_NAME_CHLD_PRNT")); } @Test // GH-1657 @@ -125,7 +127,7 @@ static class EntityWithUuid { } static class WithEmbedded { - @Embedded.Empty(prefix = "prnt_") Parent parent; + @Embedded.Empty(prefix = "prnt_", suffix = "_prnt") Parent parent; } static class WithEmbeddedId { @@ -138,7 +140,7 @@ private record CompositeId(int a, int b) { static class Parent { - @Embedded.Empty(prefix = "chld_") Child child; + @Embedded.Empty(prefix = "chld_", suffix = "_chld") Child child; } static class Child {