Commit 9229960
fix(unique-fields): remove @CloseDBIfOpened to restore atomicity with contentlet save (#35567)
## Problem
Closes #35566
`DBUniqueFieldValidationStrategy.innerValidate()` was annotated with
both `@WrapInTransaction` **and** `@CloseDBIfOpened`. The combination
has a critical side-effect:
- `@CloseDBIfOpened` closes any existing JDBC connection and opens a
**fresh one**, isolated from the caller's transaction.
- `@WrapInTransaction` then starts and **commits** a new transaction on
that fresh connection.
This means the `INSERT INTO unique_fields` commits **independently** of
the outer contentlet save transaction. If the outer transaction is later
rolled back (e.g., a failure mid-save, or a startup migration task that
rolls back), the `unique_fields` row persists as a permanent **orphan**
— a hash pointing to a contentlet that no longer exists.
```
Outer transaction (contentlet save):
├── INSERT contentlet → connection A (outer tx)
├── innerValidate()
│ ├── @CloseDBIfOpened opens connection B
│ ├── INSERT unique_fields → connection B ← COMMITS HERE ✗
│ └── closes connection B
└── ROLLBACK outer tx
├── contentlet rolled back ✓
└── unique_fields row stays ✗ (already committed on B)
```
On the next contentlet save or migration run, `findVariable()` returns
empty (no contentlet exists), but `publish()` / `save()` hits a
duplicate-key violation on `unique_fields_pkey`, incorrectly blocking a
valid operation.
## Fix
Remove `@CloseDBIfOpened` from `innerValidate()` and
`innerValidateInPreview()`.
With only `@WrapInTransaction`, the interceptor **joins the caller's
existing transaction** if one is active. The `unique_fields` INSERT and
the contentlet save are now **atomic** — a rollback of the outer
transaction rolls back both.
When no outer transaction is present, `@WrapInTransaction` starts its
own, preserving the existing behavior.
```
Outer transaction (contentlet save):
├── INSERT contentlet → connection A (outer tx)
├── innerValidate() (@WrapInTransaction joins outer tx)
│ └── INSERT unique_fields → connection A (same tx) ✓
└── COMMIT/ROLLBACK outer tx
├── contentlet committed/rolled back ✓
└── unique_fields committed/rolled back ✓ (atomic)
```
## Impact
- Prevents new orphaned `unique_fields` rows from being created on any
contentlet save rollback.
- Existing orphans in the database are not cleaned up by this PR (a
separate cleanup utility or migration would be needed for that).
- The companion PR #35565 addresses the cascade blast radius caused by
existing orphans in the language variable migration.
## Files changed
-
`dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategy.java`
- Removed `@CloseDBIfOpened` from `innerValidate()` and
`innerValidateInPreview()`
- Removed unused `import com.dotcms.business.CloseDBIfOpened`
## Test plan
- [ ] Save a contentlet with a unique field — verify `unique_fields` row
is inserted
- [ ] Roll back the save (e.g., via an outer transaction) — verify the
`unique_fields` row is also absent after rollback
- [ ] Run existing `DBUniqueFieldValidationStrategy` integration tests —
no regressions
- [ ] Verify that duplicate-key enforcement still works correctly after
the change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>1 parent 47956c3 commit 9229960
1 file changed
Lines changed: 0 additions & 3 deletions
File tree
- dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable
Lines changed: 0 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | 10 | | |
12 | 11 | | |
13 | 12 | | |
| |||
63 | 62 | | |
64 | 63 | | |
65 | 64 | | |
66 | | - | |
67 | 65 | | |
68 | 66 | | |
69 | 67 | | |
| |||
77 | 75 | | |
78 | 76 | | |
79 | 77 | | |
80 | | - | |
81 | 78 | | |
82 | 79 | | |
83 | 80 | | |
| |||
0 commit comments