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

  1. Persist the owning entity first so its identifier exists before any collection mutation is planned
  2. For assigned ids, guarantee assignment in one place (factory/@PrePersist check) so a flush can never see a null owner id
  3. Fix custom generators that may return null; throw a descriptive IdentifierGenerationException instead
  4. 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

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


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