{"record":{"id":"8d9097473888582c","repo":"hibernate/hibernate-orm","slug":"unbreakable-unique-update-cycle-detected-for-scc","errorCode":null,"errorMessage":"Unbreakable unique update cycle detected for SCC: %s","messagePattern":"Unbreakable unique update cycle detected for SCC: (.+?)","errorType":"exception","errorClass":"UnbreakableUniqueCycleException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/action/queue/internal/plan/CycleBreaker.java","lineNumber":111,"sourceCode":"\n\t\t\tfinal GraphEdge preferredOrderEdge = findPreferredOrderEdge(cycle);\n\t\t\tif (preferredOrderEdge != null) {\n\t\t\t\tpreferredOrderEdge.setBroken(true);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Unique-only UPDATE cycles occur when swapping unique values, for example\n\t\t\t// one-to-one relationships. Required unique edges participate in this\n\t\t\t// classification, but only explicit NULL_PATCHABLE_UNIQUE edges can install\n\t\t\t// a NULL-then-patch update.\n\t\t\tif ( isUniqueUpdateOrderingCycle( cycle ) ) {\n\t\t\t\tfinal GraphEdge uniquePatchCandidate = findUniquePatchCandidate( cycle );\n\t\t\t\tif ( uniquePatchCandidate != null ) {\n\t\t\t\t\tuniquePatchCandidate.setBroken( true );\n\t\t\t\t\tinstallPatchForEdge( uniquePatchCandidate );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new UnbreakableUniqueCycleException(\n\t\t\t\t\t\t\t\"Unbreakable unique update cycle detected for SCC: \" + describeScc( scc )\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfinal GraphEdge chosen = chooseEdgeToBreak(cycle);\n\t\t\tif (chosen == null) {\n\t\t\t\tif (isDeleteOnlyCycle(cycle)) {\n\t\t\t\t\tbreakArbitraryDeleteEdge(cycle, deferrableConstraintMode);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfinal GraphEdge updateEdge = findUpdateEdge(cycle);\n\t\t\t\tif (updateEdge != null) {\n\t\t\t\t\tupdateEdge.setBroken(true);\n\t\t\t\t\tcontinue;\n\t\t\t\t}","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/action/queue/internal/plan/CycleBreaker.java#L93-L129","documentation":"The graph-based flush planner detected a cycle made up only of UPDATE operations ordered by unique constraints - the classic one-to-one unique value swap - but no edge in the cycle is an explicit NULL_PATCHABLE_UNIQUE edge. Since every unique edge in the cycle is required (non-nullable), Hibernate cannot apply its NULL-then-patch strategy (temporarily null the unique column, run the updates, then patch the final value), so it throws UnbreakableUniqueCycleException rather than emit SQL that would violate the constraint.","triggerScenarios":"Swapping one-to-one references inside one un-flushed transaction (a.setOther(b2) plus b2.setOther(a), or reassigning both ends of a bidirectional one-to-one) where the unique FK columns are mapped optional=false / @JoinColumn(nullable=false) on both sides; any single-flush reassignment of NOT NULL unique foreign keys that forms a cycle.","commonSituations":"Bidirectional one-to-one with mandatory unique FKs on both tables; models migrated from the legacy action queue where hard-coded ordering happened to work; databases that cannot defer unique constraint checks (MySQL, MariaDB, SQL Server).","solutions":["Make one side of the one-to-one nullable: drop optional=false / nullable=false on one end so the planner can NULL the unique value first and patch it after the swap.","Model the association as unidirectional (unique FK owned by one side only) so no unique cycle exists.","Split the swap into two steps with an intermediate flush: null one side, flush, then assign the new partner.","On databases that support it, declare the unique constraint DEFERRABLE and enable Hibernate's deferrable constraint mode so the planner can break the edge."],"exampleFix":"// before - both sides mandatory, swap is unbreakable\n@OneToOne(mappedBy = \"a\", optional = false)\nprivate B b;\n// after - inverse side nullable, planner can NULL-then-patch\n@OneToOne(mappedBy = \"a\")\nprivate B b;","handlingStrategy":"validation","validationCode":"// never swap unique one-to-one partners in a single flush: sever one side first\na.setOther(null);\nsession.flush();\na.setOther(b2);\nb2.setOther(a);","typeGuard":null,"tryCatchPattern":"try {\n    session.flush();\n}\ncatch (HibernateException e) {\n    Throwable c = e;\n    while (c != null) {\n        if (\"UnbreakableUniqueCycleException\".equals(c.getClass().getSimpleName())) {\n            tx.rollback(); // retry with a two-step swap: null one side, flush, reassign\n            break;\n        }\n        c = c.getCause();\n    }\n}","preventionTips":["Avoid optional=false / nullable=false on both sides of a bidirectional one-to-one","Reassign one-to-one references in two steps with a flush between when partners are swapped","On PostgreSQL, mark swap-heavy unique constraints DEFERRABLE and enable Hibernate's deferrable constraint mode"],"tags":["hibernate","flush","one-to-one","unique-constraint","cycle","action-queue"],"backgroundTag":"one-to-one-unique-swap-cycle","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}