diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index c54a747c14e4..97c05015cd3c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -276,6 +276,7 @@ import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -1183,6 +1184,15 @@ private static List initUniqueKeyEntries(final AbstractEntityPer uniqueKeys.add( new UniqueKeyEntry( ukName, index, type ) ); } } + else if ( associationType instanceof ManyToOneType manyToOneType + && manyToOneType.isLogicalOneToOne() && manyToOneType.isReferenceToPrimaryKey() ) { + final AttributeMapping attributeMapping = aep.findAttributeMapping( manyToOneType.getPropertyName() ); + if ( attributeMapping != null ) { + final int index = attributeMapping.getStateArrayPosition(); + final Type type = aep.getPropertyTypes()[index]; + uniqueKeys.add( new UniqueKeyEntry( manyToOneType.getPropertyName(), index, type ) ); + } + } } } return toSmallList( uniqueKeys ); 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 2b8a64324576..77099a5e3111 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 @@ -23,6 +23,7 @@ import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; +import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; @@ -75,6 +76,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -622,6 +624,8 @@ private boolean areKeysEqual(Object key1, Object key2) { protected void resolveInstanceSubInitializers(EntityInitializerData data) { final int subclassId = data.concreteDescriptor.getSubclassId(); final EntityEntry entityEntry = data.entityHolder.getEntityEntry(); + assert entityEntry != null : "This method should only be called if the entity is already initialized"; + final Initializer[] initializers; final ImmutableBitSet maybeLazySet; if ( data.entityHolder.getEntityInitializer() == this ) { @@ -981,11 +985,13 @@ else if ( lazyInitializer.isUninitialized() ) { registerLoadingEntity( data, data.entityInstanceForNotify ); } else { - data.setState( State.INITIALIZED ); data.entityInstanceForNotify = lazyInitializer.getImplementation(); data.concreteDescriptor = session.getEntityPersister( null, data.entityInstanceForNotify ); resolveEntityKey( data, lazyInitializer.getIdentifier() ); data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); + // Even though the lazyInitializer reports it is initialized, check if the entity holder reports initialized, + // because in a nested initialization scenario, this nested initializer must initialize the entity + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); } if ( identifierAssembler != null ) { final Initializer initializer = identifierAssembler.getInitializer(); @@ -1582,11 +1588,22 @@ protected void registerPossibleUniqueKeyEntries( // one used here, which it will be if ( resolvedEntityState[index] != null ) { + final Object key; + if ( type instanceof ManyToOneType manyToOneType ) { + key = ForeignKeys.getEntityIdentifierIfNotUnsaved( + manyToOneType.getAssociatedEntityName(), + resolvedEntityState[index], + session + ); + } + else { + key = resolvedEntityState[index]; + } final EntityUniqueKey entityUniqueKey = new EntityUniqueKey( data.concreteDescriptor.getRootEntityDescriptor().getEntityName(), //polymorphism comment above ukName, - resolvedEntityState[index], + key, type, session.getFactory() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java new file mode 100644 index 000000000000..499fa1582035 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest.User.class, + RefreshAndBatchTest.UserInfo.class, + RefreshAndBatchTest.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + 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 testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + 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.LAZY) + @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; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java new file mode 100644 index 000000000000..849033fb6b7d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java @@ -0,0 +1,209 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest2.User.class, + RefreshAndBatchTest2.UserInfo.class, + RefreshAndBatchTest2.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest2 { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User parentUser = new User( 1L, "user1", null ); + session.persist( parentUser ); + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + info.parentUser = parentUser; + User user = new User( 2L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "update UserInfo set parentUser = null" ).executeUpdate(); + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 2L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + 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; + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + if ( info != null ) { + this.info = info; + info.parentUser = 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; + + @ManyToOne + private User parentUser; + + 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 getParentUser() { + return parentUser; + } + + 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; + } + } +}