Skip to content

Commit 9229960

Browse files
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/DBUniqueFieldValidationStrategy.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import static com.dotmarketing.util.Constants.DONT_RESPECT_FRONT_END_ROLES;
88
import static com.liferay.util.StringPool.BLANK;
99

10-
import com.dotcms.business.CloseDBIfOpened;
1110
import com.dotcms.business.WrapInTransaction;
1211
import com.dotcms.contenttype.business.UniqueFieldValueDuplicatedException;
1312
import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategy;
@@ -63,7 +62,6 @@ public DBUniqueFieldValidationStrategy(final UniqueFieldDataBaseUtil uniqueFiel
6362

6463
@Override
6564
@WrapInTransaction
66-
@CloseDBIfOpened
6765
public void innerValidate(final Contentlet contentlet, final Field field, final Object fieldValue,
6866
final ContentType contentType) throws UniqueFieldValueDuplicatedException, DotDataException, DotSecurityException {
6967

@@ -77,7 +75,6 @@ public void innerValidate(final Contentlet contentlet, final Field field, final
7775

7876
@Override
7977
@WrapInTransaction
80-
@CloseDBIfOpened
8178
public void innerValidateInPreview(final Contentlet contentlet, final Field field, final Object fieldValue,
8279
final ContentType contentType)
8380
throws UniqueFieldValueDuplicatedException, DotDataException, DotSecurityException {

0 commit comments

Comments
 (0)