diff --git a/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php index 2c5ca86..268d555 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php +++ b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php @@ -3,8 +3,8 @@ namespace Drupal\Core\Entity; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -82,7 +82,7 @@ public function convertSchema($entity_type_id, $options = []) { ]; $options = array_merge($default_options, $options); $this->updateEntityType($entity_type_id, $options); - $this->createTables($entity_type_id, $options); + $this->createTables($entity_type_id); $this->installRevisionableFields($entity_type_id, $options); } @@ -91,8 +91,8 @@ public function convertSchema($entity_type_id, $options = []) { */ public function copyData(EntityTypeInterface $entity_type, array &$sandbox) { // If 'progress' is not set, then this will be the first run of the batch. + $base_table = $entity_type->getBaseTable(); if (!isset($sandbox['progress'])) { - $base_table = $entity_type->getBaseTable(); $sandbox['progress'] = 0; $sandbox['current_id'] = 0; $sandbox['max'] = $this->database->select($base_table) @@ -101,52 +101,104 @@ public function copyData(EntityTypeInterface $entity_type, array &$sandbox) { ->fetchField(); } - /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */ - $table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping(); - $table_names = $table_mapping->getTableNames(); $id = $entity_type->getKey('id'); - $revision_id = $entity_type->getKey('revision'); - - $data = []; - // Loop through all tables for the entity type and combine the data. - foreach ($table_names as $table_name) { - $column_names = $table_mapping->getAllColumns($table_name); - // Process 5 entities per batch. - $results = $this->database->select($table_name, 't') - ->fields('t') - ->condition($id, $sandbox['current_id'], '>') - ->range(0, 5) - ->orderBy($id) - ->execute() - ->fetchAll(); - foreach ($results as $key => $result) { - foreach ($column_names as $column_name) { - if (!empty($result->{$column_name})) { - $data[$key][$column_name] = $result->{$column_name}; - } - } - } + + // Get the next 5 entity IDs to migrate. + $entity_ids = $this->database->select($base_table) + ->fields($base_table, [$id]) + ->condition($id, $sandbox['current_id'], '>') + ->range(0, 5) + ->execute() + ->fetchAllKeyed(0, 0); + + foreach ($entity_ids as $entity_id) { + $this->copySingleEntity($entity_type, $entity_id); + $sandbox['progress']++; + $sandbox['current_id'] = $entity_id; } + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + } + + protected function getTableData($table_name, array $conditions, $id_key) { + $query = $this->database->select($table_name, 't') + ->fields('t'); + + foreach ($conditions as $field => $value) { + $query->condition($field, $value); + } + + return $query->execute()->fetchAllAssoc($id_key); + } + + protected function copySingleEntity(EntityTypeInterface $entity_type, $entity_id) { + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityTypeManager->getStorage($entity_type->id()) + ->getTableMapping(); - // Loop through all the collected data and update / insert missing rows. - foreach ($data as $record) { - if (!empty($record[$id])) { - $record[$revision_id] = $record[$id]; + $id_key = $entity_type->getKey('id'); + $revision_key = $entity_type->getKey('revision'); + + // Copy data to the revision base table. + $base_table = $entity_type->getBaseTable(); + $revision_table = $entity_type->getRevisionTable(); + $revision_column_names = $table_mapping->getAllColumns($revision_table); + + $base_table_data = $this->getTableData($base_table, [ + $id_key => $entity_id, + ], $id_key); + $this->database->upsert($base_table) + ->key($id_key) + ->fields((array) reset($base_table_data) + [$revision_key => $entity_id]) + ->execute(); + $this->database->upsert($revision_table) + ->key($id_key) + ->fields(array_intersect_key((array) reset($base_table_data) + [$revision_key => $entity_id], array_flip($revision_column_names))) + ->execute(); + + // Copy data to the revision data table. + if ($data_table = $entity_type->getDataTable()) { + $revision_data_table = $entity_type->getRevisionDataTable(); + $revision_data_column_names = $table_mapping->getAllColumns($revision_data_table); + $data_table_data = $this->getTableData($data_table, [$id_key => $entity_id], $id_key); + + // Note: These values could be by language. + foreach ($data_table_data as $values) { + $this->database->upsert($data_table) + ->key($id_key) + ->fields((array) $values + [$revision_key => $entity_id]) + ->execute(); + + $this->database->upsert($revision_data_table) + ->key($id_key) + ->fields(array_intersect_key((array) $values + [$revision_key => $entity_id], array_flip($revision_data_column_names))) + ->execute(); } - foreach ($table_names as $table_name) { - $values = []; - $column_names = $table_mapping->getAllColumns($table_name); - foreach ($column_names as $column_name) { - if (!empty($record[$column_name])) { - $values[$column_name] = $record[$column_name]; - } - } - $this->database->upsert($table_name)->key('id')->fields($values)->execute(); + } + + // Copy data to the dedicated tables. + $dedicated_fields = array_filter($this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()), function (FieldStorageDefinitionInterface $field_storage) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($field_storage); + }); + + foreach ($dedicated_fields as $dedicated_field => $field_storage) { + $dedicated_table_name = $table_mapping->getFieldTableName($dedicated_field); + $dedicated_revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage); + $dedicated_revision_column_names = $table_mapping->getAllColumns($dedicated_revision_table); + $dedicated_table_data = $this->getTableData($dedicated_table_name, ['entity_id' => $entity_id], 'entity_id'); + + // Note: These values could be by language and delta. + foreach ($dedicated_table_data as $values) { + $this->database->upsert($dedicated_table_name) + ->key('entity_id') + ->fields((array) $values + ['revision_id' => $entity_id]) + ->execute(); + + $this->database->upsert($dedicated_revision_table) + ->key('entity_id') + ->fields(array_intersect_key((array) $values + ['revision_id' => $entity_id], array_flip($dedicated_revision_column_names))) + ->execute(); } - $sandbox['progress']++; - $sandbox['current_id'] = $record['id']; } - $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); } /** @@ -176,7 +228,7 @@ protected function updateEntityType($entity_type_id, $options) { protected function createTables($entity_type_id) { $storage = $this->entityTypeManager->getStorage($entity_type_id); $entity_type = $this->entityDefinitionUpdateManager->getEntityType($entity_type_id); - if ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) { + if (method_exists($storage, 'entityTypeResolveMissingSchema')) { $storage->entityTypeResolveMissingSchema($entity_type); } } @@ -193,6 +245,7 @@ protected function installRevisionableFields($entity_type_id, $options) { $revision_id = BaseFieldDefinition::create('integer') ->setLabel(new TranslatableMarkup('Revision ID')) ->setReadOnly(TRUE) + // @todo Fix the entity key to be unassigned as well. ->setSetting('unsigned', TRUE); $this->entityDefinitionUpdateManager ->installFieldStorageDefinition($options['revision'], $entity_type_id, $entity_type_id, $revision_id);