diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 52a057a8155d..66833859f886 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -630,6 +630,11 @@ private static PropertyData getUniqueIdPropertyFromBaseClass( return baseClassElements.get( 0 ); } + /** + * Given the id class of the current entity, as specified by an + * {@link IdClass} annotation, determine if it's actually the + * identifier type or {@link IdClass} of some associated entity. + */ private boolean isIdClassPrimaryKeyOfAssociatedEntity( ElementsToProcess elementsToProcess, ClassDetails compositeClass, @@ -639,26 +644,58 @@ private boolean isIdClassPrimaryKeyOfAssociatedEntity( Map inheritanceStates, MetadataBuildingContext context) { if ( elementsToProcess.getIdPropertyCount() == 1 ) { + // There's only one @Id field, so it might be the @EmbeddedId of an associated + // entity referenced via a @ManyToOne or @OneToOne association final PropertyData idPropertyOnBaseClass = getUniqueIdPropertyFromBaseClass( inferredData, baseInferredData, propertyAccessor, context ); - final InheritanceState state = - inheritanceStates.get( idPropertyOnBaseClass.getClassOrElementType().determineRawClass() ); + final TypeDetails idPropertyType = idPropertyOnBaseClass.getClassOrElementType(); + final InheritanceState state = inheritanceStates.get( idPropertyType.determineRawClass() ); if ( state == null ) { - return false; //while it is likely a user error, let's consider it is something that might happen - } - final ClassDetails associatedClassWithIdClass = state.getClassWithIdClass( true ); - if ( associatedClassWithIdClass == null ) { - //we cannot know for sure here unless we try and find the @EmbeddedId - //Let's not do this thorough checking but do some extra validation - return hasToOneAnnotation( idPropertyOnBaseClass.getAttributeMember() ); - + // Likely a user error, but treat it as something that might happen + return false; } else { - final IdClass idClass = associatedClassWithIdClass.getAnnotationUsage( IdClass.class, modelsContext() ); - return compositeClass.getName().equals( idClass.value().getName() ); + final ClassDetails associatedClassWithIdClass = state.getClassWithIdClass( true ); + if ( associatedClassWithIdClass == null ) { + // If annotated @OneToOne or @ManyToOne, it's an association to another entity + return hasToOneAnnotation( idPropertyOnBaseClass.getAttributeMember() ) + // determine if the @Id or @EmbeddedId tpe is the same + && isIdClassOfAssociatedEntity( compositeClass, propertyAccessor, context, idPropertyType ); + } + else { + // The associated entity has an @IdClass, so check if it's the same + final IdClass idClass = + associatedClassWithIdClass.getAnnotationUsage( IdClass.class, modelsContext() ); + return compositeClass.getName().equals( idClass.value().getName() ); + } } } else { + // There are multiple @Id fields, so we know for sure that the id class of + // this entity can't be the identifier type of the associated entity + return false; + } + } + + private static boolean isIdClassOfAssociatedEntity( + ClassDetails compositeClass, + AccessType propertyAccessor, + MetadataBuildingContext context, + TypeDetails idPropertyType) { + // Determine the @Id type or @EmbeddedId class of the associated entity + final var propertyContainer = + new PropertyContainer( idPropertyType.determineRawClass(), idPropertyType, propertyAccessor ); + final List idProperties = new ArrayList<>(); + final int idPropertyCount = addElementsOfClass( idProperties, propertyContainer, context, 0 ); + if ( idPropertyCount == 1 ) { + // Exactly one @Id or @EmbeddedId attribute + final PropertyData idPropertyOfAssociatedEntity = idProperties.get( 0 ); + return compositeClass.getName() + .equals( idPropertyOfAssociatedEntity.getPropertyType().getName() ); + } + else { + // No id property found in the associated class, + // or multiple id properties but no @IdClass return false; } } diff --git a/hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java b/hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java new file mode 100644 index 000000000000..be3f172e2d31 --- /dev/null +++ b/hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package x; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.OneToOne; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@DomainModel( annotatedClasses = { + IdClassSingleOneToOneTest.EntityA.class, + IdClassSingleOneToOneTest.EntityB.class, +} ) +@SessionFactory +public class IdClassSingleOneToOneTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.getSessionFactory(); + scope.inTransaction( session -> { + EntityA entityA = new EntityA(3); + EntityB entityB = new EntityB( entityA ); + entityA.entityB = entityB; + session.persist( entityA ); + session.persist( entityB ); + assertEquals( new EntityBId(3), + session.getIdentifier( entityB ) ); + } ); + scope.inTransaction( session -> { + EntityB entityB = session.find( EntityB.class, new EntityBId(3) ); + assertNotNull( entityB ); + } ); + } + + @Entity( name = "EntityA" ) + static class EntityA { + + @Id + private Integer id; + + @OneToOne( mappedBy = "entityA", fetch = FetchType.LAZY ) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + } + + @IdClass( EntityBId.class ) + @Entity( name = "EntityB" ) + static class EntityB { + + @Id + @OneToOne( fetch = FetchType.LAZY ) + private EntityA entityA; + + public EntityB() { + } + + public EntityB(EntityA entityA) { + this.entityA = entityA; + } + + } + + static class EntityBId { + private final Integer entityA; + + EntityBId() { + entityA = null; + } + EntityBId(Integer entityA) { + this.entityA = entityA; + } + + @Override + public boolean equals(Object o) { + return o instanceof EntityBId entityBId + && Objects.equals( entityA, entityBId.entityA ); + } + + @Override + public int hashCode() { + return Objects.hashCode( entityA ); + } + } +}