From c9c3078a4e941a4a1facb8422c230f3f89d68eee Mon Sep 17 00:00:00 2001 From: Rob Green Date: Wed, 8 Oct 2025 08:05:29 -0400 Subject: [PATCH] HHH-19056 prevent NPE when using @mapsid on an embeddable --- .../boot/model/internal/ToOneBinder.java | 8 + ...ompositeNestedGeneratedValueGenerator.java | 2 +- .../java/org/hibernate/mapping/Component.java | 26 ++- .../java/org/hibernate/mapping/ToOne.java | 9 + .../internal/ToOneAttributeMapping.java | 8 + .../mapsid/MapsEmbeddedIdNullTest.java | 182 ++++++++++++++++++ 6 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/annotations/mapsid/MapsEmbeddedIdNullTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java index 697442fff068..56a01026b9dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java @@ -335,6 +335,7 @@ static void defineFetchingStrategy( handleLazy( toOne, property ); handleFetch( toOne, property ); handleFetchProfileOverrides( toOne, property, propertyHolder, inferredData ); + handleMapsId( toOne, property ); } private static void handleLazy(ToOne toOne, MemberDetails property) { @@ -373,6 +374,13 @@ private static void handleFetch(ToOne toOne, MemberDetails property) { } } + private static void handleMapsId(ToOne toOne, MemberDetails property) { + final MapsId mapsIdAnnotation = property.getDirectAnnotationUsage( MapsId.class ); + if ( mapsIdAnnotation != null ) { + toOne.setHasMapsId( true ); + } + } + private static void setHibernateFetchMode(ToOne toOne, MemberDetails property, org.hibernate.annotations.FetchMode fetchMode) { switch ( fetchMode ) { case JOIN: diff --git a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java index b6e44cdd7878..6a630be1c765 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java @@ -133,7 +133,7 @@ public void addGeneratedValuePlan(GenerationPlan plan) { public Object generate(SharedSessionContractImplementor session, Object object) { final Object context = generationContextLocator.locateGenerationContext( session, object ); final List generatedValues = generatedValues( session, object, context ); - if ( generatedValues != null) { + if ( generatedValues != null ) { final Object[] values = compositeType.getPropertyValues( context ); for ( int i = 0; i < generatedValues.size(); i++ ) { values[generationPlans.get( i ).getPropertyIndex()] = generatedValues.get( i ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index f77bcd806b85..debaf18bf7fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -35,10 +35,13 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.DiscriminatorType; import org.hibernate.metamodel.mapping.EmbeddableDiscriminatorConverter; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.internal.DiscriminatorTypeImpl; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.persister.entity.DiscriminatorHelper; import org.hibernate.models.spi.ClassDetails; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.property.access.spi.Setter; import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer; import org.hibernate.type.ComponentType; @@ -782,7 +785,28 @@ public StandardGenerationContextLocator(String entityName) { @Override public Object locateGenerationContext(SharedSessionContractImplementor session, Object incomingObject) { - return session.getEntityPersister( entityName, incomingObject ).getIdentifier( incomingObject, session ); + final var persister = session.getEntityPersister( entityName, incomingObject ); + final var context = persister.getIdentifier( incomingObject, session ); + if ( context != null ) { + return context; + } + + if ( persister.getIdentifierMapping() instanceof EmbeddableValuedModelPart embeddableId && containsMapsId( persister ) ) { + final var strategy = embeddableId.getEmbeddableTypeDescriptor().getRepresentationStrategy(); + return strategy.getInstantiator().instantiate( null ); + } + return null; + } + + private static boolean containsMapsId(EntityPersister persister) { + final var attributeMappings = persister.getAttributeMappings(); + for ( var i = 0; i < attributeMappings.size(); i++ ) { + final var attributeMapping = attributeMappings.get( i ); + if ( attributeMapping instanceof ToOneAttributeMapping toOneMapping && toOneMapping.hasMapsId() ) { + return true; + } + } + return false; } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java b/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java index 4826d44f23cf..9561f9870def 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java @@ -34,6 +34,7 @@ public abstract sealed class ToOne private boolean unwrapProxy; private boolean unwrapProxyImplicit; private boolean referenceToPrimaryKey = true; + private boolean hasMapsId = false; protected ToOne(MetadataBuildingContext buildingContext, Table table) { super( buildingContext, table ); @@ -79,6 +80,14 @@ public void setReferencedEntityName(String referencedEntityName) { null : referencedEntityName.intern(); } + public boolean hasMapsId() { + return hasMapsId; + } + + public void setHasMapsId(boolean hasMapsId) { + this.hasMapsId = hasMapsId; + } + public String getPropertyName() { return propertyName; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index b2dd2b44219a..e9ec1ff72b22 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -155,6 +155,7 @@ public class Entity1 { private final Cardinality cardinality; private final boolean hasJoinTable; + private final boolean hasMapsId; /* Capture the other side's name of a possibly bidirectional association to allow resolving circular fetches. It may be null if the referenced property is a non-entity. @@ -183,6 +184,7 @@ protected ToOneAttributeMapping(ToOneAttributeMapping original) { targetKeyPropertyName = original.targetKeyPropertyName; cardinality = original.cardinality; hasJoinTable = original.hasJoinTable; + hasMapsId = original.hasMapsId; bidirectionalAttributePath = original.bidirectionalAttributePath; declaringTableGroupProducer = original.declaringTableGroupProducer; isKeyTableNullable = original.isKeyTableNullable; @@ -250,6 +252,7 @@ public ToOneAttributeMapping( ); sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( name ); isNullable = bootValue.isNullable(); + hasMapsId = bootValue.hasMapsId(); isLazy = navigableRole.getParent().getParent() == null && declaringEntityPersister.getBytecodeEnhancementMetadata() .getLazyAttributesMetadata() @@ -679,6 +682,7 @@ private ToOneAttributeMapping( this.targetKeyPropertyNames = original.targetKeyPropertyNames; this.cardinality = original.cardinality; this.hasJoinTable = original.hasJoinTable; + this.hasMapsId = original.hasMapsId; this.bidirectionalAttributePath = original.bidirectionalAttributePath; this.declaringTableGroupProducer = declaringTableGroupProducer; this.isInternalLoadNullable = original.isInternalLoadNullable; @@ -2395,6 +2399,10 @@ public boolean hasNotFoundAction() { return notFoundAction != null; } + public boolean hasMapsId() { + return hasMapsId; + } + @Override public boolean isUnwrapProxy() { return unwrapProxy; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/mapsid/MapsEmbeddedIdNullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/mapsid/MapsEmbeddedIdNullTest.java new file mode 100644 index 000000000000..6f745466b151 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/mapsid/MapsEmbeddedIdNullTest.java @@ -0,0 +1,182 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.mapsid; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import org.hibernate.id.IdentifierGenerationException; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SessionFactory +@DomainModel( + annotatedClasses = { + MapsEmbeddedIdNullTest.Level0.class, + MapsEmbeddedIdNullTest.Level1.class, + MapsEmbeddedIdNullTest.Level2.class, + MapsEmbeddedIdNullTest.Level1NoMapsId.class, + MapsEmbeddedIdNullTest.Level1OneToOne.class, + MapsEmbeddedIdNullTest.Level1NoMapsIdOneToOne.class, + }) +@Jira("https://hibernate.atlassian.net/browse/HHH-19056") +public class MapsEmbeddedIdNullTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( s -> { + Level0 level0 = new Level0(); + Level2 level2 = new Level2(); + Level1 level1 = new Level1( level0, level2 ); + level0.level1s.add( level1 ); + s.persist( level0 ); + } ); + + scope.inTransaction( s -> { + Level0 level0 = new Level0(); + Level2 level2 = new Level2(); + Level1OneToOne level1 = new Level1OneToOne( level0, level2 ); + s.persist( level1 ); + } ); + + assertThrows( IdentifierGenerationException.class, () -> + scope.inTransaction( s -> { + Level0 level0 = new Level0(); + Level2 level2 = new Level2(); + Level1NoMapsId level1NoMapsId = new Level1NoMapsId(); + level1NoMapsId.level0 = level0; + level1NoMapsId.level2 = level2; + s.persist( level1NoMapsId ); + } ) + ); + + assertThrows( IdentifierGenerationException.class, () -> + scope.inTransaction( s -> { + Level0 level0 = new Level0(); + Level2 level2 = new Level2(); + Level1NoMapsIdOneToOne level1 = new Level1NoMapsIdOneToOne(); + level1.level0 = level0; + level1.level2 = level2; + s.persist( level1 ); + } ) + ); + } + + @Entity(name = "Level0") + public static class Level0 { + @Id + @GeneratedValue + private Integer id; + @OneToMany(mappedBy = "level0", cascade = CascadeType.ALL) + private List level1s = new ArrayList<>(); + } + + @Entity(name = "Level1") + public static class Level1 { + @EmbeddedId + Level1PK id; + @MapsId("level0Id") + @ManyToOne + private Level0 level0; + @MapsId("level2Id") + @ManyToOne(cascade = CascadeType.ALL) + private Level2 level2; + + public Level1() { + } + + public Level1(Level0 level0, Level2 level2) { + super(); + this.level0 = level0; + this.level2 = level2; + } + } + + @Entity(name = "Level1OneToOne") + public static class Level1OneToOne { + @EmbeddedId + Level1PK id; + @OneToOne + @MapsId("level0Id") + private Level0 level0; + @OneToOne + @MapsId("level2Id") + private Level2 level2; + + public Level1OneToOne() { + } + + public Level1OneToOne(Level0 level0, Level2 level2) { + super(); + this.level0 = level0; + this.level2 = level2; + } + } + + + @Entity(name = "Level1NoMapsId") + public static class Level1NoMapsId { + @EmbeddedId + Level1PK id; + @ManyToOne + private Level0 level0; + @ManyToOne(cascade = CascadeType.ALL) + private Level2 level2; + } + + @Entity(name = "Level1NoMapsIdOneToOne") + public static class Level1NoMapsIdOneToOne { + @EmbeddedId + Level1PK id; + @ManyToOne + private Level0 level0; + @ManyToOne(cascade = CascadeType.ALL) + private Level2 level2; + } + + @Entity(name = "Level2") + public static class Level2 { + @Id + @GeneratedValue + private Integer id; + } + + public static class Level1PK { + private Integer level0Id; + private Integer level2Id; + + @Override + public final boolean equals(Object o) { + if ( !(o instanceof Level1PK level1PK) ) { + return false; + } + + return Objects.equals( level0Id, level1PK.level0Id ) + && Objects.equals( level2Id, level1PK.level2Id ); + } + + @Override + public int hashCode() { + int result = Objects.hashCode( level0Id ); + result = 31 * result + Objects.hashCode( level2Id ); + return result; + } + } +}