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

View File

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

View File

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

View File

@ -6,6 +6,8 @@
*/
package org.hibernate.engine.profile;
import java.util.Locale;
/**
* 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
* 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).
* The type or style of fetch.
*/
public enum Style {
/**
* Fetch via a join
*/
JOIN( "join" ),
JOIN,
/**
* Fetch via a subsequent select
*/
SELECT( "select" );
private final String name;
Style(String name) {
this.name = name;
}
SELECT,
/**
* Fetch via a subsequent subselect
*/
SUBSELECT;
@Override
public String toString() {
return name;
return name().toLowerCase(Locale.ROOT);
}
/**
@ -70,13 +66,12 @@ public class Fetch {
* @return The style; {@link #JOIN} is returned if not recognized
*/
public static Style parse(String name) {
if ( SELECT.name.equals( name ) ) {
return SELECT;
}
else {
// the default...
return JOIN;
for ( Style style: values() ) {
if ( style.name().equalsIgnoreCase( name ) ) {
return style;
}
}
return JOIN;
}
}

View File

@ -167,4 +167,9 @@ public class FetchProfile {
public boolean isContainsJoinFetchedBag() {
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.UnknownProfileException;
import org.hibernate.engine.profile.Fetch;
import org.hibernate.engine.profile.FetchProfile;
import org.hibernate.internal.FilterImpl;
import org.hibernate.internal.SessionCreationOptions;
import org.hibernate.loader.ast.spi.CascadingFetchProfile;
import org.hibernate.persister.collection.CollectionPersister;
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
* entity. Currently, such influencers are defined as:<ul>
@ -157,7 +162,7 @@ public class LoadQueryInfluencers implements Serializable {
*/
public Set<String> getEnabledFilterNames() {
if ( enabledFilters == null ) {
return Collections.emptySet();
return emptySet();
}
else {
return Collections.unmodifiableSet( enabledFilters.keySet() );
@ -221,7 +226,7 @@ public class LoadQueryInfluencers implements Serializable {
}
public Set<String> getEnabledFetchProfileNames() {
return Objects.requireNonNullElse( enabledFetchProfileNames, Collections.emptySet() );
return Objects.requireNonNullElse( enabledFetchProfileNames, emptySet() );
}
private void checkFetchProfileName(String name) {
@ -302,7 +307,24 @@ public class LoadQueryInfluencers implements Serializable {
}
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() {
@ -315,6 +337,23 @@ public class LoadQueryInfluencers implements Serializable {
public boolean hasSubselectLoadableCollections(EntityPersister persister) {
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.Set;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
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 static java.util.Collections.singletonList;
import static org.hibernate.engine.profile.Fetch.Style;
import static org.hibernate.query.results.ResultsHelper.attributeName;
/**
@ -553,7 +554,7 @@ public class LoaderSelectBuilder {
}
private LoaderSqlAstCreationState createSqlAstCreationState(QuerySpec rootQuerySpec) {
final LoaderSqlAstCreationState sqlAstCreationState = new LoaderSqlAstCreationState(
return new LoaderSqlAstCreationState(
rootQuerySpec,
new SqlAliasBaseManager(),
new SimpleFromClauseAccessImpl(),
@ -563,7 +564,6 @@ public class LoaderSelectBuilder {
loadQueryInfluencers,
creationContext
);
return sqlAstCreationState;
}
private void applyRestriction(
@ -875,31 +875,25 @@ public class LoaderSelectBuilder {
for ( String enabledFetchProfileName : loadQueryInfluencers.getEnabledFetchProfileNames() ) {
final FetchProfile enabledFetchProfile = creationContext.getSessionFactory()
.getFetchProfile( enabledFetchProfileName );
final org.hibernate.engine.profile.Fetch profileFetch = enabledFetchProfile.getFetchByRole(
fetchableRole );
final org.hibernate.engine.profile.Fetch profileFetch =
enabledFetchProfile.getFetchByRole( fetchableRole );
if ( profileFetch != null ) {
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 );
}
}
}
}
else if ( loadQueryInfluencers.getEnabledCascadingFetchProfile() != null ) {
final CascadeStyle cascadeStyle = fetchable.asAttributeMapping().getAttributeMetadata()
.getCascadeStyle();
final CascadingAction<?> cascadingAction = loadQueryInfluencers.getEnabledCascadingFetchProfile()
.getCascadingAction();
final CascadeStyle cascadeStyle = fetchable.
asAttributeMapping().getAttributeMetadata().getCascadeStyle();
final CascadingAction<?> cascadingAction =
loadQueryInfluencers.getEnabledCascadingFetchProfile().getCascadingAction();
if ( cascadeStyle == null || cascadeStyle.doCascade( cascadingAction ) ) {
fetchTiming = FetchTiming.IMMEDIATE;
// In 5.x the CascadeEntityJoinWalker only join fetched the first collection fetch
if ( isFetchablePluralAttributeMapping ) {
joined = rowCardinality == RowCardinality.SINGLE;
}
else {
joined = true;
}
joined = !isFetchablePluralAttributeMapping || rowCardinality == RowCardinality.SINGLE;
}
}
}

View File

@ -5,7 +5,6 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchProfile;
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.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
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.SELECT;
import static org.hibernate.annotations.FetchMode.SUBSELECT;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.OLD_PROFILE,
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 static final String NEW_PROFILE = "new-profile";
public static final String OLD_PROFILE = "old-profile";
static final String NEW_PROFILE = "new-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) {
scope.inTransaction( s-> {
@ -43,22 +55,109 @@ public class NewFetchTest {
});
F f = scope.fromSession( s -> s.find(F.class, 1));
assertFalse( Hibernate.isInitialized( f.g ) );
assertFalse( Hibernate.isInitialized( f.es ) );
assertFalse( isInitialized( f.g ) );
assertFalse( isInitialized( f.es ) );
F ff = scope.fromSession( s -> {
s.enableFetchProfile(NEW_PROFILE);
return s.find(F.class, 1);
} );
assertTrue( Hibernate.isInitialized( ff.g ) );
assertTrue( Hibernate.isInitialized( ff.es ) );
assertTrue( isInitialized( ff.g ) );
assertTrue( isInitialized( ff.es ) );
E e = scope.fromSession( s -> s.find(E.class, 1));
assertFalse( Hibernate.isInitialized( e.f ) );
assertFalse( isInitialized( e.f ) );
E ee = scope.fromSession( s -> {
s.enableFetchProfile(OLD_PROFILE);
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")
@ -77,6 +176,9 @@ public class NewFetchTest {
G g;
@OneToMany(mappedBy = "f")
@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;
}
@Entity(name = "G")