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

  1. If you want the association loaded by join when the profile is active: use 'fetch = FetchType.EAGER' with 'mode = FetchMode.JOIN'.
  2. If you want the association lazy: keep 'fetch = LAZY' but use 'mode = SELECT' (or remove the override).
  3. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/5fb778de251a9b1a. Report an issue: GitHub.