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 aa7baf3518fd..4b65007cef35 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 @@ -12,6 +12,7 @@ import org.hibernate.AnnotationException; import org.hibernate.MappingException; import org.hibernate.annotations.Comment; +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; @@ -111,7 +112,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/aggregate/DB2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java index f9932d3e9102..348b852e6711 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 @@ -581,9 +581,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..", legacyXmlFormatEnabled ); - 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( "()" ); @@ -633,6 +633,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; @@ -664,6 +668,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/dialect/type/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java index fb4c338514a6..5a28ee9ef16d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java @@ -510,7 +510,6 @@ else if ( string.charAt( i + 1 ) == '{' ) { quotes + 1, arrayList, (BasicType) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( subEnd - 1 ) == '}'; @@ -620,7 +619,6 @@ else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() quotes + 1, arrayList, (BasicType) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( i - 1 ) == '}'; @@ -663,7 +661,6 @@ private int deserializeArray( int quotes, ArrayList values, BasicType elementType, - boolean returnEmbeddable, WrapperOptions options) throws SQLException { boolean inQuote = false; StringBuilder escapingSb = null; @@ -854,29 +851,16 @@ private int deserializeArray( i + 1, quotes + 1, subValues, - returnEmbeddable, + true, options ); - if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( - structJdbcType.embeddableMappingType, - structJdbcType.orderMapping, - subValues, - options - ); - values.add( instantiate( structJdbcType.embeddableMappingType, attributeValues ) ); - } - else { - if ( structJdbcType.inverseOrderMapping != null ) { - StructHelper.orderJdbcValues( - structJdbcType.embeddableMappingType, - structJdbcType.inverseOrderMapping, - subValues.clone(), - subValues - ); - } - values.add( subValues ); - } + final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( + structJdbcType.embeddableMappingType, + structJdbcType.orderMapping, + subValues, + options + ); + values.add( instantiate( structJdbcType.embeddableMappingType, attributeValues ) ); // 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 ); @@ -994,38 +978,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 = getSubPart( embeddableMappingType, orderMapping[i] ); - if ( modelPart.getMappedType() instanceof EmbeddableMappingType embeddableMappingType ) { - 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/metamodel/mapping/internal/AnyDiscriminatorPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java index 6e829ea238f6..25a8c03367dc 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 @@ -17,6 +17,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.metamodel.spi.ImplicitDiscriminatorStrategy; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; @@ -54,6 +55,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; @@ -72,7 +74,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, @@ -88,6 +93,7 @@ public AnyDiscriminatorPart( this.declaringType = declaringType; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; this.columnDefinition = columnDefinition; @@ -142,6 +148,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 bd912c6a7dca..3e5e07857ed7 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 @@ -15,6 +15,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; +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; @@ -43,6 +44,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; @@ -61,6 +63,7 @@ public AnyKeyPart( DiscriminatedAssociationModelPart anyPart, String table, String column, + SelectablePath selectablePath, String customReadExpression, String customWriteExpression, String columnDefinition, @@ -75,6 +78,7 @@ public AnyKeyPart( this.navigableRole = navigableRole; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.anyPart = anyPart; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; @@ -99,6 +103,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 59f2d16585b6..5265b7eda6ca 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 @@ -23,6 +23,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; @@ -71,13 +72,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(), @@ -100,6 +106,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 579e78181ab6..26fdec0d2dae 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 @@ -950,6 +950,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa ( (PropertyBasedMapping) simpleFkTarget ).getPropertyAccess() ); } + final SelectablePath parentSelectablePath = getSelectablePath( attributeMapping.getDeclaringType() ); final SelectableMapping keySelectableMapping; int i = 0; final Value value = bootProperty.getValue(); @@ -957,6 +958,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa keySelectableMapping = SelectableMappingImpl.from( tableExpression, columnIterator.next(), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( i ), @@ -973,6 +975,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa keySelectableMapping = SelectableMappingImpl.from( tableExpression, table.getPrimaryKey().getColumn( 0 ), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( 0 ), @@ -1104,6 +1107,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 collectionBootValueMapping ) { @@ -1115,6 +1119,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, collectionBootValueMapping.getKey(), getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1139,6 +1144,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, bootValueMapping, getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1186,6 +1192,11 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( } } + public static @Nullable SelectablePath getSelectablePath(ManagedMappingType type) { + return type instanceof EmbeddableMappingType embeddableType && embeddableType.getAggregateMapping() != null + ? embeddableType.getAggregateMapping().getSelectablePath() : null; + } + public static int[] getPropertyOrder(Value bootValueMapping, MappingModelCreationProcess creationProcess) { final RuntimeModelCreationContext creationContext = creationProcess.getCreationContext(); final ComponentType componentType; 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 a9c236394eba..fb43a87ec780 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 @@ -6,6 +6,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; @@ -124,7 +125,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, @@ -152,7 +153,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 8330c8475d12..b6bdfb07955e 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 @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Selectable; @@ -16,6 +17,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; @@ -62,53 +64,35 @@ public static SelectableMappings from( Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { - if ( insertable.length == 0 ) { - return from( - containingTableExpression, - value, - propertyOrder, - mappingContext, - typeConfiguration, - dialect, - sqmFunctionRegistry, - creationContext - ); - } - final List jdbcMappings = new ArrayList<>(); - resolveJdbcMappings( jdbcMappings, mappingContext, 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, + mappingContext, + typeConfiguration, + insertable, + updateable, + dialect, + sqmFunctionRegistry, + creationContext + ); } - private static SelectableMappings from( + public static SelectableMappings from( String containingTableExpression, Value value, int[] propertyOrder, - MappingContext mapping, + @Nullable SelectablePath parentSelectablePath, + MappingContext mappingContext, TypeConfiguration typeConfiguration, + boolean[] insertable, + boolean[] updateable, Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { final List jdbcMappings = new ArrayList<>(); - resolveJdbcMappings( jdbcMappings, mapping, value.getType() ); + resolveJdbcMappings( jdbcMappings, mappingContext, value.getType() ); final List selectables = value.getVirtualSelectables(); @@ -117,10 +101,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 9dd34979d364..6681381a9304 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 @@ -26,7 +26,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(); } @@ -35,6 +35,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 d9b9a57f8eea..962bda928a9b 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 @@ -137,6 +137,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 c64de889622b..ff0eadddcd31 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 @@ -4,6 +4,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; @@ -12,11 +13,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; @@ -29,6 +34,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; @@ -44,6 +50,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, @@ -81,6 +88,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() + .getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); } @@ -96,6 +116,7 @@ protected EmbeddableFetchImpl(EmbeddableFetchImpl original) { tableGroup = original.tableGroup; hasTableGroup = original.hasTableGroup; discriminatorFetch = original.discriminatorFetch; + nullIndicatorResult = original.nullIndicatorResult; } @Override @@ -169,7 +190,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 3253da97e54f..bd529327b365 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 @@ -123,7 +123,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 d76e9ac85f27..992884e754b2 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 @@ -22,6 +22,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; @@ -58,6 +59,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; @@ -99,6 +101,7 @@ public int getSubclassId() { public EmbeddableInitializerImpl( EmbeddableResultGraphNode resultDescriptor, BasicFetch discriminatorFetch, + @Nullable DomainResult nullIndicatorResult, InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { @@ -184,6 +187,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 @@ -471,7 +476,7 @@ private void prepareCompositeInstance(EmbeddableInitializerData data) { // EMBEDDED_LOAD_LOGGER.tracef( "Created composite instance [%s]", navigablePath ); } - private void extractRowState(EmbeddableInitializerData data) { + protected void extractRowState(EmbeddableInitializerData data) { boolean stateAllNull = true; final DomainResultAssembler[] subAssemblers = assemblers[data.getSubclassId()]; final RowProcessingState rowProcessingState = data.getRowProcessingState(); @@ -496,10 +501,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 276b273c6e6a..a5eb83f4333d 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 @@ -4,14 +4,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; @@ -25,6 +29,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; /** @@ -38,6 +43,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, @@ -55,7 +61,7 @@ public EmbeddableResultImpl( final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - fromClauseAccess.resolveTableGroup( + final TableGroup embeddableTableGroup = fromClauseAccess.resolveTableGroup( getNavigablePath(), np -> { final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping(); @@ -76,6 +82,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() + .getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); @@ -141,6 +160,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/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java index 0ab60c3579a3..1e0abde4045c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java @@ -788,6 +788,10 @@ else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { if ( tagName != null ) { sb.append( '<' ); sb.append( tagName ); + if ( attributeValue == null ) { + sb.append( "/>" ); + continue; + } sb.append( '>' ); } toString( @@ -920,21 +924,22 @@ private static void convertedBasicValueToString( final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); for ( int i = 0; i < length; i++ ) { final Object arrayElement = Array.get( value, i ); - final Object[] arrayElementValues = arrayElement == null - ? null - : embeddableMappingType.getValues( arrayElement ); - appender.append( START_TAG ); - toString( embeddableMappingType, arrayElementValues, options, appender ); - appender.append( END_TAG ); + if ( arrayElement == null ) { + appender.append( NULL_TAG ); + } + else { + final Object[] arrayElementValues = embeddableMappingType.getValues( arrayElement ); + appender.append( START_TAG ); + toString( embeddableMappingType, arrayElementValues, options, appender ); + appender.append( END_TAG ); + } } } else { for ( int i = 0; i < length; i++ ) { final Object arrayElement = Array.get( value, i ); if ( arrayElement == null ) { - appender.append( '<' ); - appender.append( ROOT_TAG ); - appender.append( "/>" ); + appender.append( NULL_TAG ); } else { appender.append( START_TAG ); 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..1a14be3a1d8f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.getSessionFactory().getSchemaManager().truncate(); + } + + @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; + } +}