Unverified Commit 223992a2 authored by Alex Pott's avatar Alex Pott
Browse files

perf: #3551308 Combine single cardinality fields into a single database query when loading entities

By: heddn
By: catch
By: godotislate
By: alexpott
parent bdf59040
Loading
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
@@ -1222,7 +1222,97 @@ protected function loadFromDedicatedTables(array &$values, $load_from_revision)

    // Load field data.
    $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));

    $single_cardinality_fields = [];
    $multiple_cardinality_fields = [];

    foreach ($storage_definitions as $field_name => $storage_definition) {
      if ($storage_definition->getCardinality() === 1) {
        $single_cardinality_fields[$field_name] = $storage_definition;
      }
      else {
        $multiple_cardinality_fields[$field_name] = $storage_definition;
      }
    }
    if ($single_cardinality_fields) {
      $id_key = !$load_from_revision ? 'entity_id' : 'revision_id';

      // Because any field could potentially have no data, we need to begin
      // the query from a table that will reliably exist, which means the base,
      // data, or revision table.
      $base_table = !$load_from_revision ? ($this->dataTable ?? $this->baseTable) : $this->revisionDataTable ?? $this->revisionTable;
      $base_id_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
      $query = $this->database->select($base_table, $base_table)
        ->fields($base_table, [$base_id_key])
        ->condition("[$base_table].[$base_id_key]", $ids, 'IN');

      // If the entity is translatable, ensure only rows with valid langcodes
      // are loaded.
      if ($this->langcodeKey) {
        $query->condition("[$base_table].[$this->langcodeKey]", $langcodes, 'IN');
        $query->addField($base_table, $this->langcodeKey);
      }

      // Add a left join for each single cardinality field.
      foreach ($single_cardinality_fields as $field_name => $storage_definition) {
        $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
        // If the entity is translatable, add the langcode to the join and
        // a condition on valid langcodes.
        if ($this->langcodeKey) {
          $query->leftJoin($table, $table, "[$table].[$id_key] = [$base_table].[$base_id_key] AND [$table].[langcode] = [$base_table].[$this->langcodeKey] AND [$table].[deleted] = 0");
        }
        else {
          $query->leftJoin($table, $table, "[$table].[$id_key] = [$base_table].[$base_id_key] AND [$table].[deleted] = 0");
        }
        $query->fields($table, $this->tableMapping->getColumnNames($field_name));
      }

      $results = $query->execute();

      $is_not_null = fn($value) => !is_null($value);
      foreach ($results as $row) {
        $row = (array) $row;
        $value_key = $row[$base_id_key];
        // Field values in default language are stored with
        // LanguageInterface::LANGCODE_DEFAULT as key.
        $langcode = LanguageInterface::LANGCODE_DEFAULT;
        if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row[$this->langcodeKey] != $default_langcodes[$value_key]) {
          $langcode = $row[$this->langcodeKey];
        }

        foreach ($single_cardinality_fields as $field_name => $storage_definition) {
          $bundle = $this->bundleKey ? $values[$value_key][$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId;
          $field_values = array_intersect_key($row, array_flip($this->tableMapping->getColumnNames($field_name)));

          // If all the field values are null, then there was no result for this
          // field.
          if (!array_any($field_values, $is_not_null)) {
            continue;
          }
          if (!isset($values[$value_key][$field_name][$langcode])) {
            $values[$value_key][$field_name][$langcode] = [];
          }

          // Ensure that records for non-translatable fields having invalid
          // languages are skipped.
          if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
            if (empty($values[$value_key][$field_name][$langcode])) {
              $item = [];
              // For each column declared by the field, populate the item from
              // the prefixed database column.
              foreach ($storage_definition->getColumns() as $column => $attributes) {
                $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
                // Unserialize the value if specified in the column schema.
                $item[$column] = (!empty($attributes['serialize'])) ? $this->handleNullableFieldUnserialize($row[$column_name]) : $row[$column_name];
              }
              // Add the item to the field values for the entity.
              $values[$value_key][$field_name][$langcode][] = $item;
            }
          }
        }
      }
    }
    foreach ($multiple_cardinality_fields as $field_name => $storage_definition) {
      $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);

      // Ensure that only values having valid languages are retrieved. Since we
