hibernate/hibernate-orm · error · AnnotationException
Fetch profile '{}' has a '@FetchOverride' with 'fetch=LAZY'
Error message
Fetch profile '{}' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN' (join fetching is eager by nature) What it means
Inside a '@FetchProfile', a '@FetchOverride' combines 'fetch = LAZY' with 'mode = JOIN'. Join fetching works by eagerly pulling the association in the same SELECT, so it is inherently eager — asking it to be lazy is contradictory and Hibernate rejects the profile at bootstrap.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java:485
.findClassDetails( packageName + ".package-info" );
if ( packageInfoClassDetails != null ) {
bindFetchProfiles( packageInfoClassDetails, context );
}
}
private static void bindFetchProfiles(AnnotationTarget annotatedElement, MetadataBuildingContext context) {
annotatedElement.forEachAnnotationUsage( FetchProfile.class, modelsContext( context ), (usage) -> {
bindFetchProfile( usage, context );
} );
}
private static void bindFetchProfile(FetchProfile fetchProfile, MetadataBuildingContext context) {
final String name = fetchProfile.name();
if ( reuseOrCreateFetchProfile( context, name ) ) {
for ( var fetchOverride : fetchProfile.fetchOverrides() ) {
if ( fetchOverride.fetch() == FetchType.LAZY
&& fetchOverride.mode() == FetchMode.JOIN ) {
throw new AnnotationException(
"Fetch profile '" + name
+ "' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN'"
+ " (join fetching is eager by nature)"
);
}
context.getMetadataCollector()
.addSecondPass( new FetchOverrideSecondPass( name, fetchOverride, 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
final var collector = context.getMetadataCollector();
var existing = collector.getFetchProfile( name );
if ( existing == null ) {View on GitHub (pinned to fad1729dce)
Solutions
- If you want the association loaded by join when the profile is active: use 'fetch = FetchType.EAGER' with 'mode = FetchMode.JOIN'.
- If you want the association lazy: keep 'fetch = LAZY' but use 'mode = SELECT' (or remove the override).
- Review every override in the profile named in the message — the check applies per @FetchOverride.
Example fix
// before
@FetchProfile(name = "order-with-items", fetchOverrides = {
@FetchOverride(entity = Order.class, association = "items",
fetch = FetchType.LAZY, mode = FetchMode.JOIN) // contradictory
})
// after
@FetchProfile(name = "order-with-items", fetchOverrides = {
@FetchOverride(entity = Order.class, association = "items",
fetch = FetchType.EAGER, mode = FetchMode.JOIN)
}) Defensive patterns
Strategy: validation
Validate before calling
// Guard: reject LAZY+JOIN overrides before bootstrap
FetchProfile profile = Order.class.getAnnotation(FetchProfile.class);
if (profile != null) {
for (FetchOverride o : profile.fetchOverrides()) {
if (o.fetch() == FetchType.LAZY && o.mode() == FetchMode.JOIN) {
throw new IllegalStateException(
"Fetch profile '" + profile.name() + "': LAZY + JOIN is contradictory");
}
}
} Try / catch
try {
factory = cfg.buildSessionFactory();
} catch (AnnotationException e) {
// 'fetch=LAZY and mode=JOIN' -> pick EAGER+JOIN or LAZY+SELECT
throw newConfigurationException("Invalid fetch profile", e);
} Prevention
- Remember mode=JOIN always loads eagerly; pair it only with fetch=EAGER.
- Keep fetch profiles small and review the full override tuple (entity, association, fetch, mode) on every edit.
When it happens
Trigger: '@FetchProfile(name = "p", fetchOverrides = @FetchOverride(entity = X.class, association = "a", fetch = FetchType.LAZY, mode = FetchMode.JOIN))'; editing a profile and changing only the fetch attribute while leaving mode=JOIN; profiles migrated from XML with the same combination.
Common situations: Trying to tune N+1 queries by flipping mode to JOIN and leaving an old LAZY setting behind; teams copying a fetch override block and adjusting one field; misunderstandings that JOIN fetch could be deferred.
Related errors
- Class '<componentClassName>' is an '@Embeddable' type and ma
- Property '<propertyName>' may not be annotated '@BatchSize'
- One to many association '<propertyName>' was annotated '@Col
- Collection '<propertyName>' was annotated '@Collate'
- Root entity '<entityName>' is annotated '@DiscriminatorOptio
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/5fb778de251a9b1a.
Report an issue: GitHub.