hibernate/hibernate-orm · error · UnbreakableUniqueCycleException
Unbreakable unique update cycle detected for SCC
Error message
Unbreakable unique update cycle detected for SCC: %s
What it means
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.
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.
Example fix
// before - both sides mandatory, swap is unbreakable @OneToOne(mappedBy = "a", optional = false) private B b; // after - inverse side nullable, planner can NULL-then-patch @OneToOne(mappedBy = "a") private B b;
Defensive patterns
Strategy: validation
Validate before calling
// never swap unique one-to-one partners in a single flush: sever one side first a.setOther(null); session.flush(); a.setOther(b2); b2.setOther(a);
Try / catch
try {
session.flush();
}
catch (HibernateException e) {
Throwable c = e;
while (c != null) {
if ("UnbreakableUniqueCycleException".equals(c.getClass().getSimpleName())) {
tx.rollback(); // retry with a two-step swap: null one side, flush, reassign
break;
}
c = c.getCause();
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- Unbreakable cycle detected for SCC
- Composite key breakdown not yet implemented
- There are delayed insert actions before operation as…
- Unsupported JdbcOperation type
- Action was vetoed: " + entityAction
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/8d9097473888582c.
Report an issue: GitHub.
Appendix: source
Thrown at hibernate-core/src/main/java/org/hibernate/action/queue/internal/plan/CycleBreaker.java:111
final GraphEdge preferredOrderEdge = findPreferredOrderEdge(cycle);
if (preferredOrderEdge != null) {
preferredOrderEdge.setBroken(true);
continue;
}
// Unique-only UPDATE cycles occur when swapping unique values, for example
// one-to-one relationships. Required unique edges participate in this
// classification, but only explicit NULL_PATCHABLE_UNIQUE edges can install
// a NULL-then-patch update.
if ( isUniqueUpdateOrderingCycle( cycle ) ) {
final GraphEdge uniquePatchCandidate = findUniquePatchCandidate( cycle );
if ( uniquePatchCandidate != null ) {
uniquePatchCandidate.setBroken( true );
installPatchForEdge( uniquePatchCandidate );
}
else {
throw new UnbreakableUniqueCycleException(
"Unbreakable unique update cycle detected for SCC: " + describeScc( scc )
);
}
continue;
}
final GraphEdge chosen = chooseEdgeToBreak(cycle);
if (chosen == null) {
if (isDeleteOnlyCycle(cycle)) {
breakArbitraryDeleteEdge(cycle, deferrableConstraintMode);
continue;
}
final GraphEdge updateEdge = findUpdateEdge(cycle);
if (updateEdge != null) {
updateEdge.setBroken(true);
continue;
}View on GitHub (pinned to fad1729dce)