HHH-14212 fix Fetch Graph by simply returning false in TwoPhaseLoad#getOverridingEager() when Fetch Graph is being enforced

This commit is contained in:
Nathan Xu 2020-09-13 09:43:17 -04:00 committed by Christian Beikov
parent 39b42c0a6a
commit 99a4edfac0
5 changed files with 217 additions and 2 deletions

View File

@ -406,7 +406,11 @@ public final class TwoPhaseLoad {
} }
/** /**
* Check if eager of the association is overridden by anything. * Check if eager of the association is overridden (i.e. skipping metamodel strategy), including (order sensitive):
* <ol>
* <li>fetch graph</li>
* <li>fetch profile</li>
* </ol>
* *
* @param session session * @param session session
* @param entityName entity name * @param entityName entity name
@ -424,6 +428,12 @@ public final class TwoPhaseLoad {
// Performance: check type.isCollectionType() first, as type.isAssociationType() is megamorphic // Performance: check type.isCollectionType() first, as type.isAssociationType() is megamorphic
if ( associationType.isCollectionType() || associationType.isAssociationType() ) { if ( associationType.isCollectionType() || associationType.isAssociationType() ) {
// we can return false invariably for if the entity has been covered by entity graph,
// its associated JOIN has been present in the SQL generated and hence it would be loaded anyway
if ( session.isEnforcingFetchGraph() ) {
return false;
}
// check 'fetch profile' next; skip 'metamodel' if 'fetch profile' takes effect // check 'fetch profile' next; skip 'metamodel' if 'fetch profile' takes effect
final Boolean overridingEager = isEagerFetchProfile( session, entityName, associationName ); final Boolean overridingEager = isEagerFetchProfile( session, entityName, associationName );

View File

@ -522,4 +522,11 @@ public interface SharedSessionContractImplementor
*/ */
PersistenceContext getPersistenceContextInternal(); PersistenceContext getPersistenceContextInternal();
default boolean isEnforcingFetchGraph() {
return false;
}
default void setEnforcingFetchGraph(boolean enforcingFetchGraph) {
}
} }

View File

@ -215,6 +215,8 @@ public class SessionImpl
private transient TransactionObserver transactionObserver; private transient TransactionObserver transactionObserver;
private transient boolean isEnforcingFetchGraph;
public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) {
super( factory, options ); super( factory, options );
@ -3313,6 +3315,10 @@ public class SessionImpl
loadAccess.with( lockOptions ); loadAccess.with( lockOptions );
} }
if ( getLoadQueryInfluencers().getEffectiveEntityGraph().getSemantic() == GraphSemantic.FETCH ) {
setEnforcingFetchGraph( true );
}
return loadAccess.load( (Serializable) primaryKey ); return loadAccess.load( (Serializable) primaryKey );
} }
catch ( EntityNotFoundException ignored ) { catch ( EntityNotFoundException ignored ) {
@ -3357,6 +3363,7 @@ public class SessionImpl
finally { finally {
getLoadQueryInfluencers().getEffectiveEntityGraph().clear(); getLoadQueryInfluencers().getEffectiveEntityGraph().clear();
getLoadQueryInfluencers().setReadOnly( null ); getLoadQueryInfluencers().setReadOnly( null );
setEnforcingFetchGraph( false );
} }
} }
@ -3785,4 +3792,15 @@ public class SessionImpl
} }
return readOnly; return readOnly;
} }
@Override
public boolean isEnforcingFetchGraph() {
return this.isEnforcingFetchGraph;
}
@Override
public void setEnforcingFetchGraph(boolean isEnforcingFetchGraph) {
this.isEnforcingFetchGraph = isEnforcingFetchGraph;
}
} }

View File

@ -1437,6 +1437,9 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
sessionCacheMode = getProducer().getCacheMode(); sessionCacheMode = getProducer().getCacheMode();
getProducer().setCacheMode( effectiveCacheMode ); getProducer().setCacheMode( effectiveCacheMode );
} }
if ( entityGraphQueryHint != null && entityGraphQueryHint.getSemantic() == GraphSemantic.FETCH ) {
getProducer().setEnforcingFetchGraph( true );
}
} }
protected void afterQuery() { protected void afterQuery() {
@ -1448,6 +1451,7 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
getProducer().setCacheMode( sessionCacheMode ); getProducer().setCacheMode( sessionCacheMode );
sessionCacheMode = null; sessionCacheMode = null;
} }
getProducer().setEnforcingFetchGraph( false );
} }
@Override @Override

