hibernate/hibernate-orm · error · MappingException

many-to-one attribute [%s] specified delete-orphan but is no

Error message

many-to-one attribute [%s] specified delete-orphan but is not specified as unique; remove delete-orphan cascading or specify unique="true"

What it means

delete-orphan cascade is only meaningful for associations shaped like a logical one-to-one, because Hibernate must be able to treat the single referenced row as owned. For <many-to-one>, the binding counts as a logical one-to-one only when it is unique; if the cascade string contains delete-orphan and manyToOneBinding.isLogicalOneToOne() is false, binding fails. A code comment in the source notes the reverse case (uniqueness declared on the <column> rather than the <many-to-one>) can also produce false exceptions when binding is delayed.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java:1889

	private static void checkManyToOneOrphanDelete(
			MappingDocument sourceDocument,
			SingularAttributeSourceManyToOne manyToOneSource,
			ManyToOne manyToOneBinding) {
		// TODO: would be better to delay this until the end of binding (second pass, etc)
		//       in order to properly allow for a singular unique column for a many-to-one to
		//       to also trigger a "logical one-to-one". As is, this can occasionally lead to
		//       false exceptions if the many-to-one column binding is delayed and the
		//       uniqueness is indicated on the <column/> rather than on the <many-to-one/>
		//
		//       Ideally, would love to see a SimpleValue#validate approach, rather than a
		//       SimpleValue#isValid that is then handled at a higher level (Property, etc).
		//       The reason being that the current approach misses the exact reason a
		//       "validation" fails since it loses "context"
		final String cascadeStyleName = manyToOneSource.getCascadeStyleName();
		if ( cascadeStyleName != null && cascadeStyleName.contains( "delete-orphan" )
				&& !manyToOneBinding.isLogicalOneToOne() ) {
			throw new MappingException(
					"""
					many-to-one attribute [%s] specified delete-orphan but is not specified as unique; \
					remove delete-orphan cascading or specify unique="true"
					"""
							.formatted( manyToOneSource.getAttributeRole().getFullPath() ),
					sourceDocument.getOrigin()
			);
		}
	}

	private static void checkConstrainedOneToOneOrphanDelete(
			MappingDocument sourceDocument,
			SingularAttributeSourceOneToOne oneToOneSource) {
		final String cascadeStyleName = oneToOneSource.getCascadeStyleName();
		if ( cascadeStyleName != null && cascadeStyleName.contains( "delete-orphan" ) ) {
			throw new MappingException(
					"one-to-one attribute [%s] cannot specify orphan delete cascading as it is constrained"
							.formatted( oneToOneSource.getAttributeRole().getFullPath() ),

View on GitHub (pinned to fad1729dce)

Solutions

  1. Add unique='true' to the <many-to-one> element so it becomes a logical one-to-one
  2. Otherwise remove delete-orphan from the cascade attribute (keep cascade='delete' if that is what you need)
  3. If uniqueness was declared on the <column> element and the exception still fires, move unique='true' onto the <many-to-one> itself

Example fix

// before
<many-to-one name='details' class='Details' cascade='all-delete-orphan' column='details_id'/>

// after
<many-to-one name='details' class='Details' cascade='all-delete-orphan' column='details_id' unique='true'/>
Defensive patterns

Strategy: validation

Validate before calling

NodeList mtos = doc.getElementsByTagName("many-to-one");
for (int i = 0; i < mtos.getLength(); i++) {
    Element m = (Element) mtos.item(i);
    String cascade = m.getAttribute("cascade");
    boolean unique = "true".equals(m.getAttribute("unique"));
    boolean colUnique = false;
    NodeList cols = m.getElementsByTagName("column");
    for (int j = 0; j < cols.getLength(); j++) {
        colUnique |= "true".equals(((Element) cols.item(j)).getAttribute("unique"));
    }
    if (cascade.contains("delete-orphan") && !(unique || colUnique)) {
        throw new IllegalStateException("many-to-one " + m.getAttribute("name") + " uses delete-orphan without unique");
    }
}

Try / catch

catch (MappingException e) at bootstrap; the message names the attribute role. Either add unique='true' to that many-to-one or drop delete-orphan from its cascade, then rebuild.

Prevention

When it happens

Trigger: <many-to-one name='...' cascade='all-delete-orphan' ...> without unique='true' and with no unique column; uniqueness indicated only on a nested <column unique='true'/> such that the check cannot see it.

Common situations: Copying cascade='all-delete-orphan' from a one-to-many or one-to-one mapping onto a many-to-one; expecting orphan removal on the FK-holding side of a bidirectional one-to-one without marking it unique.

Related errors


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