HHH-16651 support all fetch styles in fetch profiles

Finally, after all these years!
This commit is contained in:
Gavin 2023-05-22 19:56:08 +02:00 committed by Gavin King
parent 4795b94f68
commit a5ae1a479a
9 changed files with 194 additions and 54 deletions

View File

@ -22,7 +22,6 @@ import org.hibernate.annotations.ConverterRegistration;
import org.hibernate.annotations.ConverterRegistrations; import org.hibernate.annotations.ConverterRegistrations;
import org.hibernate.annotations.EmbeddableInstantiatorRegistration; import org.hibernate.annotations.EmbeddableInstantiatorRegistration;
import org.hibernate.annotations.EmbeddableInstantiatorRegistrations; import org.hibernate.annotations.EmbeddableInstantiatorRegistrations;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.FetchProfile; import org.hibernate.annotations.FetchProfile;
import org.hibernate.annotations.FetchProfile.FetchOverride; import org.hibernate.annotations.FetchProfile.FetchOverride;
import org.hibernate.annotations.FetchProfiles; import org.hibernate.annotations.FetchProfiles;
@ -853,9 +852,7 @@ public final class AnnotationBinder {
final String name = fetchProfile.name(); final String name = fetchProfile.name();
if ( reuseOrCreateFetchProfile( context, name ) ) { if ( reuseOrCreateFetchProfile( context, name ) ) {
for ( FetchOverride fetch : fetchProfile.fetchOverrides() ) { for ( FetchOverride fetch : fetchProfile.fetchOverrides() ) {
if ( fetch.mode() != FetchMode.JOIN ) { // TODO: validate which modes are valid where
throw new MappingException( "Only 'FetchMode.JOIN' is currently supported" );
}
context.getMetadataCollector() context.getMetadataCollector()
.addSecondPass( new FetchOverrideSecondPass( name, fetch, context ) ); .addSecondPass( new FetchOverrideSecondPass( name, fetch, context ) );
} }

View File

@ -1486,6 +1486,10 @@ public abstract class CollectionBinder {
setHibernateFetchMode( fetch.value() ); setHibernateFetchMode( fetch.value() );
result = true; result = true;
} }
else {
buildingContext.getMetadataCollector()
.addSecondPass( new FetchSecondPass( fetch, propertyHolder, propertyName, buildingContext ) );
}
} }
return result; return result;
} }

View File

@ -361,6 +361,11 @@ public class ToOneBinder {
setHibernateFetchMode( toOne, property, fetch.value() ); setHibernateFetchMode( toOne, property, fetch.value() );
result = true; result = true;
} }
else {
final MetadataBuildingContext context = toOne.getBuildingContext();
context.getMetadataCollector()
.addSecondPass( new FetchSecondPass( fetch, propertyHolder, inferredData.getPropertyName(), context ) );
}
} }
return result; return result;
} }

View File

