From b03fb5e54e5e872bfe4ca0b47c4bb990a68171fc Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Mon, 7 Apr 2025 10:07:31 +0200 Subject: [PATCH] HHH-19362 JsonHelper improvements to handle more mapping types - Add the `expandProperties` flag that indicates when serializing entier object trees - Handle entity values, including tracking circular relationships - Handle plural attribute values --- .../AbstractPostgreSQLStructJdbcType.java | 6 +- .../org/hibernate/dialect/JsonHelper.java | 355 ++++++++++++++---- .../org/hibernate/dialect/StructHelper.java | 17 +- .../org/hibernate/dialect/StructJdbcType.java | 6 +- .../java/org/hibernate/dialect/XmlHelper.java | 4 +- 5 files changed, 302 insertions(+), 86 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java index d64f7578c902..a5d51af79f4d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java @@ -41,7 +41,7 @@ import org.hibernate.type.descriptor.jdbc.StructJdbcType; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.dialect.StructHelper.getEmbeddedPart; +import static org.hibernate.dialect.StructHelper.getSubPart; import static org.hibernate.dialect.StructHelper.instantiate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; @@ -997,7 +997,7 @@ private SelectableMapping getJdbcValueSelectable(int jdbcValueSelectableIndex) { final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); int count = 0; for ( int i = 0; i < size; i++ ) { - final ValuedModelPart modelPart = getEmbeddedPart( embeddableMappingType, orderMapping[i] ); + final ValuedModelPart modelPart = getSubPart( embeddableMappingType, orderMapping[i] ); if ( modelPart.getMappedType() instanceof EmbeddableMappingType embeddableMappingType ) { final SelectableMapping aggregateMapping = embeddableMappingType.getAggregateMapping(); if ( aggregateMapping == null ) { @@ -1376,7 +1376,7 @@ private StructAttributeValues getAttributeValues( attributeIndex = orderMapping[i]; } jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, attributeIndex ), + getSubPart( embeddableMappingType, attributeIndex ), attributeValues, attributeIndex, rawJdbcValues, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java index 57175b0c719f..ae1bb8d6cadb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java @@ -4,7 +4,6 @@ */ package org.hibernate.dialect; - import java.io.OutputStream; import java.lang.reflect.Array; import java.sql.SQLException; @@ -13,20 +12,38 @@ import java.util.AbstractCollection; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.hibernate.Internal; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.collection.spi.CollectionSemantics; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.collection.spi.PersistentMap; import org.hibernate.internal.build.AllowReflection; import org.hibernate.internal.util.CharSequenceHelper; import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; +import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; @@ -44,11 +61,12 @@ import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; -import static org.hibernate.dialect.StructHelper.getEmbeddedPart; +import static org.hibernate.Hibernate.isInitialized; +import static org.hibernate.dialect.StructHelper.getSubPart; import static org.hibernate.dialect.StructHelper.instantiate; /** - * A Helper for serializing and deserializing JSON, based on an {@link org.hibernate.metamodel.mapping.EmbeddableMappingType}. + * A Helper for serializing and deserializing JSON, based on the {@link org.hibernate.metamodel.mapping mapping model}. */ @Internal public class JsonHelper { @@ -58,7 +76,7 @@ public static String toString(EmbeddableMappingType embeddableMappingType, Objec return null; } final StringBuilder sb = new StringBuilder(); - toString( embeddableMappingType, value, options, new JsonAppender( sb ) ); + toString( value, embeddableMappingType, options, new JsonAppender( sb, false ) ); return sb.toString(); } @@ -67,11 +85,11 @@ public static String arrayToString(MappingType elementMappingType, Object[] valu return "[]"; } final StringBuilder sb = new StringBuilder(); - final JsonAppender jsonAppender = new JsonAppender( sb ); + final JsonAppender jsonAppender = new JsonAppender( sb, false ); char separator = '['; for ( Object value : values ) { sb.append( separator ); - toString( elementMappingType, value, options, jsonAppender ); + toString( value, elementMappingType, options, jsonAppender ); separator = ','; } sb.append( ']' ); @@ -87,7 +105,7 @@ public static String arrayToString( return "[]"; } final StringBuilder sb = new StringBuilder(); - final JsonAppender jsonAppender = new JsonAppender( sb ); + final JsonAppender jsonAppender = new JsonAppender( sb, false ); char separator = '['; for ( Object value : values ) { sb.append( separator ); @@ -99,77 +117,258 @@ public static String arrayToString( return sb.toString(); } - private static void toString(EmbeddableMappingType embeddableMappingType, Object value, WrapperOptions options, JsonAppender appender) { - toString( embeddableMappingType, options, appender, value, '{' ); - appender.append( '}' ); - } - - private static void toString( - EmbeddableMappingType embeddableMappingType, + private static void managedTypeToString( + Object object, + ManagedMappingType managedMappingType, WrapperOptions options, JsonAppender appender, - Object domainValue, char separator) { - final Object[] values = embeddableMappingType.getValues( domainValue ); + final Object[] values = managedMappingType.getValues( object ); for ( int i = 0; i < values.length; i++ ) { - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); - if ( attributeMapping instanceof SelectableMapping selectableMapping ) { - final String name = selectableMapping.getSelectableName(); - appender.append( separator ); - appender.append( '"' ); - appender.append( name ); - appender.append( "\":" ); - toString( attributeMapping.getMappedType(), values[i], options, appender ); + final ValuedModelPart subPart = getSubPart( managedMappingType, i ); + final Object value = values[i]; + separator = toString( value, subPart, options, appender, separator ); + } + } + + public static Character toString( + Object value, + ValuedModelPart modelPart, + WrapperOptions options, + JsonAppender appender, + Character separator) { + if ( modelPart instanceof SelectableMapping selectable ) { + separateAndQuote( + () -> appender.expandProperties() ? modelPart.getPartName() : selectable.getSelectableName(), + separator, + appender + ); + toString( value, modelPart.getMappedType(), options, appender ); + return ','; + } + else if ( modelPart instanceof EmbeddedAttributeMapping embeddedAttribute ) { + if ( appender.expandProperties() ) { + separateAndQuote( embeddedAttribute::getAttributeName, separator, appender ); + toString( value, embeddedAttribute.getMappedType(), options, appender ); } - else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { - if ( values[i] == null ) { + else { + if ( value == null ) { // Skipping the update of the separator is on purpose - continue; + return separator; } - final EmbeddableMappingType mappingType = (EmbeddableMappingType) attributeMapping.getMappedType(); + + final EmbeddableMappingType mappingType = embeddedAttribute.getMappedType(); final SelectableMapping aggregateMapping = mappingType.getAggregateMapping(); if ( aggregateMapping == null ) { - toString( - mappingType, - options, - appender, - values[i], - separator - ); + managedTypeToString( value, mappingType, options, appender, separator ); } else { - final String name = aggregateMapping.getSelectableName(); - appender.append( separator ); - appender.append( '"' ); - appender.append( name ); - appender.append( "\":" ); - toString( mappingType, values[i], options, appender ); + separateAndQuote( aggregateMapping::getSelectableName, separator, appender ); + toString( value, mappingType, options, appender ); } } + return ','; + } + else if ( appender.expandProperties() ) { + if ( modelPart instanceof EntityValuedModelPart entityPart ) { + separateAndQuote( entityPart::getPartName, separator, appender ); + toString( value, entityPart.getEntityMappingType(), options, appender ); + return ','; + } + else if ( modelPart instanceof PluralAttributeMapping plural ) { + separateAndQuote( plural::getPartName, separator, appender ); + pluralAttributeToString( value, plural, options, appender ); + return ','; + } + } + + // could not handle model part, throw exception + throw new UnsupportedOperationException( + "Support for model part type not yet implemented: " + + ( modelPart != null ? modelPart.getClass().getName() : "null" ) + ); + } + + private static void separateAndQuote(Supplier nameSupplier, Character separator, JsonAppender appender) { + if ( separator != null ) { + final String name = nameSupplier.get(); + appender.append( separator ).append( '"' ).append( name ).append( "\":" ); + } + } + + private static void entityToString( + Object value, + EntityMappingType entityType, + WrapperOptions options, + JsonAppender appender) { + final EntityIdentifierMapping identifierMapping = entityType.getIdentifierMapping(); + appender.trackingEntity( value, entityType, shouldProcessEntity -> { + if ( shouldProcessEntity ) { + appender.append( "{\"" ).append( identifierMapping.getAttributeName() ).append( "\":" ); + entityIdentifierToString( value, identifierMapping, options, appender ); + managedTypeToString( value, entityType, options, appender, ',' ); + appender.append( '}' ); + } else { - throw new UnsupportedOperationException( "Support for attribute mapping type not yet implemented: " + attributeMapping.getClass().getName() ); + // if it was already encountered, only append the identity string + appender.append( '\"' ).append( entityType.getEntityName() ).append( '#' ); + entityIdentifierToString( value, identifierMapping, options, appender ); + appender.append( '\"' ); } + } ); + } + + private static void entityIdentifierToString( + Object value, + EntityIdentifierMapping identifierMapping, + WrapperOptions options, + JsonAppender appender) { + final Object identifier = identifierMapping.getIdentifier( value ); + if ( identifierMapping instanceof SingleAttributeIdentifierMapping singleAttribute ) { + //noinspection unchecked + convertedValueToString( + (JavaType) singleAttribute.getJavaType(), + singleAttribute.getSingleJdbcMapping().getJdbcType(), + identifier, + options, + appender + ); + } + else if ( identifier instanceof CompositeIdentifierMapping composite ) { + toString( identifier, composite.getMappedType(), options, appender ); + } + else { + throw new UnsupportedOperationException( "Unsupported identifier type: " + identifier.getClass().getName() ); + } + } + + private static void pluralAttributeToString( + Object value, + PluralAttributeMapping plural, + WrapperOptions options, + JsonAppender appender) { + if ( handleNullOrLazy( value, appender ) ) { + // nothing left to do + return; + } + + final CollectionPart element = plural.getElementDescriptor(); + final CollectionSemantics collectionSemantics = plural.getMappedType().getCollectionSemantics(); + switch ( collectionSemantics.getCollectionClassification() ) { + case MAP: + case SORTED_MAP: + case ORDERED_MAP: + final PersistentMap pm = (PersistentMap) value; + persistentMapToString( pm, plural.getIndexDescriptor(), element, options, appender ); + break; + default: + final PersistentCollection pc = (PersistentCollection) value; + final Iterator entries = pc.entries( plural.getCollectionDescriptor() ); + char separator = '['; + while ( entries.hasNext() ) { + appender.append( separator ); + collectionPartToString( entries.next(), element, options, appender ); + separator = ','; + } + appender.append( ']' ); + } + } + + private static void persistentMapToString( + PersistentMap map, + CollectionPart key, + CollectionPart value, + WrapperOptions options, + JsonAppender appender) { + char separator = '{'; + for ( final Map.Entry entry : map.entrySet() ) { + appender.append( separator ); + collectionPartToString( entry.getKey(), key, options, appender ); + appender.append( ':' ); + collectionPartToString( entry.getValue(), value, options, appender ); separator = ','; } + appender.append( '}' ); } - private static void toString(MappingType mappedType, Object value, WrapperOptions options, JsonAppender appender) { - if ( value == null ) { - appender.append( "null" ); + private static void collectionPartToString( + Object value, + CollectionPart collectionPart, + WrapperOptions options, + JsonAppender appender) { + if ( collectionPart instanceof BasicValuedCollectionPart basic ) { + // special case for basic values as they use lambdas as mapping type + //noinspection unchecked + convertedValueToString( + (JavaType) basic.getJavaType(), + basic.getJdbcMapping().getJdbcType(), + value, + options, + appender + ); } - else if ( mappedType instanceof EmbeddableMappingType embeddableMappingType ) { - toString( embeddableMappingType, value, options, appender ); + else { + toString( value, collectionPart.getMappedType(), options, appender ); } - else if ( mappedType instanceof BasicType ) { + } + + public static void toString(Object value, MappingType mappedType, WrapperOptions options, JsonAppender appender) { + if ( handleNullOrLazy( value, appender ) ) { + // nothing left to do + return; + } + + if ( mappedType instanceof EntityMappingType entityType ) { + entityToString( value, entityType, options, appender ); + } + else if ( mappedType instanceof ManagedMappingType managedMappingType ) { + managedTypeToString( value, managedMappingType, options, appender, '{' ); + appender.append( '}' ); + } + else if ( mappedType instanceof BasicType type ) { //noinspection unchecked - final BasicType basicType = (BasicType) mappedType; - convertedBasicValueToString( basicType.convertToRelationalValue( value ), options, appender, basicType ); + convertedBasicValueToString( + type.convertToRelationalValue( value ), + options, + appender, + (JavaType) type.getJdbcJavaType(), + type.getJdbcType() + ); } else { - throw new UnsupportedOperationException( "Support for mapping type not yet implemented: " + mappedType.getClass().getName() ); + throw new UnsupportedOperationException( + "Support for mapping type not yet implemented: " + mappedType.getClass().getName() + ); } } + /** + * Checks the provided {@code value} is either null or a lazy property. + * + * @param value the value to check + * @param appender the current {@link JsonAppender} + * + * @return {@code true} if it was, indicating no further processing of the value is needed, {@code false otherwise}. + */ + private static boolean handleNullOrLazy(Object value, JsonAppender appender) { + if ( value == null ) { + appender.append( "null" ); + return true; + } + else if ( appender.expandProperties() ) { + // avoid force-initialization when serializing all properties + if ( value == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + appender.append( '"' ).append( value.toString() ).append( '"' ); + return true; + } + else if ( !isInitialized( value ) ) { + appender.append( '"' ).append( "" ).append( '"' ); + return true; + } + } + return false; + } + private static void convertedValueToString( JavaType javaType, JdbcType jdbcType, @@ -180,29 +379,13 @@ private static void convertedValueToString( appender.append( "null" ); } else if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) { - toString( aggregateJdbcType.getEmbeddableMappingType(), value, options, appender ); + toString( value, aggregateJdbcType.getEmbeddableMappingType(), options, appender ); } else { convertedBasicValueToString( value, options, appender, javaType, jdbcType ); } } - - private static void convertedBasicValueToString( - Object value, - WrapperOptions options, - JsonAppender appender, - BasicType basicType) { - //noinspection unchecked - convertedBasicValueToString( - value, - options, - appender, - (JavaType) basicType.getJdbcJavaType(), - basicType.getJdbcType() - ); - } - private static void convertedBasicValueToString( Object value, WrapperOptions options, @@ -1370,15 +1553,23 @@ String expectedChars() { } } - private static class JsonAppender extends OutputStream implements SqlAppender { + public static class JsonAppender extends OutputStream implements SqlAppender { private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private final StringBuilder sb; + private final boolean expandProperties; + private boolean escape; + private Map> circularityTracker; - public JsonAppender(StringBuilder sb) { + public JsonAppender(StringBuilder sb, boolean expandProperties) { this.sb = sb; + this.expandProperties = expandProperties; + } + + public boolean expandProperties() { + return expandProperties; } @Override @@ -1477,6 +1668,29 @@ public void write(byte[] bytes, int off, int len) { } } + /** + * Tracks the provided {@code entity} instance and invokes the {@code action} with either + * {@code true} if the entity was not already encountered or {@code false} otherwise. + * + * @param entity the entity instance to track + * @param entityType the type of the entity instance + * @param action the action to invoke while tracking the entity + */ + public void trackingEntity(Object entity, EntityMappingType entityType, Consumer action) { + if ( circularityTracker == null ) { + circularityTracker = new HashMap<>(); + } + final IdentitySet entities = circularityTracker.computeIfAbsent( + entityType.getEntityName(), + k -> new IdentitySet<>() + ); + final boolean added = entities.add( entity ); + action.accept( added ); + if ( added ) { + entities.remove( entity ); + } + } + private void appendEscaped(char fragment) { switch ( fragment ) { case 0: @@ -1539,7 +1753,6 @@ private void appendEscaped(char fragment) { break; } } - } private static class CustomArrayList extends AbstractCollection implements Collection { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java index 727695115efb..f8ed6f8ff7ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java @@ -17,6 +17,7 @@ import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; @@ -38,13 +39,13 @@ public static StructAttributeValues getAttributeValues( Object[] rawJdbcValues, WrapperOptions options) throws SQLException { final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); - final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + final int size = numberOfAttributeMappings + (embeddableMappingType.isPolymorphic() ? 1 : 0); final StructAttributeValues attributeValues = new StructAttributeValues( numberOfAttributeMappings, rawJdbcValues ); int jdbcIndex = 0; for ( int i = 0; i < size; i++ ) { jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, i ), + getSubPart( embeddableMappingType, i ), attributeValues, i, rawJdbcValues, @@ -171,7 +172,7 @@ private static int injectJdbcValues( int offset = 0; for ( int i = 0; i < values.length; i++ ) { offset += injectJdbcValue( - getEmbeddedPart( embeddableMappingType, i ), + getSubPart( embeddableMappingType, i ), values, i, jdbcValues, @@ -203,10 +204,12 @@ private static EmbeddableInstantiator embeddableInstantiator( } } - public static ValuedModelPart getEmbeddedPart(EmbeddableMappingType embeddableMappingType, int position) { - return position == embeddableMappingType.getNumberOfAttributeMappings() - ? embeddableMappingType.getDiscriminatorMapping() - : embeddableMappingType.getAttributeMapping( position ); + public static ValuedModelPart getSubPart(ManagedMappingType type, int position) { + if ( position == type.getNumberOfAttributeMappings() ) { + assert type instanceof EmbeddableMappingType; + return ( (EmbeddableMappingType) type ).getDiscriminatorMapping(); + } + return type.getAttributeMapping( position ); } private static int injectJdbcValue( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java index 52fa301e4ac9..10512b59e692 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java @@ -36,7 +36,7 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.dialect.StructHelper.getEmbeddedPart; +import static org.hibernate.dialect.StructHelper.getSubPart; import static org.hibernate.dialect.StructHelper.instantiate; /** @@ -245,7 +245,7 @@ private StructAttributeValues getAttributeValues( for ( int i = 0; i < size; i++ ) { final int attributeIndex = orderMapping == null ? i : orderMapping[i]; jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, attributeIndex ), + getSubPart( embeddableMappingType, attributeIndex ), attributeValues, attributeIndex, rawJdbcValues, @@ -387,7 +387,7 @@ private int wrapRawJdbcValues( WrapperOptions options) throws SQLException { final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); for ( int i = 0; i < numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); i++ ) { - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); + final ValuedModelPart attributeMapping = getSubPart( embeddableMappingType, i ); if ( attributeMapping instanceof ToOneAttributeMapping toOneAttributeMapping ) { if ( toOneAttributeMapping.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { continue; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java index ebd38c250ef7..2052c557eb41 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java @@ -43,7 +43,7 @@ import static java.lang.Character.isLetter; import static java.lang.Character.isLetterOrDigit; -import static org.hibernate.dialect.StructHelper.getEmbeddedPart; +import static org.hibernate.dialect.StructHelper.getSubPart; import static org.hibernate.dialect.StructHelper.instantiate; /** @@ -762,7 +762,7 @@ private static void toString( final int attributeCount = embeddableMappingType.getNumberOfAttributeMappings(); for ( int i = 0; i < attributeCount; i++ ) { final Object attributeValue = attributeValues == null ? null : attributeValues[i]; - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); + final ValuedModelPart attributeMapping = getSubPart( embeddableMappingType, i ); if ( attributeMapping instanceof SelectableMapping selectable ) { final String tagName = selectable.getSelectableName(); sb.append( '<' );