HHH-18645 Handle proxies when resolving from existing entity in batch initializer

This commit is contained in:
Christian Beikov 2024-10-02 17:43:32 +02:00
parent eaf0ba3aee
commit 5a04c37edc
4 changed files with 254 additions and 19 deletions

View File

@ -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<Data extends AbstractBatchEntitySelectFetchInitializer.AbstractBatchEntitySelectFetchInitializerData>
extends EntitySelectFetchInitializer<Data> implements EntityInitializer<Data> {
@ -111,6 +111,10 @@ public abstract class AbstractBatchEntitySelectFetchInitializer<Data extends Abs
return;
}
}
resolveInstanceFromIdentifier( data );
}
protected void resolveInstanceFromIdentifier(Data data) {
if ( data.batchDisabled ) {
initialize( data );
}
@ -137,21 +141,36 @@ public abstract class AbstractBatchEntitySelectFetchInitializer<Data extends Abs
// Only need to extract the identifier if the identifier has a many to one
final LazyInitializer lazyInitializer = extractLazyInitializer( instance );
data.entityKey = null;
data.entityIdentifier = null;
if ( lazyInitializer == null ) {
// Entity is initialized
data.setState( State.INITIALIZED );
if ( keyIsEager ) {
// Entity is most probably initialized
data.setInstance( instance );
final PersistentAttributeInterceptor interceptor;
if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading()
&& isPersistentAttributeInterceptable( instance )
&& ( interceptor = getAttributeInterceptor( instance ) ) instanceof EnhancementAsProxyLazinessInterceptor ) {
final EnhancementAsProxyLazinessInterceptor enhancementInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor;
if ( enhancementInterceptor.isInitialized() ) {
data.setState( State.INITIALIZED );
}
else {
data.setState( State.RESOLVED );
data.entityIdentifier = enhancementInterceptor.getIdentifier();
}
}
else {
// If the entity initializer is null, we know the entity is fully initialized,
// otherwise it will be initialized by some other initializer
data.setState( State.RESOLVED );
data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() );
}
if ( keyIsEager && data.entityIdentifier == null ) {
data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() );
}
data.setInstance( instance );
}
else if ( lazyInitializer.isUninitialized() ) {
data.setState( State.RESOLVED );
if ( keyIsEager ) {
data.entityIdentifier = lazyInitializer.getIdentifier();
}
// Resolve and potentially create the entity instance
registerToBatchFetchQueue( data );
data.entityIdentifier = lazyInitializer.getIdentifier();
}
else {
// Entity is initialized
@ -161,6 +180,10 @@ public abstract class AbstractBatchEntitySelectFetchInitializer<Data extends Abs
}
data.setInstance( lazyInitializer.getImplementation() );
}
if ( data.getState() == State.RESOLVED ) {
resolveInstanceFromIdentifier( data );
}
if ( keyIsEager ) {
final Initializer<?> initializer = keyAssembler.getInitializer();
assert initializer != null;

View File

@ -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<EntityKey, List<ParentInfo>> toBatchLoad = data.toBatchLoad;
if ( toBatchLoad == null ) {
toBatchLoad = data.toBatchLoad = new HashMap<>();
}
// Always register the entity key for resolution
final List<ParentInfo> 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<EntityKey, List<ParentInfo>> 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()

View File

@ -1779,7 +1779,7 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
}
}
private static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) {
public static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) {
return asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor();
}

View File

@ -0,0 +1,209 @@
/*
* 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.bytecode.enhancement.batch;
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 org.hibernate.Hibernate;
import org.hibernate.annotations.BatchSize;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.graph.GraphSemantic;
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 java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@DomainModel(
annotatedClasses = {
BatchLazyProxyTest.User.class,
BatchLazyProxyTest.UserInfo.class,
BatchLazyProxyTest.Phone.class,
}
)
@SessionFactory
@ServiceRegistry(
settings = {
@Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100")
}
)
@JiraKey("HHH-18645")
@BytecodeEnhanced(runNotEnhancedAsWell = true)
public class BatchLazyProxyTest {
@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 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<Phone> 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<Phone> 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;
}
}
}