From b6d270a2cca9f373803406613f944beb1977129d Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 21 Jul 2025 12:39:25 +0200 Subject: [PATCH 1/4] HHH-19575 Ensure null struct is fetched as null embeddable --- .../AggregateComponentSecondPass.java | 4 +- .../AbstractPostgreSQLStructJdbcType.java | 68 +++----------- .../java/org/hibernate/dialect/XmlHelper.java | 4 + .../aggregate/DB2AggregateSupport.java | 11 ++- .../internal/AnyDiscriminatorPart.java | 18 +++- .../mapping/internal/AnyKeyPart.java | 14 +++ .../DiscriminatedAssociationMapping.java | 10 ++- .../internal/MappingModelCreationHelper.java | 15 ++++ .../internal/SelectableMappingImpl.java | 5 +- .../internal/SelectableMappingsImpl.java | 59 +++++-------- .../AggregateEmbeddableInitializerImpl.java | 13 ++- .../EmbeddableExpressionResultImpl.java | 2 +- .../internal/EmbeddableFetchImpl.java | 23 ++++- .../EmbeddableForeignKeyResultImpl.java | 2 +- .../internal/EmbeddableInitializerImpl.java | 14 ++- .../internal/EmbeddableResultImpl.java | 23 ++++- .../StructEmbeddableArrayEmptyTest.java | 88 +++++++++++++++++++ 17 files changed, 263 insertions(+), 110 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java index 8872cfb80a51..d57fa9b82273 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java @@ -15,6 +15,7 @@ import org.hibernate.MappingException; import org.hibernate.annotations.Comment; import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.boot.model.relational.QualifiedName; @@ -109,7 +110,8 @@ public void doSecondPass(Map persistentClasses) throws orderColumns( registeredUdt, originalOrder ); } else { - addAuxiliaryObjects = false; + addAuxiliaryObjects = + isAggregateArray() && namespace.locateUserDefinedArrayType( Identifier.toIdentifier( aggregateColumn.getSqlType() ) ) == null; validateEqual( registeredUdt, udt ); } } 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 ee6dfa3d06cb..e2987a117b2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java @@ -514,7 +514,6 @@ else if ( string.charAt( i + 1 ) == '{' ) { quotes + 1, arrayList, (BasicType) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( subEnd - 1 ) == '}'; @@ -624,7 +623,6 @@ else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() quotes + 1, arrayList, (BasicType) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( i - 1 ) == '}'; @@ -667,7 +665,6 @@ private int deserializeArray( int quotes, ArrayList values, BasicType elementType, - boolean returnEmbeddable, WrapperOptions options) throws SQLException { boolean inQuote = false; StringBuilder escapingSb = null; @@ -860,30 +857,17 @@ private int deserializeArray( i + 1, quotes + 1, subValues, - returnEmbeddable, + true, options ); - if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( - structJdbcType.embeddableMappingType, - structJdbcType.orderMapping, - subValues, - options - ); - final Object subValue = instantiate( structJdbcType.embeddableMappingType, attributeValues, options.getSessionFactory() ); + final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( + structJdbcType.embeddableMappingType, + structJdbcType.orderMapping, + subValues, + options + ); + final Object subValue = instantiate( structJdbcType.embeddableMappingType, attributeValues, options.getSessionFactory() ); values.add( subValue ); - } - else { - if ( structJdbcType.inverseOrderMapping != null ) { - StructHelper.orderJdbcValues( - structJdbcType.embeddableMappingType, - structJdbcType.inverseOrderMapping, - subValues.clone(), - subValues - ); - } - values.add( subValues ); - } // The subEnd points to the first character after the '}', // so move forward the index to point to the next char after quotes assert isDoubleQuote( string, subEnd, expectedQuotes ); @@ -1001,40 +985,8 @@ else if ( elementType.getJavaTypeDescriptor().getJavaTypeClass().isEnum() } private SelectableMapping getJdbcValueSelectable(int jdbcValueSelectableIndex) { - if ( orderMapping != null ) { - final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); - 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 MappingType mappedType = modelPart.getMappedType(); - if ( mappedType instanceof EmbeddableMappingType ) { - final EmbeddableMappingType embeddableMappingType = (EmbeddableMappingType) mappedType; - final SelectableMapping aggregateMapping = embeddableMappingType.getAggregateMapping(); - if ( aggregateMapping == null ) { - final SelectableMapping subSelectable = embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex - count ); - if ( subSelectable != null ) { - return subSelectable; - } - count += embeddableMappingType.getJdbcValueCount(); - } - else { - if ( count == jdbcValueSelectableIndex ) { - return aggregateMapping; - } - count++; - } - } - else { - if ( count == jdbcValueSelectableIndex ) { - return (SelectableMapping) modelPart; - } - count += modelPart.getJdbcTypeCount(); - } - } - return null; - } - return embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex ); + return embeddableMappingType.getJdbcValueSelectable( + orderMapping != null ? orderMapping[jdbcValueSelectableIndex] : jdbcValueSelectableIndex ); } private static boolean repeatsChar(String string, int start, int times, char expectedChar) { 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 f363feeb2fe8..5a913a6c1391 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java @@ -521,6 +521,10 @@ else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { final String tagName = aggregateMapping.getSelectableName(); sb.append( '<' ); sb.append( tagName ); + if ( array[i] == null ) { + sb.append( "/>" ); + continue; + } sb.append( '>' ); toString( mappingType, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java index 81cb418de6bf..3e463852c6fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java @@ -333,9 +333,9 @@ public List aggregateAuxiliaryDatabaseObjects( var serializerSb = new StringBuilder(); var deserializerSb = new StringBuilder(); serializerSb.append( "create function " ).append( columnType ).append( "_serializer(v " ).append( columnType ).append( ") returns xml language sql " ) - .append( "return xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" ); + .append( "return case when v is null then null else xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" ); appendSerializer( aggregatedColumns, serializerSb, "v.." ); - serializerSb.append( ')' ); + serializerSb.append( ") end" ); deserializerSb.append( "create function " ).append( columnType ).append( "_deserializer(v xml) returns " ).append( columnType ).append( " language sql " ) .append( "return select " ).append( columnType ).append( "()" ); @@ -385,6 +385,10 @@ private static void appendSerializer(List aggregatedColumns, StringBuild } for ( Column udtColumn : aggregatedColumns ) { serializerSb.append( sep ); + if ( udtColumn.getSqlTypeCode() == STRUCT ) { + serializerSb.append( "case when ").append( prefix ).append( udtColumn.getName() ) + .append( " is null then null else " ); + } serializerSb.append( "xmlelement(name \"" ).append( udtColumn.getName() ).append( "\"" ); if ( udtColumn.getSqlTypeCode() == STRUCT ) { final AggregateColumn aggregateColumn = (AggregateColumn) udtColumn; @@ -402,6 +406,9 @@ else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) { serializerSb.append( ',' ).append( prefix ).append( udtColumn.getName() ); } serializerSb.append( ')' ); + if ( udtColumn.getSqlTypeCode() == STRUCT ) { + serializerSb.append( " end" ); + } sep = ','; } if ( aggregatedColumns.size() > 1 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java index 85f3ea113cc6..21f6badfd8b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java @@ -25,6 +25,7 @@ import org.hibernate.metamodel.mapping.MappedDiscriminatorConverter; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.spi.NavigablePath; @@ -58,6 +59,7 @@ public class AnyDiscriminatorPart implements DiscriminatorMapping, FetchOptions private final String table; private final String column; + private final SelectablePath selectablePath; private final String customReadExpression; private final String customWriteExpression; private final String columnDefinition; @@ -76,7 +78,10 @@ public AnyDiscriminatorPart( NavigableRole partRole, DiscriminatedAssociationModelPart declaringType, String table, - String column, String customReadExpression, String customWriteExpression, + String column, + SelectablePath selectablePath, + String customReadExpression, + String customWriteExpression, String columnDefinition, Long length, Integer precision, @@ -91,6 +96,7 @@ public AnyDiscriminatorPart( this.declaringType = declaringType; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; this.columnDefinition = columnDefinition; @@ -136,6 +142,16 @@ public String getSelectionExpression() { return column; } + @Override + public String getSelectableName() { + return selectablePath.getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return selectablePath; + } + @Override public boolean isFormula() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java index 4c20f90e6008..191b23e07c3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java @@ -19,6 +19,7 @@ import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; @@ -47,6 +48,7 @@ public class AnyKeyPart implements BasicValuedModelPart, FetchOptions { private final NavigableRole navigableRole; private final String table; private final String column; + private final SelectablePath selectablePath; private final DiscriminatedAssociationModelPart anyPart; private final String customReadExpression; private final String customWriteExpression; @@ -65,6 +67,7 @@ public AnyKeyPart( DiscriminatedAssociationModelPart anyPart, String table, String column, + SelectablePath selectablePath, String customReadExpression, String customWriteExpression, String columnDefinition, @@ -79,6 +82,7 @@ public AnyKeyPart( this.navigableRole = navigableRole; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.anyPart = anyPart; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; @@ -103,6 +107,16 @@ public String getSelectionExpression() { return column; } + @Override + public String getSelectableName() { + return selectablePath.getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return selectablePath; + } + @Override public boolean isFormula() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java index 43dbd65b91ca..a45a15c90355 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java @@ -25,6 +25,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -73,13 +74,18 @@ public static DiscriminatedAssociationMapping from( assert !keySelectable.isFormula(); final Column metaColumn = (Column) metaSelectable; final Column keyColumn = (Column) keySelectable; + final SelectablePath parentSelectablePath = declaringModelPart.asAttributeMapping() != null + ? MappingModelCreationHelper.getSelectablePath( declaringModelPart.asAttributeMapping().getDeclaringType() ) + : null; final MetaType metaType = (MetaType) anyType.getDiscriminatorType(); final AnyDiscriminatorPart discriminatorPart = new AnyDiscriminatorPart( - containerRole.append( AnyDiscriminatorPart.ROLE_NAME), + containerRole.append( AnyDiscriminatorPart.ROLE_NAME ), declaringModelPart, tableName, metaColumn.getText( dialect ), + parentSelectablePath != null ? parentSelectablePath.append( metaColumn.getQuotedName( dialect ) ) + : new SelectablePath( metaColumn.getQuotedName( dialect ) ), metaColumn.getCustomReadExpression(), metaColumn.getCustomWriteExpression(), metaColumn.getSqlType(), @@ -101,6 +107,8 @@ public static DiscriminatedAssociationMapping from( declaringModelPart, tableName, keyColumn.getText( dialect ), + parentSelectablePath != null ? parentSelectablePath.append( keyColumn.getQuotedName( dialect ) ) + : new SelectablePath( keyColumn.getQuotedName( dialect ) ), keyColumn.getCustomReadExpression(), keyColumn.getCustomWriteExpression(), keyColumn.getSqlType(), diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 0d942799bb9b..f95d25ece29c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -970,6 +970,7 @@ else if ( modelPart == null ) { ( (PropertyBasedMapping) simpleFkTarget ).getPropertyAccess() ); } + final SelectablePath parentSelectablePath = getSelectablePath( attributeMapping.getDeclaringType() ); final SelectableMapping keySelectableMapping; int i = 0; final Value value = bootProperty.getValue(); @@ -977,6 +978,7 @@ else if ( modelPart == null ) { keySelectableMapping = SelectableMappingImpl.from( tableExpression, columnIterator.next(), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( i ), @@ -993,6 +995,7 @@ else if ( modelPart == null ) { keySelectableMapping = SelectableMappingImpl.from( tableExpression, table.getPrimaryKey().getColumn( 0 ), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( 0 ), @@ -1129,6 +1132,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( boolean[] updateable, Dialect dialect, MappingModelCreationProcess creationProcess) { + final SelectablePath parentSelectablePath = getSelectablePath( keyDeclaringType ); final boolean hasConstraint; final SelectableMappings keySelectableMappings; if ( bootValueMapping instanceof Collection ) { @@ -1142,6 +1146,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, collectionBootValueMapping.getKey(), getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1167,6 +1172,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, bootValueMapping, getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1214,6 +1220,15 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( } } + public static @Nullable SelectablePath getSelectablePath(ManagedMappingType type) { + if ( type instanceof EmbeddableMappingType ) { + final EmbeddableMappingType embeddableType = (EmbeddableMappingType) type; + return embeddableType.getAggregateMapping() != null + ? embeddableType.getAggregateMapping().getSelectablePath() : null; + } + return null; + } + public static int[] getPropertyOrder(Value bootValueMapping, MappingModelCreationProcess creationProcess) { final ComponentType componentType; final boolean sorted; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java index f39e8ab78e30..fe2de5416e06 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java @@ -8,6 +8,7 @@ import java.util.Locale; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.Dialect; import org.hibernate.mapping.Column; import org.hibernate.mapping.Selectable; @@ -126,7 +127,7 @@ public static SelectableMapping from( public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, - final SelectablePath parentPath, + @Nullable final SelectablePath parentPath, final JdbcMapping jdbcMapping, final TypeConfiguration typeConfiguration, boolean insertable, @@ -154,7 +155,7 @@ public static SelectableMapping from( public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, - final SelectablePath parentPath, + @Nullable final SelectablePath parentPath, final JdbcMapping jdbcMapping, final TypeConfiguration typeConfiguration, boolean insertable, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java index 7ff282dd7851..227ca356de9e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.Mapping; import org.hibernate.internal.util.collections.CollectionHelper; @@ -19,6 +20,7 @@ import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectableMappings; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.type.CompositeType; @@ -67,48 +69,30 @@ public static SelectableMappings from( Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { - if ( insertable.length == 0 ) { - return from( - containingTableExpression, - value, - propertyOrder, - mapping, - typeConfiguration, - dialect, - sqmFunctionRegistry, - creationContext - ); - } - final List jdbcMappings = new ArrayList<>(); - resolveJdbcMappings( jdbcMappings, mapping, value.getType() ); - - final List selectables = value.getVirtualSelectables(); - - final SelectableMapping[] selectableMappings = new SelectableMapping[jdbcMappings.size()]; - for ( int i = 0; i < selectables.size(); i++ ) { - selectableMappings[propertyOrder[i]] = SelectableMappingImpl.from( - containingTableExpression, - selectables.get( i ), - jdbcMappings.get( propertyOrder[i] ), - typeConfiguration, - insertable[i], - updateable[i], - false, - dialect, - sqmFunctionRegistry, - creationContext - ); - } - - return new SelectableMappingsImpl( selectableMappings ); + return from( + containingTableExpression, + value, + propertyOrder, + null, + mapping, + typeConfiguration, + insertable, + updateable, + dialect, + sqmFunctionRegistry, + creationContext + ); } - private static SelectableMappings from( + public static SelectableMappings from( String containingTableExpression, Value value, int[] propertyOrder, + @Nullable SelectablePath parentSelectablePath, Mapping mapping, TypeConfiguration typeConfiguration, + boolean[] insertable, + boolean[] updateable, Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { @@ -122,10 +106,11 @@ private static SelectableMappings from( selectableMappings[propertyOrder[i]] = SelectableMappingImpl.from( containingTableExpression, selectables.get( i ), + parentSelectablePath, jdbcMappings.get( propertyOrder[i] ), typeConfiguration, - false, - false, + i < insertable.length && insertable[i], + i < updateable.length && updateable[i], false, dialect, sqmFunctionRegistry, diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java index 194e9bc539c9..a9e45cdd5935 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java @@ -28,7 +28,7 @@ public AggregateEmbeddableInitializerImpl( InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { - super( resultDescriptor, discriminatorFetch, parent, creationState, isResultInitializer ); + super( resultDescriptor, discriminatorFetch, null, parent, creationState, isResultInitializer ); this.aggregateValuesArrayPositions = resultDescriptor.getAggregateValuesArrayPositions(); } @@ -37,6 +37,17 @@ public void startLoading(RowProcessingState rowProcessingState) { super.startLoading( NestedRowProcessingState.wrap( this, rowProcessingState ) ); } + @Override + protected void extractRowState(EmbeddableInitializerData data) { + super.extractRowState( data ); + if ( data.getState() == State.MISSING + && !isPartOfKey() + && getJdbcValues( data.getRowProcessingState().unwrap() ) != null ) { + // When all values are null, the embeddable shall be non-null if the JDBC object is not null + data.setState( State.RESOLVED ); + } + } + public int[] getAggregateValuesArrayPositions() { return aggregateValuesArrayPositions; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java index 41e19ecd85c7..e6fa8e1e5208 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java @@ -141,6 +141,6 @@ public Initializer createInitializer( @Override public Initializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, null, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, null, null, parent, creationState, true ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java index fb358a198210..66de74ad307f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.graph.embeddable.internal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.engine.FetchTiming; import org.hibernate.graph.spi.GraphHelper; import org.hibernate.graph.spi.GraphImplementor; @@ -14,11 +15,15 @@ import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupProducer; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.results.graph.AbstractFetchParent; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -31,6 +36,7 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; import org.hibernate.sql.results.graph.embeddable.EmbeddableResultGraphNode; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; +import org.hibernate.type.BasicType; import static org.hibernate.internal.util.NullnessUtil.castNonNull; @@ -46,6 +52,7 @@ public class EmbeddableFetchImpl extends AbstractFetchParent private final boolean hasTableGroup; private final EmbeddableMappingType fetchContainer; private final BasicFetch discriminatorFetch; + private final @Nullable DomainResult nullIndicatorResult; public EmbeddableFetchImpl( NavigablePath navigablePath, @@ -83,6 +90,19 @@ public EmbeddableFetchImpl( ); this.discriminatorFetch = creationState.visitEmbeddableDiscriminatorFetch( this, false ); + if ( fetchContainer.getAggregateMapping() != null ) { + final TableReference tableReference = tableGroup.resolveTableReference( + fetchContainer.getAggregateMapping().getContainingTableExpression() ); + final Expression aggregateExpression = creationState.getSqlAstCreationState().getSqlExpressionResolver() + .resolveSqlExpression( tableReference, fetchContainer.getAggregateMapping() ); + final BasicType booleanType = creationState.getSqlAstCreationState().getCreationContext() + .getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); } @@ -98,6 +118,7 @@ protected EmbeddableFetchImpl(EmbeddableFetchImpl original) { tableGroup = original.tableGroup; hasTableGroup = original.hasTableGroup; discriminatorFetch = original.discriminatorFetch; + nullIndicatorResult = original.nullIndicatorResult; } @Override @@ -171,7 +192,7 @@ public Initializer createInitializer( @Override public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, discriminatorFetch, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, discriminatorFetch, nullIndicatorResult, parent, creationState, true ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java index 50f43bc193e9..fa6a54d4f257 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java @@ -126,7 +126,7 @@ public Initializer createInitializer( public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { return getReferencedModePart() instanceof NonAggregatedIdentifierMapping ? new NonAggregatedIdentifierMappingInitializer( this, null, creationState, true ) - : new EmbeddableInitializerImpl( this, null, null, creationState, true ); + : new EmbeddableInitializerImpl( this, null, null, null, creationState, true ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java index 762af601e1a1..00d33e10821c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java @@ -25,6 +25,7 @@ import org.hibernate.proxy.LazyInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; @@ -63,6 +64,7 @@ public class EmbeddableInitializerImpl extends AbstractInitializer[][] assemblers; protected final BasicResultAssembler discriminatorAssembler; + protected final @Nullable DomainResultAssembler nullIndicatorAssembler; protected final @Nullable Initializer[][] subInitializers; protected final @Nullable Initializer[][] subInitializersForResolveFromInitialized; protected final @Nullable Initializer[][] collectionContainingSubInitializers; @@ -104,6 +106,7 @@ public int getSubclassId() { public EmbeddableInitializerImpl( EmbeddableResultGraphNode resultDescriptor, BasicFetch discriminatorFetch, + @Nullable DomainResult nullIndicatorResult, InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { @@ -191,6 +194,8 @@ public EmbeddableInitializerImpl( this.discriminatorAssembler = discriminatorFetch != null ? (BasicResultAssembler) discriminatorFetch.createAssembler( this, creationState ) : null; + this.nullIndicatorAssembler = + nullIndicatorResult == null ? null : nullIndicatorResult.createResultAssembler( this, creationState ); this.subInitializers = subInitializers; this.subInitializersForResolveFromInitialized = isEnhancedForLazyLoading( embeddableMappingType ) ? subInitializers @@ -476,7 +481,7 @@ private void prepareCompositeInstance(EmbeddableInitializerData data) { } } - private void extractRowState(EmbeddableInitializerData data) { + protected void extractRowState(EmbeddableInitializerData data) { boolean stateAllNull = true; final DomainResultAssembler[] subAssemblers = assemblers[data.getSubclassId()]; final RowProcessingState rowProcessingState = data.getRowProcessingState(); @@ -501,10 +506,15 @@ else if ( isPartOfKey ) { } } if ( stateAllNull ) { - data.setState( State.MISSING ); + data.setState( isNull( data ) ? State.MISSING : State.RESOLVED ); } } + protected boolean isNull(EmbeddableInitializerData data) { + return nullIndicatorAssembler == null + || Boolean.TRUE == nullIndicatorAssembler.assemble( data.getRowProcessingState() ); + } + @Override public void resolveState(EmbeddableInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java index ecc4918b0c02..f694cc98addb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java @@ -6,14 +6,18 @@ */ package org.hibernate.sql.results.graph.embeddable.internal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.internal.util.NullnessUtil; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.results.graph.AbstractFetchParent; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResult; @@ -27,6 +31,7 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableResult; import org.hibernate.sql.results.graph.embeddable.EmbeddableResultGraphNode; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; /** @@ -40,6 +45,7 @@ public class EmbeddableResultImpl extends AbstractFetchParent implements Embe private final boolean containsAnyNonScalars; private final EmbeddableMappingType fetchContainer; private final BasicFetch discriminatorFetch; + private final @Nullable DomainResult nullIndicatorResult; public EmbeddableResultImpl( NavigablePath navigablePath, @@ -57,7 +63,7 @@ public EmbeddableResultImpl( final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - fromClauseAccess.resolveTableGroup( + final TableGroup embeddableTableGroup = fromClauseAccess.resolveTableGroup( getNavigablePath(), np -> { final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping(); @@ -78,6 +84,19 @@ public EmbeddableResultImpl( ); this.discriminatorFetch = creationState.visitEmbeddableDiscriminatorFetch( this, false ); + if ( fetchContainer.getAggregateMapping() != null ) { + final TableReference tableReference = embeddableTableGroup.resolveTableReference( + fetchContainer.getAggregateMapping().getContainingTableExpression() ); + final Expression aggregateExpression = creationState.getSqlAstCreationState().getSqlExpressionResolver() + .resolveSqlExpression( tableReference, fetchContainer.getAggregateMapping() ); + final BasicType booleanType = creationState.getSqlAstCreationState().getCreationContext() + .getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); @@ -143,6 +162,6 @@ public Initializer createInitializer( @Override public Initializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, discriminatorFetch, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, discriminatorFetch, nullIndicatorResult, parent, creationState, true ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java new file mode 100644 index 000000000000..90ee1468f64c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.mapping.embeddable; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Struct; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@BootstrapServiceRegistry( + // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete + integrators = SharedDriverManagerTypeCacheClearingIntegrator.class +) +@ServiceRegistry( + settingProviders = @SettingProvider( + settingName = AvailableSettings.PREFERRED_ARRAY_JDBC_TYPE, + provider = OracleNestedTableSettingProvider.class + ) +) +@DomainModel(annotatedClasses = StructEmbeddableArrayEmptyTest.StructHolder.class) +@SessionFactory +@RequiresDialect( PostgreSQLDialect.class ) +@RequiresDialect( OracleDialect.class ) +public class StructEmbeddableArrayEmptyTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var entity = new StructHolder(); + entity.id = 1L; + entity.myStruct = new MyStruct(); + entity.myStructs = new MyStruct[] { new MyStruct() }; + session.persist( entity ); + } + ); + } + + @AfterEach + protected void cleanupTest(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete StructHolder" ).executeUpdate(); + } ); + } + + @Test + void testEmptyStructBehavesDifferently(SessionFactoryScope scope) { + var loadedEntity = scope.fromTransaction(session -> session.find(StructHolder.class, 1)); + assertThat(loadedEntity.myStruct).isNotNull(); + assertThat(loadedEntity.myStructs).usingRecursiveComparison() + .isEqualTo(new MyStruct[] { new MyStruct() }) + .isNotEqualTo(new MyStruct[] { null }); + } + + @Entity(name = "StructHolder") + public static class StructHolder { + @Id + Long id; + MyStruct myStruct; + MyStruct[] myStructs; + } + + @Embeddable + @Struct(name = "MyStruct") + public static class MyStruct { + String field; + } +} From d0231886ccb72dc9d2c98756491570e5cbfc06fe Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Wed, 23 Jul 2025 17:06:42 +0200 Subject: [PATCH 2/4] HHH-19639 Mark methods that are used by Hibernate Reactive --- .../results/graph/embeddable/internal/EmbeddableFetchImpl.java | 1 + .../embeddable/internal/EmbeddableForeignKeyResultImpl.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java index 66de74ad307f..2625acd60e7c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java @@ -206,6 +206,7 @@ public FetchParent asFetchParent() { return this; } + // Used by Hibernate Reactive protected BasicFetch getDiscriminatorFetch() { return discriminatorFetch; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java index fa6a54d4f257..958c14dd4488 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java @@ -54,6 +54,9 @@ public EmbeddableForeignKeyResultImpl( resetFetches( creationState.visitFetches( this ) ); } + /* + * Used by Hibernate Reactive + */ protected EmbeddableForeignKeyResultImpl(EmbeddableForeignKeyResultImpl original) { super( original ); this.resultVariable = original.resultVariable; From ec2527faeff85c37b2ed2680c2b107fefc5fefb1 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Wed, 23 Jul 2025 17:07:14 +0200 Subject: [PATCH 3/4] HHH-19639 Add EmbeddableFetchImpl#getNullIndicatorResult In Hibernate Reactive, `ReactiveEmbeddableFetchImpl#createInitializers` needs to access the value to override the initializer created by ORM. --- .../graph/embeddable/internal/EmbeddableFetchImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java index 2625acd60e7c..1b7c35034099 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java @@ -210,4 +210,9 @@ public FetchParent asFetchParent() { protected BasicFetch getDiscriminatorFetch() { return discriminatorFetch; } + + // Used by Hibernate Reactive + protected @Nullable DomainResult getNullIndicatorResult() { + return nullIndicatorResult; + } } From 5c091eb936acaebba683d840e8bc3788efb44e5b Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 24 Jul 2025 11:31:44 +0200 Subject: [PATCH 4/4] HHH-19575 Backwards compatibility for Hibernate Reactive --- .../internal/EmbeddableInitializerImpl.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java index 00d33e10821c..0c2507fa27aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java @@ -103,6 +103,17 @@ public int getSubclassId() { } } + // Used by Hibernate Reactive + @Deprecated(forRemoval = true) + public EmbeddableInitializerImpl( + EmbeddableResultGraphNode resultDescriptor, + BasicFetch discriminatorFetch, + InitializerParent parent, + AssemblerCreationState creationState, + boolean isResultInitializer) { + this( resultDescriptor, discriminatorFetch, null, parent, creationState, isResultInitializer ); + } + public EmbeddableInitializerImpl( EmbeddableResultGraphNode resultDescriptor, BasicFetch discriminatorFetch,