HHH-16666 allow fetch profiles to be defined using the @Fetch annotation

1. You may now declare an empty named @FetchProfile, and
2. add associations to it using @Fetch.

Note that @Fetch becomes a repeatable annotation.
This commit is contained in:
Gavin 2023-05-22 14:58:01 +02:00 committed by Gavin King
parent 2daeadd449
commit f2dbe7a9cb
9 changed files with 342 additions and 58 deletions

View File

@ -6,13 +6,18 @@
*/
package org.hibernate.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specifies the default fetching strategy for the annotated association.
* Specifies the default fetching strategy for the annotated association,
* or, if {@link #profile} is specified, the fetching strategy for the
* annotated association in the named {@linkplain FetchProfile fetch profile}.
* <p>
* When this annotation is <em>not</em> explicitly specified, then:
* <ul>
@ -25,17 +30,29 @@ import java.lang.annotation.Target;
* <p>
* The default fetching strategy specified by this annotation may be
* overridden in a given {@linkplain FetchProfile fetch profile}.
* <p>
* If {@link #profile} is specified, then the given profile name must
* match the name of an existing fetch profile declared using the
* {@link FetchProfile#name @FetchProfile} annotation.
*
* @author Emmanuel Bernard
*
* @see FetchMode
* @see FetchProfile
*/
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Repeatable(Fetches.class)
public @interface Fetch {
/**
* The method that should be used to fetch the association.
*/
FetchMode value();
/**
* The name of the {@link FetchProfile fetch profile} in
* which this fetch mode should be applied. By default,
* it is applied the default fetch profile.
*/
String profile() default "";
}

View File

@ -17,6 +17,14 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Defines a fetch profile, by specifying its {@link #name}, together
* with a list of {@linkplain #fetchOverrides fetch strategy overrides}.
* The definition of a single named fetch profile may be split over
* multiple {@link FetchProfile @FetchProfile} annotations which share
* the same {@link #name}.
* <p>
* Additional fetch strategy overrides may be added to a named fetch
* profile by annotating the fetched associations themselves with the
* {@link Fetch @Fetch} annotation, specifying the
* {@linkplain Fetch#profile() name of the fetch profile}.
* <p>
* A named fetch profile must be explicitly enabled in a given session
* by calling {@link org.hibernate.Session#enableFetchProfile(String)}
@ -74,8 +82,12 @@ public @interface FetchProfile {
/**
* The list of association fetching strategy overrides.
* <p>
* Additional overrides may be specified by marking the
* fetched associations themselves with the {@link Fetch @Fetch}
* annotation.
*/
FetchOverride[] fetchOverrides();
FetchOverride[] fetchOverrides() default {};
/**
* Overrides the fetching strategy pf a particular association

View File

@ -0,0 +1,23 @@
/*
* 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.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author Gavin King
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Fetches {
Fetch[] value();
}

View File

@ -22,7 +22,9 @@ 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;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.FilterDefs;
@ -93,6 +95,7 @@ import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators;
import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity;
import static org.hibernate.boot.model.internal.InheritanceState.getSuperclassInheritanceState;
import static org.hibernate.internal.CoreLogging.messageLogger;
import static org.hibernate.mapping.MetadataSource.ANNOTATIONS;
/**
* Reads annotations from Java classes and produces the Hibernate configuration-time metamodel,
@ -834,27 +837,45 @@ public final class AnnotationBinder {
}
private static void bindFetchProfiles(XAnnotatedElement annotatedElement, MetadataBuildingContext context) {
final FetchProfile fetchProfileAnnotation = annotatedElement.getAnnotation( FetchProfile.class );
final FetchProfiles fetchProfileAnnotations = annotatedElement.getAnnotation( FetchProfiles.class );
if ( fetchProfileAnnotation != null ) {
bindFetchProfile( fetchProfileAnnotation, context );
final FetchProfile fetchProfile = annotatedElement.getAnnotation( FetchProfile.class );
final FetchProfiles fetchProfiles = annotatedElement.getAnnotation( FetchProfiles.class );
if ( fetchProfile != null ) {
bindFetchProfile( fetchProfile, context );
}
if ( fetchProfileAnnotations != null ) {
for ( FetchProfile profile : fetchProfileAnnotations.value() ) {
if ( fetchProfiles != null ) {
for ( FetchProfile profile : fetchProfiles.value() ) {
bindFetchProfile( profile, context );
}
}
}
private static void bindFetchProfile(FetchProfile fetchProfileAnnotation, MetadataBuildingContext context) {
for ( FetchProfile.FetchOverride fetch : fetchProfileAnnotation.fetchOverrides() ) {
org.hibernate.annotations.FetchMode mode = fetch.mode();
if ( !mode.equals( org.hibernate.annotations.FetchMode.JOIN ) ) {
throw new MappingException( "Only FetchMode.JOIN is currently supported" );
private static void bindFetchProfile(FetchProfile fetchProfile, MetadataBuildingContext context) {
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" );
}
context.getMetadataCollector()
.addSecondPass( new FetchOverrideSecondPass( name, fetch, context ) );
}
context.getMetadataCollector().addSecondPass(
new VerifyFetchProfileReferenceSecondPass( fetchProfileAnnotation.name(), fetch, context )
);
}
// otherwise, it's a fetch profile defined in XML, and it overrides
// the annotations, so we simply ignore this annotation completely
}
private static boolean reuseOrCreateFetchProfile(MetadataBuildingContext context, String name) {
// We tolerate multiple @FetchProfile annotations for same named profile
org.hibernate.mapping.FetchProfile existing = context.getMetadataCollector().getFetchProfile( name );
if ( existing == null ) {
// no existing profile, so create a new one
org.hibernate.mapping.FetchProfile profile =
new org.hibernate.mapping.FetchProfile( name, ANNOTATIONS );
context.getMetadataCollector().addFetchProfile( profile );
return true;
}
else {
return existing.getSource() == ANNOTATIONS;
}
}

View File

@ -33,6 +33,7 @@ import org.hibernate.annotations.CollectionType;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.CompositeType;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.Fetches;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterJoinTable;
import org.hibernate.annotations.FilterJoinTables;
@ -64,11 +65,11 @@ import org.hibernate.annotations.Persister;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.SQLInsert;
import org.hibernate.annotations.SQLSelect;
import org.hibernate.annotations.SQLUpdate;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.annotations.SQLJoinTableRestriction;
import org.hibernate.annotations.SQLOrder;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.annotations.SQLSelect;
import org.hibernate.annotations.SQLUpdate;
import org.hibernate.annotations.SortComparator;
import org.hibernate.annotations.SortNatural;
import org.hibernate.annotations.Synchronize;
@ -1454,17 +1455,47 @@ public abstract class CollectionBinder {
}
private void handleFetch() {
if ( property.isAnnotationPresent( Fetch.class ) ) {
if ( !handleHibernateFetchMode() ) {
// Hibernate @Fetch annotation takes precedence
handleHibernateFetchMode();
}
else {
collection.setFetchMode( getFetchMode( getJpaFetchType() ) );
}
}
private void handleHibernateFetchMode() {
switch ( property.getAnnotation( Fetch.class ).value() ) {
private boolean handleHibernateFetchMode() {
if ( property.isAnnotationPresent( Fetch.class ) ) {
final Fetch fetch = property.getAnnotation( Fetch.class );
if ( fetch.profile().isEmpty() ) {
setHibernateFetchMode( fetch.value() );
return true;
}
else {
buildingContext.getMetadataCollector()
.addSecondPass( new FetchSecondPass( fetch, propertyHolder, propertyName, buildingContext ) );
return false;
}
}
else if ( property.isAnnotationPresent( Fetches.class ) ) {
boolean result = false;
for ( Fetch fetch: property.getAnnotation( Fetches.class ).value() ) {
if ( fetch.profile().isEmpty() ) {
if ( result ) {
throw new AnnotationException( "Collection '" + safeCollectionRole()
+ "' had multiple '@Fetch' annotations which did not specify a named fetch 'profile'"
+ " (only one annotation may be specified for the default profile)" );
}
setHibernateFetchMode( fetch.value() );
result = true;
}
}
return result;
}
else {
return false;
}
}
private void setHibernateFetchMode(org.hibernate.annotations.FetchMode fetchMode) {
switch ( fetchMode ) {
case JOIN:
collection.setFetchMode( FetchMode.JOIN );
collection.setLazy( false );

View File

@ -5,27 +5,28 @@
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.boot.model.internal;
import java.util.Locale;
import java.util.Map;
import org.hibernate.MappingException;
import org.hibernate.annotations.FetchProfile;
import org.hibernate.annotations.FetchProfile.FetchOverride;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.SecondPass;
import org.hibernate.mapping.MetadataSource;
import org.hibernate.mapping.FetchProfile;
import org.hibernate.mapping.PersistentClass;
import java.util.Locale;
import java.util.Map;
/**
* @author Hardy Ferentschik
*/
public class VerifyFetchProfileReferenceSecondPass implements SecondPass {
public class FetchOverrideSecondPass implements SecondPass {
private final String fetchProfileName;
private final FetchProfile.FetchOverride fetch;
private final FetchOverride fetch;
private final MetadataBuildingContext buildingContext;
public VerifyFetchProfileReferenceSecondPass(
public FetchOverrideSecondPass(
String fetchProfileName,
FetchProfile.FetchOverride fetch,
FetchOverride fetch,
MetadataBuildingContext buildingContext) {
this.fetchProfileName = fetchProfileName;
this.fetch = fetch;
@ -34,21 +35,13 @@ public class VerifyFetchProfileReferenceSecondPass implements SecondPass {
@Override
public void doSecondPass(Map<String, PersistentClass> persistentClasses) throws MappingException {
org.hibernate.mapping.FetchProfile profile = buildingContext.getMetadataCollector().getFetchProfile( fetchProfileName );
if ( profile != null ) {
if ( profile.getSource() != MetadataSource.ANNOTATIONS ) {
return;
}
}
else {
profile = new org.hibernate.mapping.FetchProfile( fetchProfileName, MetadataSource.ANNOTATIONS );
buildingContext.getMetadataCollector().addFetchProfile( profile );
}
PersistentClass clazz = buildingContext.getMetadataCollector().getEntityBinding( fetch.entity().getName() );
// throws MappingException in case the property does not exist
clazz.getProperty( fetch.association() );
buildingContext.getMetadataCollector()
.getEntityBinding( fetch.entity().getName() )
.getProperty( fetch.association() );
final FetchProfile profile = buildingContext.getMetadataCollector().getFetchProfile( fetchProfileName );
// we already know that the FetchProfile exists and is good to use
profile.addFetch(
fetch.entity().getName(),
fetch.association(),

View File

@ -0,0 +1,64 @@
/*
* 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.boot.model.internal;
import java.util.Locale;
import java.util.Map;
import org.hibernate.AnnotationException;
import org.hibernate.MappingException;
import org.hibernate.annotations.Fetch;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.SecondPass;
import org.hibernate.mapping.FetchProfile;
import org.hibernate.mapping.PersistentClass;
import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.mapping.MetadataSource.ANNOTATIONS;
/**
* @author Gavin King
*/
public class FetchSecondPass implements SecondPass {
private final Fetch fetch;
private final PropertyHolder propertyHolder;
private final String propertyName;
private final MetadataBuildingContext buildingContext;
public FetchSecondPass(Fetch fetch, PropertyHolder propertyHolder, String propertyName, MetadataBuildingContext buildingContext) {
this.fetch = fetch;
this.propertyHolder = propertyHolder;
this.propertyName = propertyName;
this.buildingContext = buildingContext;
}
@Override
public void doSecondPass(Map<String, PersistentClass> persistentClasses) throws MappingException {
//TODO: handle propertyHolder.getPath() !!!!
// throws MappingException in case the property does not exist
buildingContext.getMetadataCollector()
.getEntityBinding( propertyHolder.getEntityName() )
.getProperty( propertyName );
FetchProfile profile = buildingContext.getMetadataCollector().getFetchProfile( fetch.profile() );
if ( profile == null ) {
throw new AnnotationException( "Property '" + qualify( propertyHolder.getPath(), propertyName )
+ "' refers to an unknown fetch profile named '" + fetch.profile() + "'" );
}
else if ( profile.getSource() == ANNOTATIONS ) {
profile.addFetch(
propertyHolder.getEntityName(),
propertyName,
fetch.value().toString().toLowerCase(Locale.ROOT)
);
}
// otherwise, it's a fetch profile defined in XML, and it overrides
// the annotations, so we simply ignore this annotation completely
}
}

View File

@ -15,6 +15,7 @@ import org.hibernate.FetchMode;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.Fetches;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.annotations.NotFound;
@ -307,15 +308,12 @@ public class ToOneBinder {
PropertyData inferredData,
PropertyHolder propertyHolder) {
handleLazy( toOne, property, inferredData, propertyHolder );
handleFetch( toOne, property );
handleFetch( toOne, property, propertyHolder, inferredData );
}
private static void handleFetch(ToOne toOne, XProperty property) {
if ( property.isAnnotationPresent( Fetch.class ) ) {
private static void handleFetch(ToOne toOne, XProperty property, PropertyHolder propertyHolder, PropertyData inferredData) {
if ( !handleHibernateFetchMode( toOne, property, propertyHolder, inferredData ) ) {
// Hibernate @Fetch annotation takes precedence
handleHibernateFetchMode( toOne, property );
}
else {
toOne.setFetchMode( getFetchMode( getJpaFetchType( property ) ) );
}
}
@ -333,8 +331,46 @@ public class ToOneBinder {
}
}
private static void handleHibernateFetchMode(ToOne toOne, XProperty property) {
switch ( property.getAnnotation( Fetch.class ).value() ) {
private static boolean handleHibernateFetchMode(
ToOne toOne,
XProperty property,
PropertyHolder propertyHolder,
PropertyData inferredData) {
if ( property.isAnnotationPresent( Fetch.class ) ) {
final Fetch fetch = property.getAnnotation( Fetch.class );
if ( fetch.profile().isEmpty() ) {
setHibernateFetchMode( toOne, property, fetch.value() );
return true;
}
else {
final MetadataBuildingContext context = toOne.getBuildingContext();
context.getMetadataCollector()
.addSecondPass( new FetchSecondPass( fetch, propertyHolder, inferredData.getPropertyName(), context ) );
return false;
}
}
else if ( property.isAnnotationPresent( Fetches.class ) ) {
boolean result = false;
for ( Fetch fetch: property.getAnnotation( Fetches.class ).value() ) {
if ( fetch.profile().isEmpty() ) {
if ( result ) {
throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData )
+ "' had multiple '@Fetch' annotations which did not specify a named fetch 'profile'"
+ " (only one annotation may be specified for the default profile)" );
}
setHibernateFetchMode( toOne, property, fetch.value() );
result = true;
}
}
return result;
}
else {
return false;
}
}
private static void setHibernateFetchMode(ToOne toOne, XProperty property, org.hibernate.annotations.FetchMode fetchMode) {
switch ( fetchMode ) {
case JOIN:
toOne.setFetchMode( FetchMode.JOIN );
toOne.setLazy( false );

View File

@ -0,0 +1,87 @@
package org.hibernate.orm.test.annotations.fetchprofile;
import jakarta.persistence.Entity;
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;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static jakarta.persistence.FetchType.LAZY;
import static org.hibernate.annotations.FetchMode.JOIN;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SessionFactory
@DomainModel(annotatedClasses = {NewFetchTest.class,NewFetchTest.E.class, NewFetchTest.F.class, NewFetchTest.G.class})
@FetchProfile(name = NewFetchTest.NEW_PROFILE)
@FetchProfile(name = NewFetchTest.OLD_PROFILE,
fetchOverrides = @FetchProfile.FetchOverride(entity = NewFetchTest.E.class, association = "f", mode = JOIN))
public class NewFetchTest {
public static final String NEW_PROFILE = "new-profile";
public static final String OLD_PROFILE = "old-profile";
@Test void test(SessionFactoryScope scope) {
scope.inTransaction( 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);
});
F f = scope.fromSession( s -> s.find(F.class, 1));
assertFalse( Hibernate.isInitialized( f.g ) );
assertFalse( Hibernate.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 ) );
E e = scope.fromSession( s -> s.find(E.class, 1));
assertFalse( Hibernate.isInitialized( e.f ) );
E ee = scope.fromSession( s -> {
s.enableFetchProfile(OLD_PROFILE);
return s.find(E.class, 1);
} );
assertTrue( Hibernate.isInitialized( ee.f ) );
}
@Entity(name = "E")
static class E {
@Id @GeneratedValue
Long id;
@ManyToOne(fetch = LAZY)
F f;
}
@Entity(name = "F")
static class F {
@Id @GeneratedValue
Long id;
@ManyToOne(fetch = LAZY)
@Fetch(value = JOIN, profile = NEW_PROFILE)
G g;
@OneToMany(mappedBy = "f")
@Fetch(value = JOIN, profile = NEW_PROFILE)
Set<E> es;
}
@Entity(name = "G")
static class G {
@Id @GeneratedValue
Long id;
}
}