@ -6,6 +6,8 @@
*/ */
package org.hibernate.engine.profile; package org.hibernate.engine.profile;
import java.util.Locale;
/** /**
* Models an individual fetch override within a profile. * Models an individual fetch override within a profile.
* *
@ -35,31 +37,25 @@ public class Fetch {
} }
/** /**
* The type or style of fetch. For the moment we limit this to * The type or style of fetch.
* join and select, though technically subselect would be valid
* here as well; however, to support subselect here would
* require major changes to the subselect loading code (which is
* needed for other things as well anyway).
*/ */
public enum Style { public enum Style {
/** /**
* Fetch via a join * Fetch via a join
*/ */
JOIN( "join" ), JOIN,
/** /**
* Fetch via a subsequent select * Fetch via a subsequent select
*/ */
SELECT( "select" ); SELECT,
/**
private final String name; * Fetch via a subsequent subselect
*/
Style(String name) { SUBSELECT;
this.name = name;
}
@Override @Override
public String toString() { public String toString() {
return name; return name().toLowerCase(Locale.ROOT);
} }
/** /**
@ -70,15 +66,14 @@ public class Fetch {
* @return The style; {@link #JOIN} is returned if not recognized * @return The style; {@link #JOIN} is returned if not recognized
*/ */
public static Style parse(String name) { public static Style parse(String name) {
if ( SELECT.name.equals( name ) ) { for ( Style style: values() ) {
return SELECT; if ( style.name().equalsIgnoreCase( name ) ) {
return style;
}
} }
else {
// the default...
return JOIN; return JOIN;
} }
} }
}
@Override @Override
public String toString() { public String toString() {

View File

@ -167,4 +167,9 @@ public class FetchProfile {
public boolean isContainsJoinFetchedBag() { public boolean isContainsJoinFetchedBag() {
return containsJoinFetchedBag; return containsJoinFetchedBag;
} }
@Override
public String toString() {
return "FetchProfile[" + name + "]";
}
} }

View File

@ -17,12 +17,17 @@ import java.util.function.Supplier;
import org.hibernate.Filter; import org.hibernate.Filter;
import org.hibernate.UnknownProfileException; import org.hibernate.UnknownProfileException;
import org.hibernate.engine.profile.Fetch;
import org.hibernate.engine.profile.FetchProfile;
import org.hibernate.internal.FilterImpl; import org.hibernate.internal.FilterImpl;
import org.hibernate.internal.SessionCreationOptions; import org.hibernate.internal.SessionCreationOptions;
import org.hibernate.loader.ast.spi.CascadingFetchProfile; import org.hibernate.loader.ast.spi.CascadingFetchProfile;
import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import static java.util.Collections.emptySet;
import static org.hibernate.engine.profile.Fetch.Style.SUBSELECT;
/** /**
* Centralize all options which can influence the SQL query needed to load an * Centralize all options which can influence the SQL query needed to load an
* entity. Currently, such influencers are defined as:<ul> * entity. Currently, such influencers are defined as:<ul>
@ -157,7 +162,7 @@ public class LoadQueryInfluencers implements Serializable {
*/ */
public Set<String> getEnabledFilterNames() { public Set<String> getEnabledFilterNames() {
if ( enabledFilters == null ) { if ( enabledFilters == null ) {
return Collections.emptySet(); return emptySet();
} }
else { else {
return Collections.unmodifiableSet( enabledFilters.keySet() ); return Collections.unmodifiableSet( enabledFilters.keySet() );
@ -221,7 +226,7 @@ public class LoadQueryInfluencers implements Serializable {
} }
public Set<String> getEnabledFetchProfileNames() { public Set<String> getEnabledFetchProfileNames() {
return Objects.requireNonNullElse( enabledFetchProfileNames, Collections.emptySet() ); return Objects.requireNonNullElse( enabledFetchProfileNames, emptySet() );
} }
private void checkFetchProfileName(String name) { private void checkFetchProfileName(String name) {
@ -302,7 +307,24 @@ public class LoadQueryInfluencers implements Serializable {
} }
public boolean effectiveSubselectFetchEnabled(CollectionPersister persister) { public boolean effectiveSubselectFetchEnabled(CollectionPersister persister) {
return subselectFetchEnabled || persister.isSubselectLoadable(); return subselectFetchEnabled
|| persister.isSubselectLoadable()
|| isSubselectFetchEnabledInProfile( persister );
}
private boolean isSubselectFetchEnabledInProfile(CollectionPersister persister) {
if ( hasEnabledFetchProfiles() ) {
for ( String profile : getEnabledFetchProfileNames() ) {
final FetchProfile fetchProfile = sessionFactory.getFetchProfile( profile );
if ( fetchProfile != null ) {
final Fetch fetch = fetchProfile.getFetchByRole( persister.getRole() );
if ( fetch != null && fetch.getStyle() == SUBSELECT ) {
return true;
}
}
}
}
return false;
} }
private void checkMutability() { private void checkMutability() {
@ -315,6 +337,23 @@ public class LoadQueryInfluencers implements Serializable {
public boolean hasSubselectLoadableCollections(EntityPersister persister) { public boolean hasSubselectLoadableCollections(EntityPersister persister) {
return persister.hasSubselectLoadableCollections() return persister.hasSubselectLoadableCollections()
|| subselectFetchEnabled && persister.hasCollections(); || subselectFetchEnabled && persister.hasCollections()
|| hasSubselectLoadableCollectionsEnabledInProfile( persister );
}
private boolean hasSubselectLoadableCollectionsEnabledInProfile(EntityPersister persister) {
if ( hasEnabledFetchProfiles() ) {
for ( String profile : getEnabledFetchProfileNames() ) {
final FetchProfile fetchProfile = sessionFactory.getFetchProfile( profile );
for ( Fetch fetch : fetchProfile.getFetches().values() ) {
// TODO: check that it's relevant to this persister??
if ( fetch.getStyle() == SUBSELECT ) {
return true;
} }
} }
}
}
return false;
}
}

View File

@ -12,7 +12,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.spi.NavigablePath; import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroup;

View File

@ -88,6 +88,7 @@ import org.hibernate.sql.results.internal.StandardEntityGraphTraversalStateImpl;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.hibernate.engine.profile.Fetch.Style;
import static org.hibernate.query.results.ResultsHelper.attributeName; import static org.hibernate.query.results.ResultsHelper.attributeName;
/** /**
@ -553,7 +554,7 @@ public class LoaderSelectBuilder {
} }
private LoaderSqlAstCreationState createSqlAstCreationState(QuerySpec rootQuerySpec) { private LoaderSqlAstCreationState createSqlAstCreationState(QuerySpec rootQuerySpec) {
final LoaderSqlAstCreationState sqlAstCreationState = new LoaderSqlAstCreationState( return new LoaderSqlAstCreationState(
rootQuerySpec, rootQuerySpec,
new SqlAliasBaseManager(), new SqlAliasBaseManager(),
new SimpleFromClauseAccessImpl(), new SimpleFromClauseAccessImpl(),
@ -563,7 +564,6 @@ public class LoaderSelectBuilder {
loadQueryInfluencers, loadQueryInfluencers,
creationContext creationContext
); );
return sqlAstCreationState;
} }
private void applyRestriction( private void applyRestriction(
@ -875,31 +875,25 @@ public class LoaderSelectBuilder {
for ( String enabledFetchProfileName : loadQueryInfluencers.getEnabledFetchProfileNames() ) { for ( String enabledFetchProfileName : loadQueryInfluencers.getEnabledFetchProfileNames() ) {
final FetchProfile enabledFetchProfile = creationContext.getSessionFactory() final FetchProfile enabledFetchProfile = creationContext.getSessionFactory()
.getFetchProfile( enabledFetchProfileName ); .getFetchProfile( enabledFetchProfileName );
final org.hibernate.engine.profile.Fetch profileFetch = enabledFetchProfile.getFetchByRole( final org.hibernate.engine.profile.Fetch profileFetch =
fetchableRole ); enabledFetchProfile.getFetchByRole( fetchableRole );
if ( profileFetch != null ) { if ( profileFetch != null ) {
fetchTiming = FetchTiming.IMMEDIATE; fetchTiming = FetchTiming.IMMEDIATE;
joined = joined || profileFetch.getStyle() == org.hibernate.engine.profile.Fetch.Style.JOIN; joined = joined || profileFetch.getStyle() == Style.JOIN;
explicitFetch = shouldExplicitFetch( maximumFetchDepth, fetchable, creationState ); explicitFetch = shouldExplicitFetch( maximumFetchDepth, fetchable, creationState );
} }
} }
} }
} }
else if ( loadQueryInfluencers.getEnabledCascadingFetchProfile() != null ) { else if ( loadQueryInfluencers.getEnabledCascadingFetchProfile() != null ) {
final CascadeStyle cascadeStyle = fetchable.asAttributeMapping().getAttributeMetadata() final CascadeStyle cascadeStyle = fetchable.
.getCascadeStyle(); asAttributeMapping().getAttributeMetadata().getCascadeStyle();
final CascadingAction<?> cascadingAction = loadQueryInfluencers.getEnabledCascadingFetchProfile() final CascadingAction<?> cascadingAction =
.getCascadingAction(); loadQueryInfluencers.getEnabledCascadingFetchProfile().getCascadingAction();
if ( cascadeStyle == null || cascadeStyle.doCascade( cascadingAction ) ) { if ( cascadeStyle == null || cascadeStyle.doCascade( cascadingAction ) ) {
fetchTiming = FetchTiming.IMMEDIATE; fetchTiming = FetchTiming.IMMEDIATE;
// In 5.x the CascadeEntityJoinWalker only join fetched the first collection fetch // In 5.x the CascadeEntityJoinWalker only join fetched the first collection fetch
if ( isFetchablePluralAttributeMapping ) { joined = !isFetchablePluralAttributeMapping || rowCardinality == RowCardinality.SINGLE;
joined = rowCardinality == RowCardinality.SINGLE;
}
else {
joined = true;
}
} }
} }
} }

View File

@ -5,7 +5,6 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Fetch; import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchProfile; import org.hibernate.annotations.FetchProfile;
import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModel;
@ -13,10 +12,14 @@ import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set; import java.util.Set;
import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.FetchType.LAZY;
import static org.hibernate.Hibernate.isInitialized;
import static org.hibernate.annotations.FetchMode.JOIN; import static org.hibernate.annotations.FetchMode.JOIN;
import static org.hibernate.annotations.FetchMode.SELECT;
import static org.hibernate.annotations.FetchMode.SUBSELECT;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -25,10 +28,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@FetchProfile(name = NewFetchTest.NEW_PROFILE) @FetchProfile(name = NewFetchTest.NEW_PROFILE)
@FetchProfile(name = NewFetchTest.OLD_PROFILE, @FetchProfile(name = NewFetchTest.OLD_PROFILE,
fetchOverrides = @FetchProfile.FetchOverride(entity = NewFetchTest.E.class, association = "f")) fetchOverrides = @FetchProfile.FetchOverride(entity = NewFetchTest.E.class, association = "f"))
@FetchProfile(name = NewFetchTest.SUBSELECT_PROFILE)
@FetchProfile(name = NewFetchTest.SELECT_PROFILE)
@FetchProfile(name = NewFetchTest.JOIN_PROFILE)
@FetchProfile(name = NewFetchTest.OLD_SUBSELECT_PROFILE,
fetchOverrides = @FetchProfile.FetchOverride(entity = NewFetchTest.F.class, association = "es", mode = SUBSELECT))
public class NewFetchTest { public class NewFetchTest {
public static final String NEW_PROFILE = "new-profile"; static final String NEW_PROFILE = "new-profile";
public static final String OLD_PROFILE = "old-profile"; static final String OLD_PROFILE = "old-profile";
static final String SUBSELECT_PROFILE = "subselect-profile";
static final String SELECT_PROFILE = "select-profile";
static final String JOIN_PROFILE = "join-profile";
static final String OLD_SUBSELECT_PROFILE = "old-subselect-profile";
@Test void test(SessionFactoryScope scope) { @Test void test(SessionFactoryScope scope) {
scope.inTransaction( s-> { scope.inTransaction( s-> {
@ -43,22 +55,109 @@ public class NewFetchTest {
}); });
F f = scope.fromSession( s -> s.find(F.class, 1)); F f = scope.fromSession( s -> s.find(F.class, 1));
assertFalse( Hibernate.isInitialized( f.g ) ); assertFalse( isInitialized( f.g ) );
assertFalse( Hibernate.isInitialized( f.es ) ); assertFalse( isInitialized( f.es ) );
F ff = scope.fromSession( s -> { F ff = scope.fromSession( s -> {
s.enableFetchProfile(NEW_PROFILE); s.enableFetchProfile(NEW_PROFILE);
return s.find(F.class, 1); return s.find(F.class, 1);
} ); } );
assertTrue( Hibernate.isInitialized( ff.g ) ); assertTrue( isInitialized( ff.g ) );
assertTrue( Hibernate.isInitialized( ff.es ) ); assertTrue( isInitialized( ff.es ) );
E e = scope.fromSession( s -> s.find(E.class, 1)); E e = scope.fromSession( s -> s.find(E.class, 1));
assertFalse( Hibernate.isInitialized( e.f ) ); assertFalse( isInitialized( e.f ) );
E ee = scope.fromSession( s -> { E ee = scope.fromSession( s -> {
s.enableFetchProfile(OLD_PROFILE); s.enableFetchProfile(OLD_PROFILE);
return s.find(E.class, 1); return s.find(E.class, 1);
} ); } );
assertTrue( Hibernate.isInitialized( ee.f ) ); assertTrue( isInitialized( ee.f ) );
}
@Test void testStyles(SessionFactoryScope scope) {
long id = scope.fromTransaction( s-> {
G g = new G();
F f = new F();
E e = new E();
f.g = g;
e.f = f;
s.persist(g);
s.persist(f);
s.persist(e);
return f.id;
});
F f0 = scope.fromSession( s -> s.find(F.class, id));
assertFalse(isInitialized(f0.es));
F f1 = scope.fromSession( s -> {
s.enableFetchProfile(SELECT_PROFILE);
return s.find(F.class, id);
});
assertTrue(isInitialized(f1.es));
F f2 = scope.fromSession( s -> {
s.enableFetchProfile(JOIN_PROFILE);
return s.find(F.class, id);
});
assertTrue(isInitialized(f2.es));
F f3 = scope.fromSession( s -> {
s.enableFetchProfile(SUBSELECT_PROFILE);
return s.find(F.class, id);
});
assertTrue(isInitialized(f3.es));
}
@Test void subselectTest(SessionFactoryScope scope) {
scope.inTransaction(s -> {
G g = new G();
F f1 = new F();
F f2 = new F();
E e1 = new E();
E e2 = new E();
E e3 = new E();
E e4 = new E();
f1.g = g;
f2.g = g;
e1.f = f1;
e2.f = f1;
e3.f = f1;
e4.f = f2;
s.persist(g);
s.persist(f1);
s.persist(f2);
s.persist(e1);
s.persist(e2);
s.persist(e3);
});
scope.inSession( s -> {
List<F> fs = s.createSelectionQuery("from F", F.class).getResultList();
F f0 = fs.get(0);
F f1 = fs.get(1);
assertFalse( isInitialized( f0.es ) );
assertFalse( isInitialized( f1.es ) );
f0.es.size();
assertTrue( isInitialized( f0.es ) );
assertFalse( isInitialized( f1.es ) );
});
scope.inSession( s -> {
s.enableFetchProfile(SUBSELECT_PROFILE);
List<F> fs = s.createSelectionQuery("from F", F.class).getResultList();
F f0 = fs.get(0);
F f1 = fs.get(1);
assertFalse( isInitialized( f0.es ) );
assertFalse( isInitialized( f1.es ) );
f0.es.size();
assertTrue( isInitialized( f0.es ) );
assertTrue( isInitialized( f1.es ) );
});
scope.inSession( s -> {
s.enableFetchProfile(OLD_SUBSELECT_PROFILE);
List<F> fs = s.createSelectionQuery("from F", F.class).getResultList();
F f0 = fs.get(0);
F f1 = fs.get(1);
assertFalse( isInitialized( f0.es ) );
assertFalse( isInitialized( f1.es ) );
f0.es.size();
assertTrue( isInitialized( f0.es ) );
assertTrue( isInitialized( f1.es ) );
});
} }
@Entity(name = "E") @Entity(name = "E")
@ -77,6 +176,9 @@ public class NewFetchTest {
G g; G g;
@OneToMany(mappedBy = "f") @OneToMany(mappedBy = "f")
@Fetch(value = JOIN, profile = NEW_PROFILE) @Fetch(value = JOIN, profile = NEW_PROFILE)
@Fetch(value = SUBSELECT, profile = SUBSELECT_PROFILE)
@Fetch(value = SELECT, profile = SELECT_PROFILE)
@Fetch(value = JOIN, profile = JOIN_PROFILE)
Set<E> es; Set<E> es;
} }
@Entity(name = "G") @Entity(name = "G")