diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java index 55eee4d12fae..0698e8ccb6c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java @@ -11,6 +11,7 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.MappedSuperclassDomainType; import org.hibernate.query.sqm.SqmBindableType; import org.hibernate.query.sqm.tuple.TupleType; import org.hibernate.metamodel.model.domain.internal.EntityDiscriminatorSqmPathSource; @@ -331,10 +332,13 @@ private static boolean isTypeAssignable( } // entities can be assigned if they belong to the same inheritance hierarchy - if ( targetType instanceof EntityType targetEntity && expressionType instanceof EntityType expressionEntity ) { - return isEntityTypeAssignable( targetEntity, expressionEntity, bindingContext); + return isEntityTypeAssignable( targetEntity, expressionEntity, bindingContext ); + } + if ( targetType instanceof MappedSuperclassDomainType expressionMappedSuperclass + && expressionType instanceof EntityType expressionEntity ) { + return isMappedSuperclassTypeAssignable( expressionMappedSuperclass, expressionEntity, bindingContext ); } // Treat the expression as assignable to the target path if they belong @@ -394,6 +398,23 @@ private static Class canonicalize(Class lhs) { }; } + private static boolean isMappedSuperclassTypeAssignable( + MappedSuperclassDomainType lhsType, + EntityType rhsType, + BindingContext bindingContext) { + + for ( ManagedDomainType candidate : lhsType.getSubTypes() ) { + if ( candidate instanceof EntityType candidateEntityType + && isEntityTypeAssignable( candidateEntityType, rhsType, bindingContext ) ) { + return true; + } + else if ( candidate instanceof MappedSuperclassDomainType candidateMappedSuperclass + && isMappedSuperclassTypeAssignable( candidateMappedSuperclass, rhsType, bindingContext ) ) { + return true; + } + } + return false; + } private static boolean isEntityTypeAssignable( EntityType lhsType, EntityType rhsType, BindingContext bindingContext) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/generics/GenericMappedSuperclassPropertyUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/generics/GenericMappedSuperclassPropertyUpdateTest.java new file mode 100644 index 000000000000..375e18ad6a01 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/generics/GenericMappedSuperclassPropertyUpdateTest.java @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.generics; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + GenericMappedSuperclassPropertyUpdateTest.CommonEntity.class, + GenericMappedSuperclassPropertyUpdateTest.SpecificEntity.class + } +) +@SessionFactory +@JiraKey(value = "HHH-19872") +public class GenericMappedSuperclassPropertyUpdateTest implements SessionFactoryScopeAware { + private SessionFactoryScope scope; + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); + } + + @ParameterizedTest + @MethodSource("criteriaUpdateFieldSetters") + void testGenericHierarchy(Consumer updater) { + scope.inTransaction( session -> { + SpecificEntity relative = new SpecificEntity(); + SpecificEntity base = new SpecificEntity(); + session.persist( relative ); + session.persist( base ); + + final CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaUpdate criteriaUpdate = cb.createCriteriaUpdate( SpecificEntity.class ); + Root root = criteriaUpdate.from( SpecificEntity.class ); + + updater.accept( new UpdateContext( criteriaUpdate, root, base.getId(), relative ) ); + criteriaUpdate.where( cb.equal( root.get( GenericMappedSuperclassPropertyUpdateTest_.SpecificEntity_.id ), base.getId() ) ); + + int updates = session.createQuery( criteriaUpdate ).executeUpdate(); + session.refresh( base ); + + assertThat( updates ) + .isEqualTo( 1L ); + assertThat( base.getRelative() ) + .isEqualTo( relative ); + } ); + + } + + + static class UpdateContext { + final CriteriaUpdate query; + final Root root; + final Long id; + final SpecificEntity relative; + + UpdateContext(CriteriaUpdate query, Root root, Long id, SpecificEntity relative) { + this.query = query; + this.root = root; + this.id = id; + this.relative = relative; + } + } + + static Stream criteriaUpdateFieldSetters() { + Consumer updateUsingPath = context -> + context.query.set( context.root.get( GenericMappedSuperclassPropertyUpdateTest_.SpecificEntity_.relative ), context.relative ); + Consumer updateUsingSingularAttribute = context -> + context.query.set( GenericMappedSuperclassPropertyUpdateTest_.SpecificEntity_.relative, context.relative ); + Consumer updateUsingName = context -> + context.query.set( GenericMappedSuperclassPropertyUpdateTest_.SpecificEntity_.RELATIVE, context.relative ); + return Stream.of( + Arguments.of( updateUsingPath ), + Arguments.of( updateUsingSingularAttribute ), + Arguments.of( updateUsingName ) + ); + } + + @Override + public void injectSessionFactoryScope(SessionFactoryScope scope) { + this.scope = scope; + } + + @MappedSuperclass + public abstract static class CommonEntity> { + + @Id + @GeneratedValue + private Long id; + + Long getId() { + return id; + } + + @ManyToOne + @JoinColumn + private E relative; + + void setRelative(E relative) { + this.relative = relative; + } + + E getRelative() { + return relative; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + CommonEntity that = (CommonEntity) o; + return Objects.equals( id, that.id ) && Objects.equals( relative, that.relative ); + } + + @Override + public int hashCode() { + return Objects.hash( id, relative ); + } + } + + @Entity(name = "SpecificEntity") + public static class SpecificEntity extends CommonEntity { + } +}