From ed472eff8aabdd3d487bf80aa81b7fd3dcc892ae Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 20 Jun 2023 10:29:56 +0200 Subject: [PATCH] HHH-13857 Avoid initialization when obtaining persistent class with Hibernate.getClass() --- .../main/java/org/hibernate/Hibernate.java | 20 ++ .../proxy/AbstractLazyInitializer.java | 14 ++ .../org/hibernate/proxy/LazyInitializer.java | 16 ++ .../proxy/map/MapLazyInitializer.java | 5 + .../proxy/pojo/BasicLazyInitializer.java | 21 ++ .../test/getclass/HibernateGetClassTest.java | 189 ++++++++++++++++++ 6 files changed, 265 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/getclass/HibernateGetClassTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/Hibernate.java b/hibernate-core/src/main/java/org/hibernate/Hibernate.java index 3751a943d3..16bb1e0c5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/Hibernate.java +++ b/hibernate-core/src/main/java/org/hibernate/Hibernate.java @@ -259,6 +259,26 @@ public final class Hibernate { return (Class) result; } + /** + * Get the true, underlying class of a proxied entity. This operation might + * initialize a proxy by side effect. + * + * @param proxy an entity instance or proxy + * @return the true class of the instance + */ + @SuppressWarnings("unchecked") + public static Class getClassLazy(T proxy) { + Class result; + final LazyInitializer lazyInitializer = extractLazyInitializer( proxy ); + if ( lazyInitializer != null ) { + result = lazyInitializer.getImplementationClass(); + } + else { + result = proxy.getClass(); + } + return (Class) result; + } + /** * Determine if the true, underlying class of the proxied entity is assignable * to the given class. This operation will initialize a proxy by side effect. diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/AbstractLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/AbstractLazyInitializer.java index 0a7ca1aedd..7d267b271e 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/AbstractLazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/AbstractLazyInitializer.java @@ -323,6 +323,20 @@ public abstract class AbstractLazyInitializer implements LazyInitializer { return entityKey == null ? null : s.getPersistenceContext().getEntity( entityKey ); } + @Override + public String getImplementationEntityName() { + if ( session == null ) { + throw new LazyInitializationException( "could not retrieve real entity name [" + entityName + "#" + id + "] - no Session" ); + } + final SessionFactoryImplementor factory = session.getFactory(); + final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( entityName ); + if ( entityDescriptor.getEntityMetamodel().hasSubclasses() ) { + initialize(); + return factory.bestGuessEntityName( target ); + } + return entityName; + } + /** * Getter for property 'target'. *

diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/LazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/LazyInitializer.java index 4fced0d558..41c71441af 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/LazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/LazyInitializer.java @@ -100,6 +100,22 @@ public interface LazyInitializer { */ void setImplementation(Object target); + /** + * Get the actual class of the entity, possibly initializing the entity if it has subclasses. + * + * @return The actual entity class. + * @since 6.3 + */ + Class getImplementationClass(); + + /** + * Get the actual name of the entity, possibly initializing the entity if it has subclasses. + * + * @return The actual entity name. + * @since 6.3 + */ + String getImplementationEntityName(); + /** * Is the proxy's read-only/modifiable setting available? * @return true, if the setting is available diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/map/MapLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/map/MapLazyInitializer.java index 0122fcf6d5..6253ff804d 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/map/MapLazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/map/MapLazyInitializer.java @@ -32,6 +32,11 @@ public class MapLazyInitializer extends AbstractLazyInitializer implements Seria throw new UnsupportedOperationException("dynamic-map entity representation"); } + @Override + public Class getImplementationClass() { + throw new UnsupportedOperationException("dynamic-map entity representation"); + } + // Expose the following methods to MapProxy by overriding them (so that classes in this package see them) @Override diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java index ffd6d50071..4d56431ae0 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java @@ -8,8 +8,11 @@ package org.hibernate.proxy.pojo; import java.lang.reflect.Method; +import org.hibernate.LazyInitializationException; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.MarkerObject; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.AbstractLazyInitializer; import org.hibernate.type.CompositeType; @@ -108,8 +111,26 @@ public abstract class BasicLazyInitializer extends AbstractLazyInitializer { } + @Override public final Class getPersistentClass() { return persistentClass; } + @Override + public Class getImplementationClass() { + if ( !isUninitialized() ) { + return getImplementation().getClass(); + } + final SharedSessionContractImplementor session = getSession(); + if ( session == null ) { + throw new LazyInitializationException( "could not retrieve real entity class [" + getEntityName() + "#" + getIdentifier() + "] - no Session" ); + } + final SessionFactoryImplementor factory = session.getFactory(); + final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( getEntityName() ); + if ( entityDescriptor.getEntityMetamodel().hasSubclasses() ) { + return getImplementation().getClass(); + } + return persistentClass; + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/getclass/HibernateGetClassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/getclass/HibernateGetClassTest.java new file mode 100644 index 0000000000..a9006a0be8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/getclass/HibernateGetClassTest.java @@ -0,0 +1,189 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.getclass; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + HibernateGetClassTest.TestEntity.class, + HibernateGetClassTest.TestSubEntity.class, + HibernateGetClassTest.TestRegularEntity.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +@JiraKey("HHH-15453") +public class HibernateGetClassTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity e1 = new TestSubEntity( 1, "E1" ); + TestRegularEntity e2 = new TestRegularEntity( 2, "E2" ); + TestEntity e3 = new TestEntity( 3, "E2", e1, e2 ); + + session.persist( e1 ); + session.persist( e2 ); + session.persist( e3 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "update TestEntity e set e.manyToOne = null" ).executeUpdate(); + session.createMutationQuery( "delete from TestEntity" ).executeUpdate(); + session.createMutationQuery( "delete from TestRegularEntity" ).executeUpdate(); + } + ); + } + + @Test + public void testSelectUserWithRole(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + scope.inSession( + session -> { + TestEntity e3 = session.find( TestEntity.class, 3 ); + statementInspector.clear(); + assertThat( Hibernate.getClassLazy( e3 ) ).isEqualTo( TestEntity.class ); + assertThat( Hibernate.getClassLazy( e3.regularToOne ) ).isEqualTo( TestRegularEntity.class ); + assertThat( statementInspector.getSqlQueries().size() ).isEqualTo( 0 ); + assertThat( Hibernate.getClassLazy( e3.manyToOne ) ).isEqualTo( TestSubEntity.class ); + assertThat( statementInspector.getSqlQueries().size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + private Integer id; + private String name; + private TestEntity manyToOne; + private TestRegularEntity regularToOne; + + public TestEntity() { + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public TestEntity(Integer id, String name, TestEntity manyToOne, TestRegularEntity regularToOne) { + this.id = id; + this.name = name; + this.manyToOne = manyToOne; + this.regularToOne = regularToOne; + } + + @Id + @Column(name = "USER_ID") + public Integer getId() { + return id; + } + + public void setId(Integer userId) { + this.id = userId; + } + + @Column(name = "USER_NAME") + public String getName() { + return name; + } + + public void setName(String userName) { + this.name = userName; + } + + @ManyToOne(fetch = FetchType.LAZY) + public TestEntity getManyToOne() { + return manyToOne; + } + + public void setManyToOne(TestEntity manyToOne) { + this.manyToOne = manyToOne; + } + + @ManyToOne(fetch = FetchType.LAZY) + public TestRegularEntity getRegularToOne() { + return regularToOne; + } + + public void setRegularToOne(TestRegularEntity regularToOne) { + this.regularToOne = regularToOne; + } + } + + @Entity(name = "TestSubEntity") + public static class TestSubEntity extends TestEntity { + public TestSubEntity() { + } + + public TestSubEntity(Integer id, String name) { + super( id, name ); + } + + public TestSubEntity(Integer id, String name, TestEntity manyToOne, TestRegularEntity regularToOne) { + super( id, name, manyToOne, regularToOne ); + } + } + + @Entity(name = "TestRegularEntity") + public static class TestRegularEntity { + + private Integer id; + private String name; + + public TestRegularEntity() { + } + + public TestRegularEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Id + @Column(name = "USER_ID") + public Integer getId() { + return id; + } + + public void setId(Integer userId) { + this.id = userId; + } + + @Column(name = "USER_NAME") + public String getName() { + return name; + } + + public void setName(String userName) { + this.name = userName; + } + } +}