+3 −2
Original line number Diff line number Diff line
@@ -3601,8 +3601,9 @@ protected function grantIncludedPermissions(array $include_paths = []) {
   * @todo Remove this after https://www.drupal.org/project/drupal/issues/3038706 lands.
   */
  protected function entityLoadUnchanged($id) {
    $this->entityStorage->resetCache();
    return $this->entityStorage->loadUnchanged($id);
    $entity_storage = \Drupal::service('entity_type.manager')->getStorage(static::$entityTypeId);
    $entity_storage->resetCache();
    return $entity_storage->loadUnchanged($id);
  }

  /**
+2 −2
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ public function testGetIndividual(): void {
      'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
      'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)',
      'SELECT "revision".* FROM "node_field_revision" "revision" WHERE ("revision"."nid" IN (1)) AND ("revision"."vid" IN ("1")) ORDER BY "revision"."nid" ASC',
      'SELECT "t".* FROM "node__body" "t" WHERE ("entity_id" IN (1)) AND ("deleted" = 0) AND ("langcode" IN ("en", "und", "zxx")) ORDER BY "delta" ASC',
      'SELECT "node_field_data"."nid" AS "nid", "node_field_data"."langcode" AS "langcode", "node__body"."body_value" AS "body_value", "node__body"."body_summary" AS "body_summary", "node__body"."body_format" AS "body_format" FROM "node_field_data" "node_field_data" LEFT OUTER JOIN "node__body" "node__body" ON "node__body"."entity_id" = "node_field_data"."nid" AND "node__body"."langcode" = "node_field_data"."langcode" AND "node__body"."deleted" = 0 WHERE ("node_field_data"."nid" IN (1)) AND ("node_field_data"."langcode" IN ("en", "und", "zxx"))',
      'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/jsonapi%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0',
      'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.node_type.relationship.get" )',
      'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.node_type.related" )',
@@ -194,7 +194,7 @@ public function testGetIndividual(): void {
      'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
      'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)',
      'SELECT "revision".* FROM "node_field_revision" "revision" WHERE ("revision"."nid" IN (1)) AND ("revision"."vid" IN ("1")) ORDER BY "revision"."nid" ASC',
      'SELECT "t".* FROM "node__body" "t" WHERE ("entity_id" IN (1)) AND ("deleted" = 0) AND ("langcode" IN ("en", "und", "zxx")) ORDER BY "delta" ASC',
      'SELECT "node_field_data"."nid" AS "nid", "node_field_data"."langcode" AS "langcode", "node__body"."body_value" AS "body_value", "node__body"."body_summary" AS "body_summary", "node__body"."body_format" AS "body_format" FROM "node_field_data" "node_field_data" LEFT OUTER JOIN "node__body" "node__body" ON "node__body"."entity_id" = "node_field_data"."nid" AND "node__body"."langcode" = "node_field_data"."langcode" AND "node__body"."deleted" = 0 WHERE ("node_field_data"."nid" IN (1)) AND ("node_field_data"."langcode" IN ("en", "und", "zxx"))',
      'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
      'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
      'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
+1 −1
Original line number Diff line number Diff line
@@ -128,7 +128,7 @@ protected function doTestNodePageAdministrator(): void {
    }, 'administratorNodePage');

    $expected = [
      'QueryCount' => 354,
      'QueryCount' => 331,
      'CacheGetCount' => 349,
      'CacheGetCountByBin' => [
        'config' => 91,
+2 −2
Original line number Diff line number Diff line
@@ -52,7 +52,7 @@ protected function testFrontPageColdCache(): void {
    $this->assertSession()->pageTextContains('Umami');

    $expected = [
      'QueryCount' => 263,
      'QueryCount' => 245,
      'CacheGetCount' => 316,
      'CacheSetCount' => 315,
      'CacheDeleteCount' => 0,
@@ -122,7 +122,7 @@ protected function testFrontPageCoolCache(): void {
    }, 'umamiFrontPageCoolCache');

    $expected = [
      'QueryCount' => 93,
      'QueryCount' => 80,
      'CacheGetCount' => 181,
      'CacheSetCount' => 79,
      'CacheDeleteCount' => 0,
Loading