diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index f34db1122c..e68ccb7a8a 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -543,6 +543,14 @@ protected function doPostSave(EntityInterface $entity, $update) { unset($entity->original); } + /** + * {@inheritdoc} + */ + public function restore(EntityInterface $entity) { + // The restore process does not invoke any pre-save or post-save operations. + $this->doSave($entity->id(), $entity); + } + /** * Builds an entity query. * diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index 1db273945a..9ceb1b09ac 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -154,6 +154,23 @@ public function delete(array $entities); */ public function save(EntityInterface $entity); + /** + * Restores a previously saved entity. + * + * Note that the entity is assumed to be in a valid state for the storage, so + * the restore process does not invoke any hooks, nor does it perform any + * pre-save or post-save operations. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to restore. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * In case of failures, an exception is thrown. + * + * @internal + */ + public function restore(EntityInterface $entity); + /** * Determines if the storage contains any data. * diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 59666b63c7..9bbbf86eb8 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -787,6 +787,55 @@ public function save(EntityInterface $entity) { } } + /** + * {@inheritdoc} + */ + public function restore(EntityInterface $entity) { + $transaction = $this->database->startTransaction(); + try { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + + // Insert the entity data in the base table only for default revisions. + if ($entity->isDefaultRevision()) { + $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); + $this->database + ->insert($this->baseTable) + ->fields((array) $record) + ->execute(); + } + + // Insert the entity data in the revision table. + if ($this->revisionTable) { + $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable); + $this->database + ->insert($this->revisionTable) + ->fields((array) $record) + ->execute(); + } + + // Insert the entity data in the data table only for default revisions. + if ($this->dataTable && $entity->isDefaultRevision()) { + $this->saveToSharedTables($entity); + } + + // Insert the entity data in the revision data table. + if ($this->revisionDataTable) { + $this->saveToSharedTables($entity, $this->revisionDataTable); + } + + // Insert the entity data in the dedicated tables. + $this->saveToDedicatedTables($entity, FALSE, []); + + // Ignore replica server temporarily. + \Drupal::service('database.replica_kill_switch')->trigger(); + } + catch (\Exception $e) { + $transaction->rollBack(); + watchdog_exception($this->entityTypeId, $e); + throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index b3313dd0be..a51518190b 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -416,7 +416,8 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E // Make sure that each storage object has a proper table mapping. /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $temporary_storage */ $temporary_storage = &$sandbox['temporary_storage']; - $temporary_table_mapping = $temporary_storage->getCustomTableMapping($entity_type, $field_storage_definitions, 'tmp_'); + $temporary_prefix = static::getTemporaryTableMappingPrefix($entity_type, $field_storage_definitions); + $temporary_table_mapping = $temporary_storage->getCustomTableMapping($entity_type, $field_storage_definitions, $temporary_prefix); $temporary_storage->setTableMapping($temporary_table_mapping); /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $original_storage */ @@ -553,6 +554,32 @@ protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInt } } + /** + * Gets a string to be used as a prefix for a temporary table mapping object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * An array of field storage definitions. + * + * @return string + * A temporary table mapping prefix. + * + * @internal + */ + public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entity_type, array $field_storage_definitions) { + // Construct a unique prefix based on the contents of the entity type and + // field storage definitions. + $prefix_parts[] = spl_object_hash($entity_type); + foreach ($field_storage_definitions as $storage_definition) { + $prefix_parts[] = spl_object_hash($storage_definition); + } + $prefix_parts[] = \Drupal::time()->getRequestTime(); + $hash = hash('sha256', implode('', $prefix_parts)); + + return 'tmp_' . substr($hash, 0, 6); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityTypeSchemaListenerTrait.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityTypeSchemaListenerTrait.php index b0196706f4..9834cd0639 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityTypeSchemaListenerTrait.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityTypeSchemaListenerTrait.php @@ -201,12 +201,8 @@ protected function copyData(EntityTypeInterface $entity_type, EntityTypeInterfac $entity->set($revision_translation_affected_key, TRUE); } - // Treat the entity as new in order to make the storage do an INSERT - // rather than an UPDATE. - $entity->enforceIsNew(TRUE); - // Finally, save the entity in the temporary storage. - $temporary_storage->save($entity); + $temporary_storage->restore($entity); } catch (\Exception $e) { $this->handleEntityTypeSchemaUpdateExceptionOnDataCopy($entity_type, $original, $sandbox); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionSchemaUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionSchemaUpdateTest.php index 451b462d40..3f4cbbb137 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionSchemaUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionSchemaUpdateTest.php @@ -158,9 +158,13 @@ public function providerTestEntityTypeSchemaUpdates() { 'new_mul' => TRUE, 'data_migration_supported' => FALSE, ], - // @todo Entity schema updates with data migration for revisionable entity - // types is not supported yet. - // 'rev non_mul to rev mul' => [], + 'rev non_mul to rev mul' => [ + 'initial_rev' => TRUE, + 'initial_mul' => FALSE, + 'new_rev' => TRUE, + 'new_mul' => TRUE, + 'data_migration_supported' => TRUE, + ], 'non_rev mul to non_rev non_mul' => [ 'initial_rev' => FALSE, 'initial_mul' => TRUE, @@ -289,12 +293,13 @@ protected function assertEntityData($revisionable, $translatable) { } if ($revisionable) { - $revisions = $this->entityTypeManager->getStorage($this->entityTypeId)->loadMultipleRevisions(); - $this->assertCount(9, $revisions); + $revisions_result = $this->entityTypeManager->getStorage($this->entityTypeId)->getQuery()->allRevisions()->execute(); + $revisions = $this->entityTypeManager->getStorage($this->entityTypeId)->loadMultipleRevisions(array_keys($revisions_result)); + $this->assertCount(6, $revisions); foreach ($revisions as $revision) { /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ - $revision_label = $revision->isDefaultRevision() ?: ' - rev2'; + $revision_label = $revision->isDefaultRevision() ? NULL : ' - rev2'; $this->assertEquals("test entity - {$revision->id()} - en{$revision_label}", $revision->label()); $this->assertEquals("dedicated table - {$revision->id()} - value 1 - en{$revision_label}", $revision->test_multiple_properties_multiple_values->value1); $this->assertEquals("dedicated table - {$revision->id()} - value 2 - en{$revision_label}", $revision->test_multiple_properties_multiple_values->value2);