{"record":{"id":"57fae961e21b650c","repo":"hibernate/hibernate-orm","slug":"null-key-for-collection-s-57fae9","errorCode":null,"errorMessage":"null key for collection: %s","messagePattern":"null key for collection: (.+?)","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/action/queue/internal/decompose/collection/HistoryCollectionMutationPlanContributor.java","lineNumber":177,"sourceCode":"\t\t\t\tObject key,\n\t\t\t\tObject entry,\n\t\t\t\tint entryIndex,\n\t\t\t\tTemporalMapping temporalMapping) {\n\t\t\tthis.persister = persister;\n\t\t\tthis.collection = collection;\n\t\t\tthis.key = key;\n\t\t\tthis.entry = entry;\n\t\t\tthis.entryIndex = entryIndex;\n\t\t\tthis.temporalMapping = temporalMapping;\n\t\t}\n\n\t\t@Override\n\t\tpublic void bindValues(\n\t\t\t\tJdbcValueBindings jdbcValueBindings,\n\t\t\t\tFlushOperation flushOperation,\n\t\t\t\tSharedSessionContractImplementor session) {\n\t\t\tif ( key == null ) {\n\t\t\t\tthrow new IllegalArgumentException( \"null key for collection: \" + persister.getNavigableRole().getFullPath() );\n\t\t\t}\n\t\t\tbindRowValues( jdbcValueBindings, session );\n\t\t\tif ( TemporalMutationHelper.isUsingParameters( session ) ) {\n\t\t\t\tjdbcValueBindings.bindValue(\n\t\t\t\t\t\tsession.getCurrentChangesetIdentifier(),\n\t\t\t\t\t\ttemporalMapping.getStartingColumnMapping().getSelectionExpression(),\n\t\t\t\t\t\tParameterUsage.SET\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tprivate void bindRowValues(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) {\n\t\t\tfinal var attributeMapping = persister.getAttributeMapping();\n\t\t\tattributeMapping.getKeyDescriptor().getKeyPart().decompose(\n\t\t\t\t\tkey,\n\t\t\t\t\tjdbcValueBindings::bindAssignment,\n\t\t\t\t\tsession\n\t\t\t);","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/action/queue/internal/decompose/collection/HistoryCollectionMutationPlanContributor.java#L159-L195","documentation":"HistoryCollectionMutationPlanContributor builds inserts for the audit/history trail of a collection. Its bindValues (HistoryCollectionMutationPlanContributor.java:177) starts with the same guard: a null collection key cannot be bound to the FK of the history row, so it throws IllegalArgumentException(\"null key for collection: <role>\"). In the history path the key must also be stable at planning time, because the trail row records which owner the change belongs to.","triggerScenarios":"Audit/history collection plans active (graph queue) and a collection mutation planned while the owner's key is null: unsaved owner, assigned id never set, custom generator returning null, or a key handle that was never resolved during deferred identity planning.","commonSituations":"Temporal/audit feature enabled on entities whose id generation is misconfigured (missing @GeneratedValue on an assigned-id entity); deferring identity inserts (hibernate.flush.queue.graph.defer_identity_inserts=true) while history collection plans capture the owner key too early; partial saves in tests.","solutions":["Persist the owning entity first so its identifier exists before any collection mutation is planned","For assigned ids, guarantee assignment in one place (factory/@PrePersist check) so a flush can never see a null owner id","Fix custom generators that may return null; throw a descriptive IdentifierGenerationException instead","If using GRAPH_DEFER_IDENTITY_INSERTS=true, verify the history plan version supports deferred key handles or turn the deferral off"],"exampleFix":"// before - owner id null (no generator, never set) and history plans the collection row\nDocument doc = new Document();     // manual id, never assigned\ndoc.getSections().add(section);\nsession.persist(section);\nsession.flush();                    // history plan -> \"null key for collection: Document.sections\"\n\n// after - assign the id (or add @GeneratedValue) before persisting the graph\nDocument doc = new Document();\ndoc.setId(idGenerator.next());      // or @GeneratedValue on the id field\nsession.persist(doc);\ndoc.getSections().add(section);","handlingStrategy":"validation","validationCode":"// with audit/history collection plans on, guarantee the owner id before any mutation\nif (doc.getId() == null) {\n    doc.setId(idGenerator.next()); // or ensure @GeneratedValue is configured\n}\nsession.persist(doc);\ndoc.getSections().add(section);","typeGuard":null,"tryCatchPattern":"try {\n    session.flush();\n} catch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"null key for collection\")) {\n        // history plan needs the owner key: persist owner first, retry flush\n    } else throw e;\n}","preventionTips":["Enable the audit/temporal feature only on entities whose ids are guaranteed non-null at persist","Be careful combining history collection plans with deferred identity inserts - test the combination","Centralize id assignment for manual-id entities and validate in @PrePersist"],"tags":["collections","null-key","audit","identifiers","flush","hibernate"],"backgroundTag":"null-collection-key","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}