View File

@ -0,0 +1,176 @@
package org.hibernate.jpa.test.graphs;
import java.util.Arrays;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EntityGraph;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.graph.GraphParser;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.TestForIssue;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* @author Yaroslav Prokipchyn
* @author Nathan Xu
*/
@TestForIssue( jiraKey = "HHH-14212" )
public class FetchGraphTest extends BaseEntityManagerFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
LedgerRecord.class,
LedgerRecordItem.class,
BudgetRecord.class,
Trigger.class,
FinanceEntity.class
};
}
@Before
public void setUp() {
doInJPA( this::entityManagerFactory, entityManager -> {
Trigger trigger = new Trigger();
entityManager.persist( trigger );
BudgetRecord budgetRecord = new BudgetRecord();
budgetRecord.amount = 100;
budgetRecord.trigger = trigger;
entityManager.persist( budgetRecord );
FinanceEntity client = new FinanceEntity();
client.name = "client";
FinanceEntity vendor = new FinanceEntity();
vendor.name = "vendor";
entityManager.persist( client );
entityManager.persist( vendor );
LedgerRecordItem item1 = new LedgerRecordItem();
item1.financeEntity = client;
LedgerRecordItem item2 = new LedgerRecordItem();
item2.financeEntity = vendor;
entityManager.persist( item1 );
entityManager.persist( item2 );
LedgerRecord ledgerRecord = new LedgerRecord();
ledgerRecord.budgetRecord = budgetRecord;
ledgerRecord.trigger = trigger;
ledgerRecord.ledgerRecordItems= Arrays.asList( item1, item2 );
item1.ledgerRecord = ledgerRecord;
item2.ledgerRecord = ledgerRecord;
entityManager.persist( ledgerRecord );
} );
}
@Test
public void testCollectionEntityGraph() {
doInJPA( this::entityManagerFactory, entityManager -> {
final EntityGraph<LedgerRecord> entityGraph = GraphParser.parse( LedgerRecord.class, "budgetRecord, ledgerRecordItems.value(financeEntity)", entityManager );
final List<LedgerRecord> records = entityManager.createQuery( "from LedgerRecord", LedgerRecord.class )
.setHint( GraphSemantic.FETCH.getJpaHintName(), entityGraph )
.getResultList();
assertThat( records.size(), is( 1 ) );
records.forEach( record -> {
assertFalse( Hibernate.isInitialized( record.trigger ) );
assertTrue( Hibernate.isInitialized( record.budgetRecord ) );
assertFalse( Hibernate.isInitialized( record.budgetRecord.trigger ) );
assertTrue( Hibernate.isInitialized( record.ledgerRecordItems) );
assertThat( record.ledgerRecordItems.size(), is( 2 ) );
record.ledgerRecordItems.forEach( item -> {
assertSame( record, item.ledgerRecord );
assertTrue( Hibernate.isInitialized( item.financeEntity ) );
} );
} );
} );
}
@Entity(name = "LedgerRecord")
@Table(name = "LedgerRecord")
static class LedgerRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
@ManyToOne
BudgetRecord budgetRecord;
@OneToMany(mappedBy = "ledgerRecord", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch(FetchMode.SUBSELECT)
List<LedgerRecordItem> ledgerRecordItems;
@ManyToOne
Trigger trigger;
}
@Entity(name = "LedgerRecordItem")
@Table(name = "LedgerRecordItem")
static class LedgerRecordItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
@ManyToOne
LedgerRecord ledgerRecord;
@ManyToOne
FinanceEntity financeEntity;
}
@Entity(name = "BudgetRecord")
@Table(name = "BudgetRecord")
static class BudgetRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
int amount;
@ManyToOne
Trigger trigger;
}
@Entity(name = "Trigger")
@Table(name = "Trigger")
static class Trigger {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
String name;
}
@Entity(name = "FinanceEntity")
@Table(name = "FinanceEntity")
static class FinanceEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
String name;
}
}