diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java index aed519b8306a..32b3a1216e61 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java @@ -128,8 +128,12 @@ private ProxyFactory resolveProxyFactory( JavaType proxyJavaType, BytecodeProvider bytecodeProvider, RuntimeModelCreationContext creationContext) { - // todo : `@ConcreteProxy` handling - if ( entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + if ( entityPersister.isAbstract() && bootDescriptor.isConcreteProxy() ) { + // The entity class is abstract, but the hierarchy always gets entities loaded/proxied using their concrete type. + // So we do not need proxies for this entity class. + return null; + } + else if ( entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() && bootDescriptor.getRootClass() == bootDescriptor && !bootDescriptor.hasSubclasses() ) { // the entity is bytecode enhanced for lazy loading diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyWithSealedClassesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyWithSealedClassesTest.java new file mode 100644 index 000000000000..3d60c930549b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyWithSealedClassesTest.java @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.proxy.concrete; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.ConcreteProxy; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the compatibility of Hibernate {@code @ConcreteProxy} with Java {@code sealed} + * inheritance hierarchies. Proxy generation may fail during {@code SessionFactory} + * bootstrap if the abstract sealed root cannot be subclassed at runtime. These tests + * document the limitation and assert the expected failure behavior. + * + * @author Vincent Bouthinon + */ +@Jpa(annotatedClasses = { + ConcreteProxyWithSealedClassesTest.Actor.class, + ConcreteProxyWithSealedClassesTest.Postman.class, + ConcreteProxyWithSealedClassesTest.Scene.class +}) +@JiraKey("HHH-19899") +class ConcreteProxyWithSealedClassesTest { + + @Test + void getReference(EntityManagerFactoryScope scope) { + var id = scope.fromTransaction( entityManager -> { + Actor actor = new Postman(); + entityManager.persist( actor ); + return actor.id; + } ); + scope.inTransaction( entityManager -> { + Actor actor = entityManager.getReference( Actor.class, id ); + assertThat( actor ).isInstanceOf( Postman.class ); + } ); + } + + @Test + void lazyAssociation(EntityManagerFactoryScope scope) { + var id = scope.fromTransaction( entityManager -> { + Actor actor = new Postman(); + entityManager.persist( actor ); + Scene scene = new Scene(); + scene.setActor( actor ); + entityManager.persist( scene ); + return scene.id; + } ); + scope.inTransaction( entityManager -> { + Scene scene = entityManager.find( Scene.class, id ); + assertThat( scene.getActor() ).isInstanceOf( Postman.class ); + } ); + } + + @Entity(name = "actor") + @Table(name = "actor") + @ConcreteProxy + public static abstract sealed class Actor { + @Id + @GeneratedValue + private Long id; + } + + @Entity(name = "Postman") + public static non-sealed class Postman extends Actor { + } + + @Entity(name = "Scene") + public static class Scene { + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Actor actor; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Actor getActor() { + return actor; + } + + public void setActor(Actor actor) { + this.actor = actor; + } + } +}