From 5a04c37edc495f6d46a020c3801235e4eed08dde Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 2 Oct 2024 17:43:32 +0200 Subject: [PATCH] HHH-18645 Handle proxies when resolving from existing entity in batch initializer --- ...ractBatchEntitySelectFetchInitializer.java | 49 ++-- .../BatchEntitySelectFetchInitializer.java | 13 +- .../internal/EntityInitializerImpl.java | 2 +- .../enhancement/batch/BatchLazyProxyTest.java | 209 ++++++++++++++++++ 4 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java index 793a5451b1..5ac6c90bcd 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java @@ -10,10 +10,8 @@ import org.hibernate.EntityFilterException; import org.hibernate.FetchNotFoundException; import org.hibernate.Hibernate; import org.hibernate.annotations.NotFoundAction; -import org.hibernate.engine.spi.EntityHolder; -import org.hibernate.engine.spi.EntityKey; -import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.*; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -31,7 +29,9 @@ import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; +import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.getAttributeInterceptor; public abstract class AbstractBatchEntitySelectFetchInitializer extends EntitySelectFetchInitializer implements EntityInitializer { @@ -111,6 +111,10 @@ public abstract class AbstractBatchEntitySelectFetchInitializer initializer = keyAssembler.getInitializer(); assert initializer != null; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java index 3eb69b6cd0..a5b623fe5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java @@ -67,14 +67,17 @@ public class BatchEntitySelectFetchInitializer extends AbstractBatchEntitySelect protected void registerResolutionListener(BatchEntitySelectFetchInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final InitializerData owningData = owningEntityInitializer.getData( rowProcessingState ); + HashMap> toBatchLoad = data.toBatchLoad; + if ( toBatchLoad == null ) { + toBatchLoad = data.toBatchLoad = new HashMap<>(); + } + // Always register the entity key for resolution + final List parentInfos = toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ); final AttributeMapping parentAttribute; + // But only add the parent info if the parent entity is not already initialized if ( owningData.getState() != State.INITIALIZED && ( parentAttribute = parentAttributes[owningEntityInitializer.getConcreteDescriptor( owningData ).getSubclassId()] ) != null ) { - HashMap> toBatchLoad = data.toBatchLoad; - if ( toBatchLoad == null ) { - toBatchLoad = data.toBatchLoad = new HashMap<>(); - } - toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ).add( + parentInfos.add( new ParentInfo( owningEntityInitializer.getTargetInstance( owningData ), parentAttribute.getStateArrayPosition() diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index 41c1866fbd..8c61d7a7e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -1779,7 +1779,7 @@ public class EntityInitializerImpl extends AbstractInitializer { + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + User user = new User( 1L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testBatchInitialize(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setEntityGraph( session.createEntityGraph( User.class ), GraphSemantic.FETCH ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "INFO_ID", referencedColumnName = "ID") + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + this.info = info; + info.user = this; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @OneToOne(mappedBy = "info", fetch = FetchType.LAZY) + private User user; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getUser() { + return user; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +}