Skip to content

Commit 99a4edf

Browse files
NathanQingyangXubeikov
authored andcommitted
HHH-14212 fix Fetch Graph by simply returning false in TwoPhaseLoad#getOverridingEager() when Fetch Graph is being enforced
1 parent 39b42c0 commit 99a4edf

File tree

5 files changed

+217
-2
lines changed

5 files changed

+217
-2
lines changed

hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public static void initializeEntityEntryLoadedState(
204204
String entityName = persister.getEntityName();
205205
String[] propertyNames = persister.getPropertyNames();
206206
final Type[] types = persister.getPropertyTypes();
207-
207+
208208
for ( int i = 0; i < hydratedState.length; i++ ) {
209209
final Object value = hydratedState[i];
210210
if ( debugEnabled ) {
@@ -406,7 +406,11 @@ public static void afterInitialize(
406406
}
407407

408408
/**
409-
* Check if eager of the association is overridden by anything.
409+
* Check if eager of the association is overridden (i.e. skipping metamodel strategy), including (order sensitive):
410+
* <ol>
411+
* <li>fetch graph</li>
412+
* <li>fetch profile</li>
413+
* </ol>
410414
*
411415
* @param session session
412416
* @param entityName entity name
@@ -424,6 +428,12 @@ private static Boolean getOverridingEager(
424428
// Performance: check type.isCollectionType() first, as type.isAssociationType() is megamorphic
425429
if ( associationType.isCollectionType() || associationType.isAssociationType() ) {
426430

431+
// we can return false invariably for if the entity has been covered by entity graph,
432+
// its associated JOIN has been present in the SQL generated and hence it would be loaded anyway
433+
if ( session.isEnforcingFetchGraph() ) {
434+
return false;
435+
}
436+
427437
// check 'fetch profile' next; skip 'metamodel' if 'fetch profile' takes effect
428438
final Boolean overridingEager = isEagerFetchProfile( session, entityName, associationName );
429439

hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,11 @@ <T> QueryImplementor<T> createQuery(
522522
*/
523523
PersistenceContext getPersistenceContextInternal();
524524

525+
default boolean isEnforcingFetchGraph() {
526+
return false;
527+
}
528+
529+
default void setEnforcingFetchGraph(boolean enforcingFetchGraph) {
530+
}
531+
525532
}

hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ public class SessionImpl
215215

216216
private transient TransactionObserver transactionObserver;
217217

218+
private transient boolean isEnforcingFetchGraph;
219+
218220
public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) {
219221
super( factory, options );
220222

@@ -3312,6 +3314,10 @@ public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode
33123314
lockOptions = buildLockOptions( lockModeType, properties );
33133315
loadAccess.with( lockOptions );
33143316
}
3317+
3318+
if ( getLoadQueryInfluencers().getEffectiveEntityGraph().getSemantic() == GraphSemantic.FETCH ) {
3319+
setEnforcingFetchGraph( true );
3320+
}
33153321

33163322
return loadAccess.load( (Serializable) primaryKey );
33173323
}
@@ -3357,6 +3363,7 @@ public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode
33573363
finally {
33583364
getLoadQueryInfluencers().getEffectiveEntityGraph().clear();
33593365
getLoadQueryInfluencers().setReadOnly( null );
3366+
setEnforcingFetchGraph( false );
33603367
}
33613368
}
33623369

@@ -3785,4 +3792,15 @@ private Boolean getReadOnlyFromLoadQueryInfluencers() {
37853792
}
37863793
return readOnly;
37873794
}
3795+
3796+
@Override
3797+
public boolean isEnforcingFetchGraph() {
3798+
return this.isEnforcingFetchGraph;
3799+
}
3800+
3801+
@Override
3802+
public void setEnforcingFetchGraph(boolean isEnforcingFetchGraph) {
3803+
this.isEnforcingFetchGraph = isEnforcingFetchGraph;
3804+
}
3805+
37883806
}

hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,9 @@ protected void beforeQuery() {
14371437
sessionCacheMode = getProducer().getCacheMode();
14381438
getProducer().setCacheMode( effectiveCacheMode );
14391439
}
1440+
if ( entityGraphQueryHint != null && entityGraphQueryHint.getSemantic() == GraphSemantic.FETCH ) {
1441+
getProducer().setEnforcingFetchGraph( true );
1442+
}
14401443
}
14411444

14421445
protected void afterQuery() {
@@ -1448,6 +1451,7 @@ protected void afterQuery() {
14481451
getProducer().setCacheMode( sessionCacheMode );
14491452
sessionCacheMode = null;
14501453
}
1454+
getProducer().setEnforcingFetchGraph( false );
14511455
}
14521456

