HHH-12297 - Relations are not loaded when using Fetch Profiles

This commit is contained in:
Ladislav Kulhanek 2018-02-16 09:08:20 +01:00 committed by Vlad Mihalcea
parent 229839b14a
commit 93c475f7e2
7 changed files with 666 additions and 26 deletions

View File

@ -15,8 +15,11 @@ import org.hibernate.LockMode;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.cache.spi.access.EntityRegionAccessStrategy;
import org.hibernate.cache.spi.entry.CacheEntry;
import org.hibernate.engine.profile.Fetch;
import org.hibernate.engine.profile.FetchProfile;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SessionEventListenerManager;
import org.hibernate.engine.spi.SessionFactoryImplementor;
@ -144,9 +147,12 @@ public final class TwoPhaseLoad {
);
}
String entityName = persister.getEntityName();
String[] propertyNames = persister.getPropertyNames();
final Type[] types = persister.getPropertyTypes();
for ( int i = 0; i < hydratedState.length; i++ ) {
final Object value = hydratedState[i];
Boolean overridingEager = getOverridingEager( session, entityName, propertyNames[i], types[i] );
if ( value == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
// IMPLEMENTATION NOTE: This is a lazy property on a bytecode-enhanced entity.
// hydratedState[i] needs to remain LazyPropertyInitializer.UNFETCHED_PROPERTY so that
@ -157,12 +163,12 @@ public final class TwoPhaseLoad {
// HHH-10989: We need to resolve the collection so that a CollectionReference is added to StatefulPersistentContext.
// As mentioned above, hydratedState[i] needs to remain LazyPropertyInitializer.UNFETCHED_PROPERTY
// so do not assign the resolved, unitialized PersistentCollection back to hydratedState[i].
types[i].resolve( value, session, entity );
types[i].resolve( value, session, entity, overridingEager );
}
}
else if ( value!= PropertyAccessStrategyBackRefImpl.UNKNOWN ) {
else if ( value != PropertyAccessStrategyBackRefImpl.UNKNOWN ) {
// we know value != LazyPropertyInitializer.UNFETCHED_PROPERTY
hydratedState[i] = types[i].resolve( value, session, entity );
hydratedState[i] = types[i].resolve( value, session, entity, overridingEager );
}
}
@ -288,7 +294,54 @@ public final class TwoPhaseLoad {
factory.getStatistics().loadEntity( persister.getEntityName() );
}
}
/**
* Check if eager of the association is overriden by anything.
*
* @param session session
* @param entityName entity name
* @param associationName association name
*
* @return null if there is no overriding, true if it is overridden to eager and false if it is overridden to lazy
*/
private static Boolean getOverridingEager(
SharedSessionContractImplementor session,
String entityName,
String associationName,
Type type) {
if ( type.isAssociationType() || type.isCollectionType() ) {
Boolean overridingEager = isEagerFetchProfile( session, entityName + "." + associationName );
if ( LOG.isDebugEnabled() ) {
if ( overridingEager != null ) {
LOG.debugf(
"Overriding eager fetching using active fetch profile. EntityName: %s, associationName: %s, eager fetching: %s",
entityName,
associationName,
overridingEager
);
}
}
return overridingEager;
}
return null;
}
private static Boolean isEagerFetchProfile(SharedSessionContractImplementor session, String role) {
LoadQueryInfluencers loadQueryInfluencers = session.getLoadQueryInfluencers();
for ( String fetchProfileName : loadQueryInfluencers.getEnabledFetchProfileNames() ) {
FetchProfile fp = session.getFactory().getFetchProfile( fetchProfileName );
Fetch fetch = fp.getFetchByRole( role );
if ( fetch != null && Fetch.Style.JOIN == fetch.getStyle() ) {
return true;
}
}
return null;
}
/**
* PostLoad cannot occur during initializeEntity, as that call occurs *before*
* the Set collections are added to the persistence context by Loader.
@ -296,7 +349,7 @@ public final class TwoPhaseLoad {
* postLoad if it acts upon the collection.
*
* HHH-6043
*
*
* @param entity The entity
* @param session The Session
* @param postLoadEvent The (re-used) post-load event
@ -305,7 +358,7 @@ public final class TwoPhaseLoad {
final Object entity,
final SharedSessionContractImplementor session,
final PostLoadEvent postLoadEvent) {
if ( session.isEventSource() ) {
final PersistenceContext persistenceContext
= session.getPersistenceContext();

View File

@ -286,7 +286,7 @@ public abstract class CollectionType extends AbstractType implements Association
final Serializable key = (Serializable) getPersister(session)
.getKeyType()
.assemble( cached, session, owner);
return resolveKey( key, session, owner );
return resolveKey( key, session, owner, null );
}
}
@ -452,14 +452,19 @@ public abstract class CollectionType extends AbstractType implements Association
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
throws HibernateException {
return resolveKey( getKeyOfOwner( owner, session ), session, owner );
return resolve(value, session, owner, null);
}
private Object resolveKey(Serializable key, SharedSessionContractImplementor session, Object owner) {
@Override
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner, Boolean overridingEager) throws HibernateException {
return resolveKey( getKeyOfOwner( owner, session ), session, owner, overridingEager );
}
private Object resolveKey(Serializable key, SharedSessionContractImplementor session, Object owner, Boolean overridingEager) {
// if (key==null) throw new AssertionFailure("owner identifier unknown when re-assembling
// collection reference");
return key == null ? null : // TODO: can this case really occur??
getCollection( key, session, owner );
getCollection( key, session, owner, overridingEager );
}
@Override
@ -745,7 +750,7 @@ public abstract class CollectionType extends AbstractType implements Association
* @param owner The collection owner
* @return The collection
*/
public Object getCollection(Serializable key, SharedSessionContractImplementor session, Object owner) {
public Object getCollection(Serializable key, SharedSessionContractImplementor session, Object owner, Boolean overridingEager) {
CollectionPersister persister = getPersister( session );
final PersistenceContext persistenceContext = session.getPersistenceContext();
@ -772,10 +777,11 @@ public abstract class CollectionType extends AbstractType implements Association
persistenceContext.addUninitializedCollection( persister, collection, key );
// some collections are not lazy:
boolean eager = overridingEager != null ? overridingEager : !persister.isLazy();
if ( initializeImmediately() ) {
session.initializeCollection( collection, false );
}
else if ( !persister.isLazy() ) {
else if ( eager ) {
persistenceContext.addNonLazyCollection( collection );
}

View File

@ -452,9 +452,14 @@ public abstract class EntityType extends AbstractType implements AssociationType
*/
@Override
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException {
return resolve(value, session, owner, null);
}
@Override
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner, Boolean overridingEager) throws HibernateException {
if ( value != null && !isNull( owner, session ) ) {
if ( isReferenceToPrimaryKey() ) {
return resolveIdentifier( (Serializable) value, session );
return resolveIdentifier( (Serializable) value, session, overridingEager );
}
else if ( uniqueKeyPropertyName != null ) {
return loadByUniqueKey( getAssociatedEntityName(), uniqueKeyPropertyName, value, session );
@ -664,11 +669,14 @@ public abstract class EntityType extends AbstractType implements AssociationType
*
* @throws org.hibernate.HibernateException Indicates problems performing the load.
*/
protected final Object resolveIdentifier(Serializable id, SharedSessionContractImplementor session) throws HibernateException {
protected final Object resolveIdentifier(Serializable id, SharedSessionContractImplementor session, Boolean overridingEager) throws HibernateException {
boolean isProxyUnwrapEnabled = unwrapProxy &&
getAssociatedEntityPersister( session.getFactory() )
.isInstrumented();
boolean eager = overridingEager != null ? overridingEager : this.eager;
Object proxyOrEntity = session.internalLoad(
getAssociatedEntityName(),
id,
@ -684,6 +692,10 @@ public abstract class EntityType extends AbstractType implements AssociationType
return proxyOrEntity;
}
protected final Object resolveIdentifier(Serializable id, SharedSessionContractImplementor session) throws HibernateException {
return resolveIdentifier( id, session, null );
}
protected boolean isNull(Object owner, SharedSessionContractImplementor session) {
return false;
}

View File

@ -460,23 +460,33 @@ public interface Type extends Serializable {
Object hydrate(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException;
/**
* @see #resolve(Object, SharedSessionContractImplementor, Object, Boolean)
*/
Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
throws HibernateException;
/**
* The second phase of 2-phase loading. Only really pertinent for entities and collections. Here we resolve the
* identifier to an entity or collection instance
*
*
* @param value an identifier or value returned by <tt>hydrate()</tt>
* @param owner the parent entity
* @param session the session
*
* @param overridingEager can override eager from the mapping. For example because of {@link org.hibernate.engine.spi.LoadQueryInfluencers}
* If null, then it does not override. If true or false then it overrides the mapping value.
*
* @return the given value, or the value associated with the identifier
*
* @throws HibernateException An error from Hibernate
*
* @see #hydrate
*/
Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
throws HibernateException;
default Object resolve(Object value, SharedSessionContractImplementor session, Object owner, Boolean overridingEager)
throws HibernateException {
return resolve(value, session, owner);
}
/**
* Given a hydrated, but unresolved value, return a value that may be used to reconstruct property-ref
* associations.

View File

@ -0,0 +1,280 @@
/*
* 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.test.fetchprofiles;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import org.hibernate.LazyInitializationException;
import org.hibernate.Session;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.FetchProfile;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Assert;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@TestForIssue( jiraKey = "HHH-12297")
public class CollectionLoadedInTwoPhaseLoadTest extends BaseCoreFunctionalTestCase {
// NOTE
// there are two fetch profiles because when I use only one the relation OrgUnit.people
// is missing in the fetch profile.
// It is missing because of logic in FetchProfile.addFetch(). Do not understand the implementation
// of the method now, so the workaround is to use two fetch profiles.
static final String FETCH_PROFILE_NAME = "fp1";
static final String FETCH_PROFILE_NAME_2 = "fp2";
private final String OU_1 = "ou_1";
private final String OU_2 = "ou_2";
private final String P_1 = "p_1";
private final String P_2 = "p_2";
public void configure(Configuration cfg) {
cfg.setProperty( Environment.GENERATE_STATISTICS, "true" );
}
@Test
public void testIfEverythingIsLoaded() {
createSampleData();
sessionFactory().getStatistics().clear();
try {
OrgUnit ou1 = this.loadOrgUnitWithFetchProfile( OU_1 );
Person p1 = ou1.findPerson( P_1 );
OrgUnit ou2 = p1.findOrgUnit( OU_2 );
Person p2 = ou2.findPerson( P_2 );
@SuppressWarnings( "unused" )
String email = p2.getEmail();
assertEquals( 4, sessionFactory().getStatistics().getEntityLoadCount() );
}
catch (LazyInitializationException e) {
fail( "Everything should be initialized" );
}
}
public OrgUnit loadOrgUnitWithFetchProfile(String groupId) {
return doInHibernate( this::sessionFactory, session -> {
session.enableFetchProfile( FETCH_PROFILE_NAME );
session.enableFetchProfile( FETCH_PROFILE_NAME_2 );
return session.get( OrgUnit.class, groupId );
} );
}
private void createSampleData() {
doInHibernate( this::sessionFactory, session -> {
OrgUnit ou1 = new OrgUnit( OU_1, "org unit one" );
OrgUnit ou2 = new OrgUnit( OU_2, "org unit two" );
Person p1 = new Person( P_1, "p1@coompany.com" );
Person p2 = new Person( P_2, "p2@company.com" );
ou1.addPerson( p1 );
ou2.addPerson( p1 );
ou2.addPerson( p2 );
session.persist( ou1 );
} );
}
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Person.class,
OrgUnit.class
};
}
@Entity(name = "OrgUnit")
@FetchProfile(name = FETCH_PROFILE_NAME, fetchOverrides = {
@FetchProfile.FetchOverride(entity = OrgUnit.class, association = "people", mode = FetchMode.JOIN)
})
public static class OrgUnit {
@Id
private String name;
private String description;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "orgUnits", cascade = CascadeType.PERSIST)
private List<Person> people = new ArrayList<>();
public OrgUnit() {
}
public OrgUnit(String name, String description) {
this.name = name;
this.description = description;
}
public Person findPerson(String personName) {
if (people == null) {
return null;
}
for ( Person person : people ) {
if (person.getName().equals( personName )) return person;
}
return null;
}
public void addPerson(Person person) {
if (people.contains( person )) return;
people.add(person);
person.addOrgUnit( this);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Person> getPeople() {
return people;
}
public void setPeople(List<Person> people) {
this.people = people;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
OrgUnit group = (OrgUnit) o;
return Objects.equals( name, group.name );
}
@Override
public int hashCode() {
return Objects.hash( name );
}
@Override
public String toString() {
return "OrgUnit{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
@Entity(name = "Person")
@FetchProfile(name = FETCH_PROFILE_NAME_2, fetchOverrides = {
@FetchProfile.FetchOverride(entity = Person.class, association = "orgUnits", mode = FetchMode.JOIN)
})
public static class Person {
@Id
private String name;
private String email;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<OrgUnit> orgUnits = new ArrayList<>();
public Person() {
}
public Person(String name, String email) {
this.name = name;
this.email = email;
}
public OrgUnit findOrgUnit(String orgUnitName) {
if ( orgUnits == null) {
return null;
}
for ( OrgUnit orgUnit : orgUnits ) {
if (orgUnit.getName().equals( orgUnitName )) return orgUnit;
}
return null;
}
public void addOrgUnit(OrgUnit orgUnit) {
if ( orgUnits.contains( orgUnit)) return;
orgUnits.add( orgUnit);
orgUnit.addPerson(this);
}
public List<OrgUnit> getOrgUnits() {
return orgUnits;
}
public void setOrgUnits(List<OrgUnit> orgUnits) {
this.orgUnits = orgUnits;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( name, person.name );
}
@Override
public int hashCode() {
return Objects.hash( name );
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
}

View File

@ -0,0 +1,284 @@
/*
* 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.test.fetchprofiles;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.LazyInitializationException;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.FetchProfile;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import static com.sun.tools.internal.ws.wsdl.parser.Util.fail;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
@TestForIssue( jiraKey = "HHH-12297")
public class EntityLoadedInTwoPhaseLoadTest extends BaseCoreFunctionalTestCase {
static final String FETCH_PROFILE_NAME = "fp1";
public void configure(Configuration cfg) {
cfg.setProperty( Environment.GENERATE_STATISTICS, "true" );
}
@Test
public void testIfAllRelationsAreInitialized() {
long startId = this.createSampleData();
sessionFactory().getStatistics().clear();
try {
Start start = this.loadStartWithFetchProfile( startId );
@SuppressWarnings( "unused" )
String value = start.getVia2().getMid().getFinish().getValue();
assertEquals( 4, sessionFactory().getStatistics().getEntityLoadCount() );
}
catch (LazyInitializationException e) {
fail( "Everything should be initialized" );
}
}
public Start loadStartWithFetchProfile(long startId) {
return doInHibernate( this::sessionFactory, session -> {
session.enableFetchProfile( FETCH_PROFILE_NAME );
return session.get( Start.class, startId );
} );
}
private long createSampleData() {
return doInHibernate( this::sessionFactory, session -> {
Finish finish = new Finish( "foo" );
Mid mid = new Mid( finish );
Via2 via2 = new Via2( mid );
Start start = new Start( null, via2 );
session.persist( start );
return start.getId();
} );
}
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Start.class,
Mid.class,
Finish.class,
Via1.class,
Via2.class
};
}
@Entity(name = "Finish")
public static class Finish {
@Id
@GeneratedValue
private long id;
@Column(name = "value", nullable = false)
private String value;
public Finish() {
}
public Finish(String value) {
this.value = value;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
@Entity(name = "Mid")
@FetchProfile(name = FETCH_PROFILE_NAME, fetchOverrides = {
@FetchProfile.FetchOverride(entity = Mid.class, association = "finish", mode = FetchMode.JOIN)
})
public static class Mid {
@Id
@GeneratedValue
private long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Finish finish;
public Mid() {
}
public Mid(Finish finish) {
this.finish = finish;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Finish getFinish() {
return finish;
}
public void setFinish(Finish finish) {
this.finish = finish;
}
}
@Entity(name = "Start")
@FetchProfile(name = FETCH_PROFILE_NAME, fetchOverrides = {
@FetchProfile.FetchOverride(entity = Start.class, association = "via1", mode = FetchMode.JOIN),
@FetchProfile.FetchOverride(entity = Start.class, association = "via2", mode = FetchMode.JOIN)
})
public static class Start {
@Id
@GeneratedValue
private long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Via1 via1;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Via2 via2;
public Start() {
}
public Start(Via1 via1, Via2 via2) {
this.via1 = via1;
this.via2 = via2;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Via1 getVia1() {
return via1;
}
public void setVia1(Via1 via1) {
this.via1 = via1;
}
public Via2 getVia2() {
return via2;
}
public void setVia2(Via2 via2) {
this.via2 = via2;
}
}
@Entity(name = "Via1")
@FetchProfile(name = FETCH_PROFILE_NAME, fetchOverrides = {
@FetchProfile.FetchOverride(entity = Via1.class, association = "mid", mode = FetchMode.JOIN)
})
public static class Via1 {
@Id
@GeneratedValue
private long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Mid mid;
public Via1() {
}
public Via1(Mid mid) {
this.mid = mid;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Mid getMid() {
return mid;
}
public void setMid(Mid mid) {
this.mid = mid;
}
}
@Entity(name = "Via2")
@FetchProfile(name = FETCH_PROFILE_NAME, fetchOverrides = {
@FetchProfile.FetchOverride(entity = Via2.class, association = "mid", mode = FetchMode.JOIN)
})
public static class Via2 {
@Id
@GeneratedValue
private long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Mid mid;
public Via2() {
}
public Via2(Mid mid) {
this.mid = mid;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Mid getMid() {
return mid;
}
public void setMid(Mid mid) {
this.mid = mid;
}
}
}

View File

@ -275,11 +275,6 @@ public class JoinFetchProfileTest extends BaseCoreFunctionalTestCase {
);
}
/**
* fetch-profiles should have no effect what-so-ever on the direct results of the HQL query.
*
* TODO : this is actually not strictly true. what we should have happen is to subsequently load those fetches
*/
@Test
public void testHQL() {
performWithStandardData(
@ -292,8 +287,8 @@ public class JoinFetchProfileTest extends BaseCoreFunctionalTestCase {
List sections = session.createQuery( "from CourseOffering" ).list();
int sectionCount = sections.size();
assertEquals( "unexpected CourseOffering count", 1, sectionCount );
assertEquals( 1, sessionFactory().getStatistics().getEntityLoadCount() );
assertEquals( 0, sessionFactory().getStatistics().getEntityFetchCount() );
assertEquals( 4, sessionFactory().getStatistics().getEntityLoadCount() );
assertEquals( 2, sessionFactory().getStatistics().getEntityFetchCount() );
session.getTransaction().commit();
session.close();
}