Skip to content

Saving contentlets with many-to-many relationship fields and large related-content volumes is extremely slow #36147

Description

@jcastro-dotcms

Problem Statement

Saving a contentlet whose Content Type has several relationship fields with MANY_TO_MANY cardinality becomes extremely slow once the volume of related content grows. On a real-world dataset (a parent type with ~1,500 contentlets, each related to ~20 children of another type, which means each child is related back to ~350 parents on average), a single Save/Publish takes from 40 seconds up to several minutes, generating tens of thousands of JDBC queries per save. The slowdown compounds over time: the more related content exists, the slower every save gets.

This blocks day-to-day content authoring for any installation that models catalog-style data (products ↔ categories, offers ↔ cards, articles ↔ tags, etc.) through many-to-many relationship fields.

Root causes identified

  1. Per-row relationship persistenceESContentletAPIImpl.deleteRelatedContent/relateContent delete and re-insert tree rows one at a time (a getTree + saveTree pair per related record: 2–3 queries each).
  2. Unnecessary full hydration — the save path loads every related contentlet (with all its fields, permissions, and version info) when only identifiers or counts are needed: dbRelatedContent for the delete scope, getRelatedContentFromIndex just to compute a tree-order base, and getRelatedContent inside the reindex-dependency calculation.
  3. Over-broad dependency reindexingESMappingAPIImpl.dependenciesLeftToReindex marks every currently-related contentlet for reindexing on every save (the old-state/new-state comparison degenerates because both sides read the new DB state), instead of only the relationships that were actually added or removed. Saving one child re-queues hundreds of unchanged parents, each of which is fully hydrated and remapped.

Steps to Reproduce

  1. Create two Content Types, ParentType and ChildType, related through a relationship field with MANY_TO_MANY cardinality.
  2. Bulk-create ~1,500 ParentType contentlets and ~90 ChildType contentlets, relating each parent to ~20 children (so each child ends up related to hundreds of parents).
  3. Edit any ChildType contentlet (or a ParentType one related to several children) and Save/Publish.
  4. Observe the save time and the number of JDBC queries (e.g., via Glowroot on port 4000).

Expected: the save completes in a bounded time, proportional to the relationships actually changed.
Actual: the save takes from ~40 seconds (3 related items) to 2–10 minutes (10+ related items on a large dataset), with tens of thousands of queries against tree, contentlet, and contentlet_version_info.

Acceptance Criteria

  • Saving a contentlet with relationship fields performs bulk/batched tree operations — no per-row delete/select/insert per related record.
  • Related-content identifiers are read without hydrating full contentlets when only identifiers are needed; only contentlets that actually require reindexing are loaded.
  • dependenciesLeftToReindex returns only the delta (relationships added or removed by the save); contentlets whose relationships did not change are not re-queued for reindexing.
  • Permission semantics are preserved: relationships to content the saving user cannot READ are not deleted by the save.
  • Duplicate related records in the payload (same identifier twice, or multiple language versions) do not fail the save with a primary-key violation.
  • Relationship ordering (tree_order) is preserved for both changed and preserved rows.
  • Multilingual related content (multiple index documents per identifier) does not trigger spurious reindexing.
  • Covered by integration tests (TreeFactory operations, permission preservation, duplicate handling, reindex delta, multilingual dedup).
  • Fix is backported to the 24.12.27 LTS.

dotCMS Version

Reproduced on 24.12.27 LTS and current main. Likely affects all currently supported versions.

Severity

High - Major functionality broken

Links

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    No fields configured for Bug.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions