ModerationStateFilter unecessarily joins the content moderation state filter multiple times

Created on 10 December 2019, about 5 years ago
Updated 26 October 2023, about 1 year ago

Problem/Motivation

When adding multiple moderation state filters, the content_moderation_state table is joined multiple times unnecessarily drastically effecting performance .

One might want to add multiple moderation state filters to first exclude certain states and a second exposed filter containing the 'any' option and remaining states. This ensure the excluded states will be filtered out when the 'any' option is selected.

Query before patch

SELECT
  node_field_data.created AS node_field_data_created,
  node_field_data.nid AS nid
FROM
  node_field_data node_field_data
  LEFT JOIN content_moderation_state_field_revision content_moderation_state
    ON node_field_data.vid = content_moderation_state.content_entity_revision_id
    AND (
      content_moderation_state.content_entity_type_id = 'node'
      AND content_moderation_state.langcode = node_field_data.langcode
    )
  LEFT JOIN content_moderation_state_field_revision content_moderation_state_1
    ON node_field_data.vid = content_moderation_state_1.content_entity_revision_id
    AND (
      content_moderation_state_1.content_entity_type_id = 'node'
      AND content_moderation_state_1.langcode = node_field_data.langcode
    )
WHERE
  (node_field_data.type IN ('page'))
  AND (
    (
      (content_moderation_state.workflow = 'editorial')
      AND (content_moderation_state.moderation_state <> 'draft')
    )
    OR (
      (content_moderation_state.workflow = 'editorial')
      AND (content_moderation_state.moderation_state <> 'archived')
    )
  )
  AND (node_field_data.type IN ('page'))
  AND (
    (content_moderation_state_1.workflow = 'editorial')
    AND (content_moderation_state_1.moderation_state = 'published')
  )
ORDER BY
  node_field_data_created DESC
LIMIT 11 OFFSET 0

Query after patch

SELECT
  node_field_data.created AS node_field_data_created,
  node_field_data.nid AS nid
FROM
  node_field_data node_field_data
  LEFT JOIN content_moderation_state_field_revision content_moderation_state
    ON node_field_data.vid = content_moderation_state.content_entity_revision_id
    AND (
      content_moderation_state.content_entity_type_id = 'node'
      AND content_moderation_state.langcode = node_field_data.langcode
    )
WHERE
  (node_field_data.type IN ('page'))
  AND (
    (
      (content_moderation_state.workflow = 'editorial')
      AND (content_moderation_state.moderation_state <> 'draft')
    )
    OR (
      (content_moderation_state.workflow = 'editorial')
      AND (content_moderation_state.moderation_state <> 'archived')
    )
  )
  AND (
    (content_moderation_state.workflow = 'editorial')
    AND (content_moderation_state.moderation_state = 'published')
  )
ORDER BY
  node_field_data_created DESC
LIMIT 11 OFFSET 0

Proposed resolution

Reuse any relationships created when joining to the content_moderation_state table.

Remaining tasks

Reviews needed.

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Content moderation 

Last updated 24 days ago

Created by

🇺🇸United States jantoine

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇺🇸United States recrit

    Use Case for this bug:

    This is a fairly common use case. The default "moderated_content" view provided by Drupal core's content_moderation module has 2 "moderation_state_filter" views filters. The multiple filters do the following:
    1. (Exposed) State selection with only Draft and Needs Review as options.
    2. (Not exposed) Filters the query to only moderation states that are not Published (ie Draft and Needs Review).

    Patch review of #12:

    1. Reducing multiple LEFT JOINs to the "content_moderation_state_field_revision" table

    Code: core/modules/content_moderation/src/Plugin/views/ModerationStateJoinViewsHandlerTrait.php
    Patch change:

    --- a/core/modules/content_moderation/src/Plugin/views/ModerationStateJoinViewsHandlerTrait.php
    +++ b/core/modules/content_moderation/src/Plugin/views/ModerationStateJoinViewsHandlerTrait.php
    @@ -15,6 +15,11 @@ trait ModerationStateJoinViewsHandlerTrait {
        * {@inheritdoc}
        */
       public function ensureMyTable() {
    +    // Try to get the alias of an already joined content_moderation_state table.
    +    if (!isset($this->tableAlias)) {
    +      $this->tableAlias = $this->query->ensureTable('content_moderation_state', $this->relationship);
    +    }
    +
    

    The above change does not work when the moderation state filter is using a relationship. The "content_moderation_state" is not the table that needs to be ensured. It is the alias used in the "addRelationship()" call later in the "ensureMyTable()" method.
    In my opinion, this should check if the relationship exists already with something like the following:

        // Use our existing relationship instead of creating multiple joins.
        if (!isset($this->tableAlias) &&
            !empty($this->query->relationships['content_moderation_state']['base']) &&
            $this->query->relationships['content_moderation_state']['base'] === 'content_moderation_state_field_revision') {
          $this->tableAlias = 'content_moderation_state';
        }
    

    2. Bundles Condition:

    Code: core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php, method "opSimple()"
    This code no longer works with the latest Drupal code since the conditions are created differently. I tested on 10.1.
    This would need a recursive function in order to find the bundles condition within $this->query->where[$this->options['group']]['conditions']. It could be easier to set a flag on the $this->query that persists between filters, perhaps $this->query->addTag().

    3. Reducing multiple "INNER JOIN {node}"

    Code: core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php method "opSimple()"
    Before the bundles condition is added, the ModerationStateFilter adds a join to the entity base table (ie "node") in order to ensure that the entity bundle can be filtered. This join also occurs multiple times when there are more than one "moderation_state_filter" added to the view.
    A similar relationships check could be used if we use a more unique join alias.
    Example:

    if ($this->table === $entity_revision_base_table) {
      $base_table_join_alias = "content_moderation_state_base_{$entity_base_table}";
      if (!empty($this->query->relationships[$base_table_join_alias]['base']) &&
          $this->query->relationships[$base_table_join_alias]['base'] === $entity_revision_base_table) {
        // Re-use the existing join.
        $entity_base_table_alias = $base_table_join_alias;
      }
      else {
        // Add a new join.
        $configuration = [ ... ];
        $entity_base_table_alias = $this->query->addRelationship($base_table_join_alias, $join, $entity_revision_base_table);
      }
    }
    
  • last update about 1 year ago
    Patch Failed to Apply
  • 🇮🇳India _utsavsharma

    Patch for 11.x.

  • last update about 1 year ago
    Custom Commands Failed
  • 🇺🇸United States recrit

    @_utsavsharma The patch #23 will not work.
    1. The Drupal\content_moderation\Plugin\views\filter\Condition is not found. The condition can be created with $this->view->query->getConnection()->condition('AND');

    2. As stated in #22 🐛 ModerationStateFilter unecessarily joins the content moderation state filter multiple times Needs work , the bundles condition search needs to be a recursive search. This logic will never be TRUE - if ($condition['field'] === $field && $condition['value'] === $moderated_bundles && $condition['operator'] === 'IN') {

Production build 0.71.5 2024