hibernate/hibernate-orm · error · IllegalArgumentException
null key for collection: %s
Error message
null key for collection: %s
What it means
SingleRowInsertBindPlan is the bind plan the graph flush planner uses for inserting one collection row. Its bindValues (SingleRowInsertBindPlan.java:73) guards the owner key first: null key means there is no FK value to bind for the row, so it throws IllegalArgumentException("null key for collection: <role>"). Like the other null-key guards, the collection is being written while its owner's identifier is null.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/action/queue/internal/decompose/collection/SingleRowInsertBindPlan.java:73
SharedSessionContractImplementor session) {
return CollectionUniqueKeyValueExtractor.extractValues(
persister,
collection,
key,
entry,
entryIndex,
constraint,
session
);
}
@Override
public void bindValues(
JdbcValueBindings jdbcValueBindings,
FlushOperation flushOperation,
SharedSessionContractImplementor session) {
if ( key == null ) {
throw new IllegalArgumentException( "null key for collection: " + persister.getNavigableRole().getFullPath() );
}
values.applyValues( collection, key, entry, entryIndex, session, jdbcValueBindings );
}
}
View on GitHub (pinned to fad1729dce)
Solutions
- Persist the owner before or together with the element: session.persist(owner) or add cascade on the side you actually traverse
- Keep both sides of the bidirectional association in sync and always persist from the cascading root
- Ensure assigned ids are always set before persist - validate in @PrePersist or the entity factory
- Guard custom IdentifierGenerator.generate() to never return null
Example fix
// before - only the child persisted; owner has no id; insert plan needs a null FK Parent p = new Parent(); // transient, no cascade reaching it p.getChildren().add(child); session.persist(child); session.flush(); // -> "null key for collection: Parent.children" // after - persist the owner, cascade saves children @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) private List<Child> children = new ArrayList<>(); session.persist(p); // p.getChildren().add(child) beforehand
Defensive patterns
Strategy: validation
Validate before calling
// persist from the cascading root so the owner key exists when rows bind @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) List<Child> children = new ArrayList<>(); Parent p = new Parent(); p.getChildren().add(child); child.setParent(p); session.persist(p); // owner id generated; child insert binds a real FK
Try / catch
try {
session.flush();
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().startsWith("null key for collection")) {
// single-row insert had no owner FK: persist the owner and re-run
} else throw e;
} Prevention
- Persist aggregates from the root and let CascadeType.ALL reach children
- Keep both sides of bidirectional associations synchronized in helper methods
- Reject code paths that persist collection elements without their owner
When it happens
Trigger: Adding an element to a collection whose owning entity has no id yet (transient owner without cascade from a saved parent; assigned id not set; null-returning generator) and flushing - the planner creates a SingleRowInsertBindPlan and the key check fails at JDBC binding time.
Common situations: One-directional cascades that persist the element but not the owner; new-parent test flows that forget session.persist(parent); code that clears ids for 'clone' operations; generators with edge-case null returns after upgrades.
Related errors
- null key for collection: %s
- null key for collection: %s
- cannot recreate collection while filter is enabled [%s : %s]
- cannot recreate collection while filter is enabled: " + coll
- Decomposition not supported for %s
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/dccf58ba0373025f.
Report an issue: GitHub.