hibernate/hibernate-orm · error · IllegalArgumentException
null key for collection: %s
Error message
null key for collection: %s
What it means
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.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/action/queue/internal/decompose/collection/HistoryCollectionMutationPlanContributor.java:177
Object key,
Object entry,
int entryIndex,
TemporalMapping temporalMapping) {
this.persister = persister;
this.collection = collection;
this.key = key;
this.entry = entry;
this.entryIndex = entryIndex;
this.temporalMapping = temporalMapping;
}
@Override
public void bindValues(
JdbcValueBindings jdbcValueBindings,
FlushOperation flushOperation,
SharedSessionContractImplementor session) {
if ( key == null ) {
throw new IllegalArgumentException( "null key for collection: " + persister.getNavigableRole().getFullPath() );
}
bindRowValues( jdbcValueBindings, session );
if ( TemporalMutationHelper.isUsingParameters( session ) ) {
jdbcValueBindings.bindValue(
session.getCurrentChangesetIdentifier(),
temporalMapping.getStartingColumnMapping().getSelectionExpression(),
ParameterUsage.SET
);
}
}
private void bindRowValues(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) {
final var attributeMapping = persister.getAttributeMapping();
attributeMapping.getKeyDescriptor().getKeyPart().decompose(
key,
jdbcValueBindings::bindAssignment,
session
);View on GitHub (pinned to fad1729dce)
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
Example fix
// before - owner id null (no generator, never set) and history plans the collection row Document doc = new Document(); // manual id, never assigned doc.getSections().add(section); session.persist(section); session.flush(); // history plan -> "null key for collection: Document.sections" // after - assign the id (or add @GeneratedValue) before persisting the graph Document doc = new Document(); doc.setId(idGenerator.next()); // or @GeneratedValue on the id field session.persist(doc); doc.getSections().add(section);
Defensive patterns
Strategy: validation
Validate before calling
// with audit/history collection plans on, guarantee the owner id before any mutation
if (doc.getId() == null) {
doc.setId(idGenerator.next()); // or ensure @GeneratedValue is configured
}
session.persist(doc);
doc.getSections().add(section); Try / catch
try {
session.flush();
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().startsWith("null key for collection")) {
// history plan needs the owner key: persist owner first, retry flush
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- null key for collection: %s
- null key for collection: %s
- cannot recreate collection while filter is enabled: " + coll
- Audit graph mutation plan used with non-graph action queue
- Audit graph mutation plan used with non-graph action queue
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/57fae961e21b650c.
Report an issue: GitHub.