Issue #3024728 by amateescu, plach: Preserve the backup tables after an entity type schema conversion process

(cherry picked from commit d938b0a8ec)
8.7.x
catch 2019-04-11 20:31:40 +09:00
parent cd16c34cd5
commit d0b603465c
3 changed files with 126 additions and 13 deletions

View File

@ -17,6 +17,7 @@ use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Site\Settings;
/**
* Defines a schema handler that supports revisionable, translatable entities.
@ -95,6 +96,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
*/
protected $installedStorageSchema;
/**
* The key-value collection for tracking entity update backup repository.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $updateBackupRepository;
/**
* The deleted fields repository.
*
@ -163,6 +171,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
return $this->deletedFieldsRepository;
}
/**
* Gets the key/value collection for tracking the entity update backups.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* A key/value collection.
*
* @todo Inject this dependency in the constructor once this class can be
* instantiated as a regular entity handler.
* @see https://www.drupal.org/node/2332857
*/
protected function updateBackupRepository() {
if (!isset($this->updateBackupRepository)) {
$this->updateBackupRepository = \Drupal::keyValue('entity.update_backup');
}
return $this->updateBackupRepository;
}
/**
* Refreshes the table mapping with updated definitions.
*
@ -432,7 +457,11 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$sandbox['temporary_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, $temporary_prefix);
$sandbox['new_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
$sandbox['original_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions);
$sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, 'old_');
$backup_prefix = static::getTemporaryTableMappingPrefix($original, $original_field_storage_definitions, 'old_');
$sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, $backup_prefix);
$sandbox['backup_prefix_key'] = substr($backup_prefix, 4);
$sandbox['backup_request_time'] = \Drupal::time()->getRequestTime();
// Create temporary tables based on the new entity type and field storage
// definitions.
@ -554,11 +583,22 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
// At this point the update process either finished successfully or any
// error has been thrown already, so we can drop the backup entity tables.
// @todo Decide whether we should keep these tables around.
// @see https://www.drupal.org/project/drupal/issues/3024728
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
$this->database->schema()->dropTable($backup_table_name);
// error has been thrown already. We can either keep the backup tables in
// place or drop them.
if (Settings::get('entity_update_backup', TRUE)) {
$backup_key = $sandbox['backup_prefix_key'];
$backup = [
'entity_type' => $original,
'field_storage_definitions' => $original_field_storage_definitions,
'table_mapping' => $backup_table_mapping,
'request_time' => $sandbox['backup_request_time'],
];
$this->updateBackupRepository()->set("{$original->id()}.$backup_key", $backup);
}
else {
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
$this->database->schema()->dropTable($backup_table_name);
}
}
}
@ -582,13 +622,15 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* An entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
* An array of field storage definitions.
* @param string $first_prefix_part
* (optional) The first part of the prefix. Defaults to 'tmp_'.
*
* @return string
* A temporary table mapping prefix.
*
* @internal
*/
public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entity_type, array $field_storage_definitions) {
public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entity_type, array $field_storage_definitions, $first_prefix_part = 'tmp_') {
// Construct a unique prefix based on the contents of the entity type and
// field storage definitions.
$prefix_parts[] = spl_object_hash($entity_type);
@ -598,7 +640,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$prefix_parts[] = \Drupal::time()->getRequestTime();
$hash = hash('sha256', implode('', $prefix_parts));
return 'tmp_' . substr($hash, 0, 6);
return $first_prefix_part . substr($hash, 0, 6);
}
/**

View File

@ -165,6 +165,9 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase {
// Check that we can still save new entities after the schema has been
// updated.
$this->insertData($new_rev, $new_mul);
// Check that the backup tables have been kept in place.
$this->assertBackupTables();
}
/**
@ -569,10 +572,25 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase {
$this->assertFalse($database_schema->tableExists($entity_type->getRevisionDataTable()));
}
/**
* Asserts that the backup tables have been kept after a successful update.
*/
protected function assertBackupTables() {
$backups = \Drupal::keyValue('entity.update_backup')->getAll();
$backup = reset($backups);
$schema = $this->database->schema();
foreach ($backup['table_mapping']->getTableNames() as $table_name) {
$this->assertTrue($schema->tableExists($table_name));
}
}
/**
* Tests that a failed entity schema update preserves the existing data.
*/
public function testFieldableEntityTypeUpdatesErrorHandling() {
$schema = $this->database->schema();
// First, convert the entity type to be translatable for better coverage and
// insert some initial data.
$entity_type = $this->getUpdatedEntityTypeDefinition(FALSE, TRUE);
@ -581,6 +599,12 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase {
$this->assertEntityTypeSchema(FALSE, TRUE);
$this->insertData(FALSE, TRUE);
$tables = $schema->findTables('old_%');
$this->assertCount(3, $tables);
foreach ($tables as $table) {
$schema->dropTable($table);
}
$original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
@ -640,14 +664,19 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase {
$this->assertEquals($original_field_schema_data, $new_field_schema_data);
// Check that temporary tables have been removed.
$schema = $this->database->schema();
$temporary_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions, 'tmp_')->getTableNames();
$current_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions)->getTableNames();
foreach (array_combine($temporary_table_names, $current_table_names) as $temp_table_name => $table_name) {
$tables = $schema->findTables('tmp_%');
$this->assertCount(0, $tables);
$current_table_names = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions)->getTableNames();
foreach ($current_table_names as $table_name) {
$this->assertTrue($schema->tableExists($table_name));
$this->assertFalse($schema->tableExists($temp_table_name));
}
// Check that backup tables do not exist anymore, since they were
// restored/renamed.
$tables = $schema->findTables('old_%');
$this->assertCount(0, $tables);
// Check that the original tables still exist and their data is intact.
$this->assertTrue($schema->tableExists('entity_test_update'));
$this->assertTrue($schema->tableExists('entity_test_update_data'));
@ -723,4 +752,37 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase {
}
}
/**
* Tests the removal of the backup tables after a successful update.
*/
public function testFieldableEntityTypeUpdatesRemoveBackupTables() {
$schema = $this->database->schema();
// Convert the entity type to be revisionable.
$entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE);
$field_storage_definitions = $this->getUpdatedFieldStorageDefinitions(TRUE, FALSE);
$this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
// Check that backup tables are kept by default.
$tables = $schema->findTables('old_%');
$this->assertCount(3, $tables);
foreach ($tables as $table) {
$schema->dropTable($table);
}
// Make the entity update process drop the backup tables after a successful
// update.
$settings = Settings::getAll();
$settings['entity_update_backup'] = FALSE;
new Settings($settings);
$entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, TRUE);
$field_storage_definitions = $this->getUpdatedFieldStorageDefinitions(TRUE, TRUE);
$this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
// Check that backup tables have been dropped.
$tables = $schema->findTables('old_%');
$this->assertCount(0, $tables);
}
}

View File

@ -754,6 +754,15 @@ $settings['file_scan_ignore_directories'] = [
*/
$settings['entity_update_batch_size'] = 50;
/**
* Entity update backup.
*
* This is used to inform the entity storage handler that the backup tables as
* well as the original entity type and field storage definitions should be
* retained after a successful entity update process.
*/
$settings['entity_update_backup'] = TRUE;
/**
* Load local development override configuration, if available.
*