{"record":{"id":"943c8b8e79557407","repo":"hibernate/hibernate-orm","slug":"many-to-one-attribute-s-specified-delete-orphan","errorCode":null,"errorMessage":"many-to-one attribute [%s] specified delete-orphan but is not specified as unique; remove delete-orphan cascading or specify unique=\"true\"","messagePattern":"many-to-one attribute \\[(.+?)\\] specified delete-orphan but is not specified as unique; remove delete-orphan cascading or specify unique=\"true\"","errorType":"exception","errorClass":"MappingException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java","lineNumber":1889,"sourceCode":"\n\tprivate static void checkManyToOneOrphanDelete(\n\t\t\tMappingDocument sourceDocument,\n\t\t\tSingularAttributeSourceManyToOne manyToOneSource,\n\t\t\tManyToOne manyToOneBinding) {\n\t\t// TODO: would be better to delay this until the end of binding (second pass, etc)\n\t\t//       in order to properly allow for a singular unique column for a many-to-one to\n\t\t//       to also trigger a \"logical one-to-one\". As is, this can occasionally lead to\n\t\t//       false exceptions if the many-to-one column binding is delayed and the\n\t\t//       uniqueness is indicated on the <column/> rather than on the <many-to-one/>\n\t\t//\n\t\t//       Ideally, would love to see a SimpleValue#validate approach, rather than a\n\t\t//       SimpleValue#isValid that is then handled at a higher level (Property, etc).\n\t\t//       The reason being that the current approach misses the exact reason a\n\t\t//       \"validation\" fails since it loses \"context\"\n\t\tfinal String cascadeStyleName = manyToOneSource.getCascadeStyleName();\n\t\tif ( cascadeStyleName != null && cascadeStyleName.contains( \"delete-orphan\" )\n\t\t\t\t&& !manyToOneBinding.isLogicalOneToOne() ) {\n\t\t\tthrow new MappingException(\n\t\t\t\t\t\"\"\"\n\t\t\t\t\tmany-to-one attribute [%s] specified delete-orphan but is not specified as unique; \\\n\t\t\t\t\tremove delete-orphan cascading or specify unique=\"true\"\n\t\t\t\t\t\"\"\"\n\t\t\t\t\t\t\t.formatted( manyToOneSource.getAttributeRole().getFullPath() ),\n\t\t\t\t\tsourceDocument.getOrigin()\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate static void checkConstrainedOneToOneOrphanDelete(\n\t\t\tMappingDocument sourceDocument,\n\t\t\tSingularAttributeSourceOneToOne oneToOneSource) {\n\t\tfinal String cascadeStyleName = oneToOneSource.getCascadeStyleName();\n\t\tif ( cascadeStyleName != null && cascadeStyleName.contains( \"delete-orphan\" ) ) {\n\t\t\tthrow new MappingException(\n\t\t\t\t\t\"one-to-one attribute [%s] cannot specify orphan delete cascading as it is constrained\"\n\t\t\t\t\t\t\t.formatted( oneToOneSource.getAttributeRole().getFullPath() ),","sourceCodeStart":1871,"sourceCodeEnd":1907,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java#L1871-L1907","documentation":"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.","triggerScenarios":"<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.","commonSituations":"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.","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"],"exampleFix":"// before\n<many-to-one name='details' class='Details' cascade='all-delete-orphan' column='details_id'/>\n\n// after\n<many-to-one name='details' class='Details' cascade='all-delete-orphan' column='details_id' unique='true'/>","handlingStrategy":"validation","validationCode":"NodeList mtos = doc.getElementsByTagName(\"many-to-one\");\nfor (int i = 0; i < mtos.getLength(); i++) {\n    Element m = (Element) mtos.item(i);\n    String cascade = m.getAttribute(\"cascade\");\n    boolean unique = \"true\".equals(m.getAttribute(\"unique\"));\n    boolean colUnique = false;\n    NodeList cols = m.getElementsByTagName(\"column\");\n    for (int j = 0; j < cols.getLength(); j++) {\n        colUnique |= \"true\".equals(((Element) cols.item(j)).getAttribute(\"unique\"));\n    }\n    if (cascade.contains(\"delete-orphan\") && !(unique || colUnique)) {\n        throw new IllegalStateException(\"many-to-one \" + m.getAttribute(\"name\") + \" uses delete-orphan without unique\");\n    }\n}","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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"],"tags":["hibernate","hbm-mapping","cascade","many-to-one","orphan-removal"],"backgroundTag":"delete-orphan-cascade-misuse","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}