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
- Add unique='true' to the <many-to-one> element so it becomes a logical one-to-one
- Otherwise remove delete-orphan from the cascade attribute (keep cascade='delete' if that is what you need)
- 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
- Reserve delete-orphan for one-to-many and true one-to-one associations
- When using delete-orphan on a many-to-one, always mark the FK unique
- Prefer JPA orphanRemoval on @OneToMany/@OneToOne over hbm cascade strings in new code
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
- one-to-one attribute [%s] cannot specify orphan delete casca
- entity name referenced by many-to-one required [%s]
- Cannot lazily initialize collection (collection is being rem
- deleted object would be re-saved by cascade (remove deleted
- Instance of '" + entityName + "' references an unsaved trans
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/943c8b8e79557407.
Report an issue: GitHub.