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

  1. Persist the owner before or together with the element: session.persist(owner) or add cascade on the side you actually traverse
  2. Keep both sides of the bidirectional association in sync and always persist from the cascading root
  3. Ensure assigned ids are always set before persist - validate in @PrePersist or the entity factory
  4. 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

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


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