14531457
@Override
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package org.hibernate.jpa.test.graphs;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
import javax.persistence.CascadeType;
6+
import javax.persistence.Entity;
7+
import javax.persistence.EntityGraph;
8+
import javax.persistence.FetchType;
9+
import javax.persistence.GeneratedValue;
10+
import javax.persistence.GenerationType;
11+
import javax.persistence.Id;
12+
import javax.persistence.ManyToOne;
13+
import javax.persistence.OneToMany;
14+
import javax.persistence.Table;
15+
16+
import org.hibernate.Hibernate;
17+
import org.hibernate.annotations.Fetch;
18+
import org.hibernate.annotations.FetchMode;
19+
import org.hibernate.graph.GraphParser;
20+
import org.hibernate.graph.GraphSemantic;
21+
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
22+
23+
import org.hibernate.testing.TestForIssue;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
import static org.hamcrest.CoreMatchers.is;
28+
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
29+
import static org.junit.Assert.assertFalse;
30+
import static org.junit.Assert.assertSame;
31+
import static org.junit.Assert.assertThat;
32+
import static org.junit.Assert.assertTrue;
33+
34+
/**
35+
* @author Yaroslav Prokipchyn
36+
* @author Nathan Xu
37+
*/
38+
@TestForIssue( jiraKey = "HHH-14212" )
39+
public class FetchGraphTest extends BaseEntityManagerFunctionalTestCase {
40+
41+
@Override
42+
protected Class<?>[] getAnnotatedClasses() {
43+
return new Class<?>[] {
44+
LedgerRecord.class,
45+
LedgerRecordItem.class,
46+
BudgetRecord.class,
47+
Trigger.class,
48+
FinanceEntity.class
49+
};
50+
}
51+
52+
@Before
53+
public void setUp() {
54+
doInJPA( this::entityManagerFactory, entityManager -> {
55+
Trigger trigger = new Trigger();
56+
entityManager.persist( trigger );
57+
58+
BudgetRecord budgetRecord = new BudgetRecord();
59+
budgetRecord.amount = 100;
60+
budgetRecord.trigger = trigger;
61+
entityManager.persist( budgetRecord );
62+
63+
FinanceEntity client = new FinanceEntity();
64+
client.name = "client";
65+
FinanceEntity vendor = new FinanceEntity();
66+
vendor.name = "vendor";
67+
entityManager.persist( client );
68+
entityManager.persist( vendor );
69+
70+
LedgerRecordItem item1 = new LedgerRecordItem();
71+
item1.financeEntity = client;
72+
LedgerRecordItem item2 = new LedgerRecordItem();
73+
item2.financeEntity = vendor;
74+
entityManager.persist( item1 );
75+
entityManager.persist( item2 );
76+
77+
LedgerRecord ledgerRecord = new LedgerRecord();
78+
ledgerRecord.budgetRecord = budgetRecord;
79+
ledgerRecord.trigger = trigger;
80+
ledgerRecord.ledgerRecordItems= Arrays.asList( item1, item2 );
81+
82+
item1.ledgerRecord = ledgerRecord;
83+
item2.ledgerRecord = ledgerRecord;
84+
85+
entityManager.persist( ledgerRecord );
86+
} );
87+
}
88+
89+
@Test
90+
public void testCollectionEntityGraph() {
91+
doInJPA( this::entityManagerFactory, entityManager -> {
92+
final EntityGraph<LedgerRecord> entityGraph = GraphParser.parse( LedgerRecord.class, "budgetRecord, ledgerRecordItems.value(financeEntity)", entityManager );
93+
final List<LedgerRecord> records = entityManager.createQuery( "from LedgerRecord", LedgerRecord.class )
94+
.setHint( GraphSemantic.FETCH.getJpaHintName(), entityGraph )
95+
.getResultList();
96+
assertThat( records.size(), is( 1 ) );
97+
records.forEach( record -> {
98+
assertFalse( Hibernate.isInitialized( record.trigger ) );
99+
assertTrue( Hibernate.isInitialized( record.budgetRecord ) );
100+
assertFalse( Hibernate.isInitialized( record.budgetRecord.trigger ) );
101+
assertTrue( Hibernate.isInitialized( record.ledgerRecordItems) );
102+
assertThat( record.ledgerRecordItems.size(), is( 2 ) );
103+
record.ledgerRecordItems.forEach( item -> {
104+
assertSame( record, item.ledgerRecord );
105+
assertTrue( Hibernate.isInitialized( item.financeEntity ) );
106+
} );
107+
} );
108+
} );
109+
}
110+
111+
@Entity(name = "LedgerRecord")
112+
@Table(name = "LedgerRecord")
113+
static class LedgerRecord {
114+
@Id
115+
@GeneratedValue(strategy = GenerationType.IDENTITY)
116+
Integer id;
117+
118+
@ManyToOne
119+
BudgetRecord budgetRecord;
120+
121+
@OneToMany(mappedBy = "ledgerRecord", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
122+
@Fetch(FetchMode.SUBSELECT)
123+
List<LedgerRecordItem> ledgerRecordItems;
124+
125+
@ManyToOne
126+
Trigger trigger;
127+
}
128+
129+
@Entity(name = "LedgerRecordItem")
130+
@Table(name = "LedgerRecordItem")
131+
static class LedgerRecordItem {
132+
@Id
133+
@GeneratedValue(strategy = GenerationType.IDENTITY)
134+
Integer id;
135+
136+
@ManyToOne
137+
LedgerRecord ledgerRecord;
138+
139+
@ManyToOne
140+
FinanceEntity financeEntity;
141+
}
142+
143+
@Entity(name = "BudgetRecord")
144+
@Table(name = "BudgetRecord")
145+
static class BudgetRecord {
146+
@Id
147+
@GeneratedValue(strategy = GenerationType.IDENTITY)
148+
Integer id;
149+
150+
int amount;
151+
152+
@ManyToOne
153+
Trigger trigger;
154+
}
155+
156+
@Entity(name = "Trigger")
157+
@Table(name = "Trigger")
158+
static class Trigger {
159+
@Id
160+
@GeneratedValue(strategy = GenerationType.IDENTITY)
161+
Integer id;
162+
163+
String name;
164+
}
165+
166+
@Entity(name = "FinanceEntity")
167+
@Table(name = "FinanceEntity")
168+
static class FinanceEntity {
169+
@Id
170+
@GeneratedValue(strategy = GenerationType.IDENTITY)
171+
Integer id;
172+
173+
String name;
174+
}
175+
}
176+

0 commit comments

Comments
 (0)