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
- Per-row relationship persistence —
ESContentletAPIImpl.deleteRelatedContent/relateContent delete and re-insert tree rows one at a time (a getTree + saveTree pair per related record: 2–3 queries each).
- 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.
- Over-broad dependency reindexing —
ESMappingAPIImpl.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
- Create two Content Types,
ParentType and ChildType, related through a relationship field with MANY_TO_MANY cardinality.
- 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).
- Edit any
ChildType contentlet (or a ParentType one related to several children) and Save/Publish.
- 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
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
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
ESContentletAPIImpl.deleteRelatedContent/relateContentdelete and re-inserttreerows one at a time (agetTree+saveTreepair per related record: 2–3 queries each).dbRelatedContentfor the delete scope,getRelatedContentFromIndexjust to compute a tree-order base, andgetRelatedContentinside the reindex-dependency calculation.ESMappingAPIImpl.dependenciesLeftToReindexmarks 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
ParentTypeandChildType, related through a relationship field with MANY_TO_MANY cardinality.ParentTypecontentlets and ~90ChildTypecontentlets, relating each parent to ~20 children (so each child ends up related to hundreds of parents).ChildTypecontentlet (or aParentTypeone related to several children) and Save/Publish.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, andcontentlet_version_info.Acceptance Criteria
treeoperations — no per-row delete/select/insert per related record.dependenciesLeftToReindexreturns only the delta (relationships added or removed by the save); contentlets whose relationships did not change are not re-queued for reindexing.tree_order) is preserved for both changed and preserved rows.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