Skip to content

Commit 5b602bd

Browse files
committed
HHH-18645 Handle proxies when resolving from existing entity in batch initializer
1 parent aa6f88f commit 5b602bd

File tree

4 files changed

+249
-16
lines changed

4 files changed

+249
-16
lines changed

hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.hibernate.FetchNotFoundException;
99
import org.hibernate.Hibernate;
1010
import org.hibernate.annotations.NotFoundAction;
11+
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
1112
import org.hibernate.engine.spi.EntityHolder;
1213
import org.hibernate.engine.spi.EntityKey;
1314
import org.hibernate.engine.spi.PersistenceContext;
@@ -29,7 +30,9 @@
2930

3031
import org.checkerframework.checker.nullness.qual.Nullable;
3132

33+
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
3234
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
35+
import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.getAttributeInterceptor;
3336

3437
public abstract class AbstractBatchEntitySelectFetchInitializer<Data extends AbstractBatchEntitySelectFetchInitializer.AbstractBatchEntitySelectFetchInitializerData>
3538
extends EntitySelectFetchInitializer<Data> implements EntityInitializer<Data> {
@@ -109,6 +112,10 @@ public void resolveInstance(Data data) {
109112
return;
110113
}
111114
}
115+
resolveInstanceFromIdentifier( data );
116+
}
117+
118+
protected void resolveInstanceFromIdentifier(Data data) {
112119
if ( data.batchDisabled ) {
113120
initialize( data );
114121
}
@@ -135,21 +142,34 @@ public void resolveInstance(Object instance, Data data) {
135142
// Only need to extract the identifier if the identifier has a many to one
136143
final LazyInitializer lazyInitializer = extractLazyInitializer( instance );
137144
data.entityKey = null;
145+
data.entityIdentifier = null;
138146
if ( lazyInitializer == null ) {
139-
// Entity is initialized
140-
data.setState( State.INITIALIZED );
141-
if ( keyIsEager ) {
147+
// Entity is most probably initialized
148+
data.setInstance( instance );
149+
if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading()
150+
&& isPersistentAttributeInterceptable( instance )
151+
&& getAttributeInterceptor( instance ) instanceof EnhancementAsProxyLazinessInterceptor enhancementInterceptor ) {
152+
if ( enhancementInterceptor.isInitialized() ) {
153+
data.setState( State.INITIALIZED );
154+
}
155+
else {
156+
data.setState( State.RESOLVED );
157+
data.entityIdentifier = enhancementInterceptor.getIdentifier();
158+
}
159+
}
160+
else {
161+
// If the entity initializer is null, we know the entity is fully initialized,
162+
// otherwise it will be initialized by some other initializer
163+
data.setState( State.RESOLVED );
164+
data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() );
165+
}
166+
if ( keyIsEager && data.entityIdentifier == null ) {
142167
data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() );
143168
}
144-
data.setInstance( instance );
145169
}
146170
else if ( lazyInitializer.isUninitialized() ) {
147171
data.setState( State.RESOLVED );
148-
if ( keyIsEager ) {
149-
data.entityIdentifier = lazyInitializer.getIdentifier();
150-
}
151-
// Resolve and potentially create the entity instance
152-
registerToBatchFetchQueue( data );
172+
data.entityIdentifier = lazyInitializer.getIdentifier();
153173
}
154174
else {
155175
// Entity is initialized
@@ -159,6 +179,10 @@ else if ( lazyInitializer.isUninitialized() ) {
159179
}
160180
data.setInstance( lazyInitializer.getImplementation() );
161181
}
182+
183+
if ( data.getState() == State.RESOLVED ) {
184+
resolveInstanceFromIdentifier( data );
185+
}
162186
if ( keyIsEager ) {
163187
final Initializer<?> initializer = keyAssembler.getInitializer();
164188
assert initializer != null;

hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,17 @@ protected InitializerData createInitializerData(RowProcessingState rowProcessing
6464
@Override
6565
protected void registerResolutionListener(BatchEntitySelectFetchInitializerData data) {
6666
final RowProcessingState rowProcessingState = data.getRowProcessingState();
67-
final InitializerData owningData = owningEntityInitializer.getData( rowProcessingState );
67+
final InitializerData owningData = owningEntityInitializer.getData( rowProcessingState );HashMap<EntityKey, List<ParentInfo>> toBatchLoad = data.toBatchLoad;
68+
if ( toBatchLoad == null ) {
69+
toBatchLoad = data.toBatchLoad = new HashMap<>();
70+
}
71+
// Always register the entity key for resolution
72+
final List<ParentInfo> parentInfos = toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() );
6873
final AttributeMapping parentAttribute;
74+
// But only add the parent info if the parent entity is not already initialized
6975
if ( owningData.getState() != State.INITIALIZED
7076
&& ( parentAttribute = parentAttributes[owningEntityInitializer.getConcreteDescriptor( owningData ).getSubclassId()] ) != null ) {
71-
HashMap<EntityKey, List<ParentInfo>> toBatchLoad = data.toBatchLoad;
72-
if ( toBatchLoad == null ) {
73-
toBatchLoad = data.toBatchLoad = new HashMap<>();
74-
}
75-
toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ).add(
77+
parentInfos.add(
7678
new ParentInfo(
7779
owningEntityInitializer.getTargetInstance( owningData ),
7880
parentAttribute.getStateArrayPosition()

hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1797,7 +1797,7 @@ protected void forEachSubInitializer(BiConsumer<Initializer<?>, RowProcessingSta
17971797
}
17981798
}
17991799

1800-
private static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) {
1800+
public static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) {
18011801
return asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor();
18021802
}
18031803

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.bytecode.enhancement.batch;
6+
7+
import jakarta.persistence.CascadeType;
8+
import jakarta.persistence.Column;
9+
import jakarta.persistence.Entity;
10+
import jakarta.persistence.FetchType;
11+
import jakarta.persistence.GeneratedValue;
12+
import jakarta.persistence.Id;
13+
import jakarta.persistence.JoinColumn;
14+
import jakarta.persistence.ManyToOne;
15+
import jakarta.persistence.OneToMany;
16+
import jakarta.persistence.OneToOne;
17+
import jakarta.persistence.Table;
18+
import org.hibernate.Hibernate;
19+
import org.hibernate.annotations.BatchSize;
20+
import org.hibernate.cfg.AvailableSettings;
21+
import org.hibernate.graph.GraphSemantic;
22+
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
23+
import org.hibernate.testing.orm.junit.DomainModel;
24+
import org.hibernate.testing.orm.junit.JiraKey;
25+
import org.hibernate.testing.orm.junit.ServiceRegistry;
26+
import org.hibernate.testing.orm.junit.SessionFactory;
27+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
28+
import org.hibernate.testing.orm.junit.Setting;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
33+
import java.util.ArrayList;
34+
import java.util.List;
35+
36+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
37+
38+
@DomainModel(
39+
annotatedClasses = {
40+
BatchLazyProxyTest.User.class,
41+
BatchLazyProxyTest.UserInfo.class,
42+
BatchLazyProxyTest.Phone.class,
43+
}
44+
45+
)
46+
@SessionFactory
47+
@ServiceRegistry(
48+
settings = {
49+
@Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100")
50+
51+
}
52+
)
53+
@JiraKey("HHH-18645")
54+
@BytecodeEnhanced(runNotEnhancedAsWell = true)
55+
public class BatchLazyProxyTest {
56+
57+
@BeforeEach
58+
public void setUp(SessionFactoryScope scope) {
59+
scope.inTransaction(
60+
session -> {
61+
UserInfo info = new UserInfo( "info" );
62+
Phone phone = new Phone( "123456" );
63+
info.addPhone( phone );
64+
User user = new User( 1L, "user1", info );
65+
session.persist( user );
66+
}
67+
);
68+
}
69+
70+
@AfterEach
71+
public void tearDown(SessionFactoryScope scope) {
72+
scope.inTransaction(
73+
session -> {
74+
session.createMutationQuery( "delete User" ).executeUpdate();
75+
session.createMutationQuery( "delete Phone" ).executeUpdate();
76+
session.createMutationQuery( "delete UserInfo" ).executeUpdate();
77+
}
78+
);
79+
}
80+
81+
@Test
82+
public void testBatchInitialize(SessionFactoryScope scope) {
83+
scope.inTransaction(
84+
session -> {
85+
User user = session.createQuery( "select u from User u where u.id = :id", User.class )
86+
.setEntityGraph( session.createEntityGraph( User.class ), GraphSemantic.FETCH )
87+
.setParameter( "id", 1L )
88+
.getSingleResult();
89+
assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse();
90+
session.createQuery( "select u from User u where u.id = :id", User.class )
91+
.setParameter( "id", 1L )
92+
.getSingleResult();
93+
assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue();
94+
}
95+
);
96+
}
97+
98+
@Entity(name = "User")
99+
@Table(name = "USER_TABLE")
100+
@BatchSize(size = 5)
101+
public static class User {
102+
103+
@Id
104+
private Long id;
105+
106+
@Column
107+
private String name;
108+
109+
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
110+
@JoinColumn(name = "INFO_ID", referencedColumnName = "ID")
111+
private UserInfo info;
112+
113+
public User() {
114+
}
115+
116+
public User(long id, String name, UserInfo info) {
117+
this.id = id;
118+
this.name = name;
119+
this.info = info;
120+
info.user = this;
121+
}
122+
123+
public long getId() {
124+
return id;
125+
}
126+
127+
public String getName() {
128+
return name;
129+
}
130+
131+
public UserInfo getInfo() {
132+
return info;
133+
}
134+
}
135+
136+
@Entity(name = "UserInfo")
137+
public static class UserInfo {
138+
@Id
139+
@GeneratedValue
140+
private Long id;
141+
142+
@OneToOne(mappedBy = "info", fetch = FetchType.LAZY)
143+
private User user;
144+
145+
private String info;
146+
147+
@OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
148+
private List<Phone> phoneList;
149+
150+
public long getId() {
151+
return id;
152+
}
153+
154+
public UserInfo() {
155+
}
156+
157+
public UserInfo(String info) {
158+
this.info = info;
159+
}
160+
161+
public User getUser() {
162+
return user;
163+
}
164+
165+
public String getInfo() {
166+
return info;
167+
}
168+
169+
public List<Phone> getPhoneList() {
170+
return phoneList;
171+
}
172+
173+
public void addPhone(Phone phone) {
174+
if ( phoneList == null ) {
175+
phoneList = new ArrayList<>();
176+
}
177+
this.phoneList.add( phone );
178+
phone.info = this;
179+
}
180+
}
181+
182+
@Entity(name = "Phone")
183+
public static class Phone {
184+
@Id
185+
@Column(name = "PHONE_NUMBER")
186+
private String number;
187+
188+
@ManyToOne
189+
@JoinColumn(name = "INFO_ID")
190+
private UserInfo info;
191+
192+
public Phone() {
193+
}
194+
195+
public Phone(String number) {
196+
this.number = number;
197+
}
198+
199+
public String getNumber() {
200+
return number;
201+
}
202+
203+
public UserInfo getInfo() {
204+
return info;
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)