{"record":{"id":"5fb778de251a9b1a","repo":"hibernate/hibernate-orm","slug":"fetch-profile-has-a-fetchoverride-with-fe","errorCode":null,"errorMessage":"Fetch profile '{}' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN' (join fetching is eager by nature)","messagePattern":"Fetch profile '(.+?)' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN' \\(join fetching is eager by nature\\)","errorType":"exception","errorClass":"AnnotationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java","lineNumber":485,"sourceCode":"\t\t\t\t\t\t.findClassDetails( packageName + \".package-info\" );\n\t\tif ( packageInfoClassDetails != null ) {\n\t\t\tbindFetchProfiles( packageInfoClassDetails, context );\n\t\t}\n\t}\n\n\tprivate static void bindFetchProfiles(AnnotationTarget annotatedElement, MetadataBuildingContext context) {\n\t\tannotatedElement.forEachAnnotationUsage( FetchProfile.class, modelsContext( context ), (usage) -> {\n\t\t\tbindFetchProfile( usage, context );\n\t\t} );\n\t}\n\n\tprivate static void bindFetchProfile(FetchProfile fetchProfile, MetadataBuildingContext context) {\n\t\tfinal String name = fetchProfile.name();\n\t\tif ( reuseOrCreateFetchProfile( context, name ) ) {\n\t\t\tfor ( var fetchOverride : fetchProfile.fetchOverrides() ) {\n\t\t\t\tif ( fetchOverride.fetch() == FetchType.LAZY\n\t\t\t\t\t&& fetchOverride.mode() == FetchMode.JOIN ) {\n\t\t\t\t\tthrow new AnnotationException(\n\t\t\t\t\t\t\t\"Fetch profile '\" + name\n\t\t\t\t\t\t\t\t\t+ \"' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN'\"\n\t\t\t\t\t\t\t\t\t+ \" (join fetching is eager by nature)\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcontext.getMetadataCollector()\n\t\t\t\t\t\t.addSecondPass( new FetchOverrideSecondPass( name, fetchOverride, context ) );\n\t\t\t}\n\t\t}\n\t\t// otherwise, it's a fetch profile defined in XML, and it overrides\n\t\t// the annotations, so we simply ignore this annotation completely\n\t}\n\n\tprivate static boolean reuseOrCreateFetchProfile(MetadataBuildingContext context, String name) {\n\t\t// We tolerate multiple @FetchProfile annotations for same named profile\n\t\tfinal var collector = context.getMetadataCollector();\n\t\tvar existing = collector.getFetchProfile( name );\n\t\tif ( existing == null ) {","sourceCodeStart":467,"sourceCodeEnd":503,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java#L467-L503","documentation":"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.","triggerScenarios":"'@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.","commonSituations":"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.","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."],"exampleFix":"// before\n@FetchProfile(name = \"order-with-items\", fetchOverrides = {\n    @FetchOverride(entity = Order.class, association = \"items\",\n        fetch = FetchType.LAZY, mode = FetchMode.JOIN) // contradictory\n})\n\n// after\n@FetchProfile(name = \"order-with-items\", fetchOverrides = {\n    @FetchOverride(entity = Order.class, association = \"items\",\n        fetch = FetchType.EAGER, mode = FetchMode.JOIN)\n})","handlingStrategy":"validation","validationCode":"// Guard: reject LAZY+JOIN overrides before bootstrap\nFetchProfile profile = Order.class.getAnnotation(FetchProfile.class);\nif (profile != null) {\n    for (FetchOverride o : profile.fetchOverrides()) {\n        if (o.fetch() == FetchType.LAZY && o.mode() == FetchMode.JOIN) {\n            throw new IllegalStateException(\n                \"Fetch profile '\" + profile.name() + \"': LAZY + JOIN is contradictory\");\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    factory = cfg.buildSessionFactory();\n} catch (AnnotationException e) {\n    // 'fetch=LAZY and mode=JOIN' -> pick EAGER+JOIN or LAZY+SELECT\n    throw newConfigurationException(\"Invalid fetch profile\", e);\n}","preventionTips":["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."],"tags":["hibernate","fetch-profile","fetch-strategy","orm-mapping"],"backgroundTag":"fetch-profile-misconfiguration","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}