Issue #2721313 by timmillwood, amateescu, dawehner, jeqq, jibran, plach, catch, jhedstrom: Upgrade path between revisionable / non-revisionable entities
parent
eef585f6aa
commit
f6fa46e5fb
|
|
@ -117,6 +117,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* Whether this storage should use the temporary table mapping.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $temporary = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
@ -266,6 +273,31 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wrapped table mapping definition.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
|
||||
* The table mapping.
|
||||
*
|
||||
* @internal Only to be used internally by Entity API. Expected to be removed
|
||||
* by https://www.drupal.org/node/2554235.
|
||||
*/
|
||||
public function setTableMapping(TableMappingInterface $table_mapping) {
|
||||
$this->tableMapping = $table_mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the temporary state of the storage.
|
||||
*
|
||||
* @param bool $temporary
|
||||
* Whether to use a temporary table mapping or not.
|
||||
*
|
||||
* @internal Only to be used internally by Entity API.
|
||||
*/
|
||||
public function setTemporary($temporary) {
|
||||
$this->temporary = $temporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
@ -279,8 +311,10 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
|
|||
// @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
|
||||
// easily instantiate a new table mapping whenever needed.
|
||||
if (!isset($this->tableMapping) || $storage_definitions) {
|
||||
$table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
|
||||
$definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
|
||||
$table_mapping = new DefaultTableMapping($this->entityType, $definitions);
|
||||
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
|
||||
$table_mapping = new $table_mapping_class($this->entityType, $definitions);
|
||||
|
||||
$shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
|
||||
return $table_mapping->allowsSharedTableStorage($definition);
|
||||
|
|
|
|||
|
|
@ -237,6 +237,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
|
||||
// Check if the entity type specifies that data migration is being handled
|
||||
// elsewhere.
|
||||
if ($entity_type->get('requires_data_migration') === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// If the original storage has existing entities, or it is impossible to
|
||||
// determine if that is the case, require entity data to be migrated.
|
||||
$original_storage_class = $original->getStorageClass();
|
||||
|
|
@ -1212,10 +1218,14 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
|
|||
$deleted = !$this->originalDefinitions;
|
||||
$table_mapping = $this->storage->getTableMapping();
|
||||
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
|
||||
$this->database->schema()->dropTable($table_name);
|
||||
if ($this->database->schema()->tableExists($table_name)) {
|
||||
$this->database->schema()->dropTable($table_name);
|
||||
}
|
||||
if ($this->entityType->isRevisionable()) {
|
||||
$revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
|
||||
$this->database->schema()->dropTable($revision_name);
|
||||
$revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
|
||||
if ($this->database->schema()->tableExists($revision_table_name)) {
|
||||
$this->database->schema()->dropTable($revision_table_name);
|
||||
}
|
||||
}
|
||||
$this->deleteFieldSchemaData($storage_definition);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,461 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Entity\Sql;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
|
||||
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Defines a schema converter for entity types with existing data.
|
||||
*
|
||||
* For now, this can only be used to convert an entity type from
|
||||
* non-revisionable to revisionable, however, it should be expanded so it can
|
||||
* also handle converting an entity type to be translatable.
|
||||
*/
|
||||
class SqlContentEntityStorageSchemaConverter {
|
||||
|
||||
/**
|
||||
* The entity type ID this schema converter is responsible for.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity definition update manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
|
||||
*/
|
||||
protected $entityDefinitionUpdateManager;
|
||||
|
||||
/**
|
||||
* The last installed schema repository service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
|
||||
*/
|
||||
protected $lastInstalledSchemaRepository;
|
||||
|
||||
/**
|
||||
* The key-value collection for tracking installed storage schema.
|
||||
*
|
||||
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
|
||||
*/
|
||||
protected $installedStorageSchema;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* SqlContentEntityStorageSchemaConverter constructor.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
|
||||
* Entity definition update manager service.
|
||||
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository
|
||||
* Last installed schema repository service.
|
||||
* @param \Drupal\Core\Database\Connection $database
|
||||
* Database connection.
|
||||
*/
|
||||
public function __construct($entity_type_id, EntityTypeManagerInterface $entity_type_manager, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository, KeyValueStoreInterface $installed_storage_schema, Connection $database) {
|
||||
$this->entityTypeId = $entity_type_id;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityDefinitionUpdateManager = $entity_definition_update_manager;
|
||||
$this->lastInstalledSchemaRepository = $last_installed_schema_repository;
|
||||
$this->installedStorageSchema = $installed_storage_schema;
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an entity type with existing data to be revisionable.
|
||||
*
|
||||
* This process does the following tasks:
|
||||
* - creates the schema from scratch with the new revisionable entity type
|
||||
* definition (i.e. the current definition of the entity type from code)
|
||||
* using temporary table names;
|
||||
* - loads the initial entity data by using the last installed entity and
|
||||
* field storage definitions;
|
||||
* - saves the entity data to the temporary tables;
|
||||
* - at the end of the process:
|
||||
* - deletes the original tables and replaces them with the temporary ones
|
||||
* that hold the new (revisionable) entity data;
|
||||
* - updates the installed entity schema data;
|
||||
* - updates the entity type definition in order to trigger the
|
||||
* \Drupal\Core\Entity\EntityTypeEvents::UPDATE event;
|
||||
* - updates the field storage definitions in order to mark the
|
||||
* revisionable ones as such.
|
||||
*
|
||||
* In case of an error during the entity save process, the temporary tables
|
||||
* are deleted and the original entity type and field storage definitions are
|
||||
* restored.
|
||||
*
|
||||
* @param array $sandbox
|
||||
* The sandbox array from a hook_update_N() implementation.
|
||||
* @param string[] $fields_to_update
|
||||
* (optional) An array of field names that should be converted to be
|
||||
* revisionable. Note that the 'langcode' field, if present, is updated
|
||||
* automatically. Defaults to an empty array.
|
||||
*
|
||||
* @throws \Exception
|
||||
* Re-throws any exception raised during the update process.
|
||||
*/
|
||||
public function convertToRevisionable(array &$sandbox, array $fields_to_update = []) {
|
||||
// If 'progress' is not set, then this will be the first run of the batch.
|
||||
if (!isset($sandbox['progress'])) {
|
||||
// Store the original entity type and field definitions in the $sandbox
|
||||
// array so we can use them later in the update process.
|
||||
$this->collectOriginalDefinitions($sandbox);
|
||||
|
||||
// Create a temporary environment in which the new data will be stored.
|
||||
$this->createTemporaryDefinitions($sandbox, $fields_to_update);
|
||||
|
||||
// Create the updated entity schema using temporary tables.
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
|
||||
$storage->setTemporary(TRUE);
|
||||
$storage->setEntityType($sandbox['temporary_entity_type']);
|
||||
$storage->onEntityTypeCreate($sandbox['temporary_entity_type']);
|
||||
}
|
||||
|
||||
// Copy over the existing data to the new temporary tables.
|
||||
$this->copyData($sandbox);
|
||||
|
||||
// If the data copying has finished successfully, we can drop the temporary
|
||||
// tables and call the appropriate update mechanisms.
|
||||
if ($sandbox['#finished'] == 1) {
|
||||
$this->entityTypeManager->useCaches(FALSE);
|
||||
$actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
|
||||
|
||||
// Rename the original tables so we can put them back in place in case
|
||||
// anything goes wrong.
|
||||
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
|
||||
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
|
||||
$this->database->schema()->renameTable($table_name, $old_table_name);
|
||||
}
|
||||
|
||||
// Put the new tables in place and update the entity type and field
|
||||
// storage definitions.
|
||||
try {
|
||||
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
|
||||
$storage->setEntityType($actual_entity_type);
|
||||
$storage->setTemporary(FALSE);
|
||||
$actual_table_names = $storage->getTableMapping()->getTableNames();
|
||||
|
||||
$table_name_mapping = [];
|
||||
foreach ($actual_table_names as $new_table_name) {
|
||||
$temp_table_name = TemporaryTableMapping::getTempTableName($new_table_name);
|
||||
$table_name_mapping[$temp_table_name] = $new_table_name;
|
||||
$this->database->schema()->renameTable($temp_table_name, $new_table_name);
|
||||
}
|
||||
|
||||
// Rename the tables in the cached entity schema data.
|
||||
$entity_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
|
||||
foreach ($entity_schema_data as $temp_table_name => $schema) {
|
||||
if (isset($table_name_mapping[$temp_table_name])) {
|
||||
$entity_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
|
||||
unset($entity_schema_data[$temp_table_name]);
|
||||
}
|
||||
}
|
||||
$this->installedStorageSchema->set($this->entityTypeId . '.entity_schema_data', $entity_schema_data);
|
||||
|
||||
// Rename the tables in the cached field schema data.
|
||||
foreach ($sandbox['updated_storage_definitions'] as $storage_definition) {
|
||||
$field_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
|
||||
foreach ($field_schema_data as $temp_table_name => $schema) {
|
||||
if (isset($table_name_mapping[$temp_table_name])) {
|
||||
$field_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
|
||||
unset($field_schema_data[$temp_table_name]);
|
||||
}
|
||||
}
|
||||
$this->installedStorageSchema->set($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), $field_schema_data);
|
||||
}
|
||||
|
||||
// Instruct the entity schema handler that data migration has been
|
||||
// handled already and update the entity type.
|
||||
$actual_entity_type->set('requires_data_migration', FALSE);
|
||||
$this->entityDefinitionUpdateManager->updateEntityType($actual_entity_type);
|
||||
|
||||
// Update the field storage definitions.
|
||||
$this->updateFieldStorageDefinitionsToRevisionable($actual_entity_type, $sandbox['original_storage_definitions'], $fields_to_update);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Something went wrong, bring back the original tables.
|
||||
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
|
||||
// We are in the 'original data recovery' phase, so we need to be sure
|
||||
// that the initial tables can be properly restored.
|
||||
if ($this->database->schema()->tableExists($table_name)) {
|
||||
$this->database->schema()->dropTable($table_name);
|
||||
}
|
||||
|
||||
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
|
||||
$this->database->schema()->renameTable($old_table_name, $table_name);
|
||||
}
|
||||
|
||||
// Re-throw the original exception.
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// At this point the update process either finished successfully or any
|
||||
// error has been handled already, so we can drop the backup entity
|
||||
// tables.
|
||||
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
|
||||
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
|
||||
$this->database->schema()->dropTable($old_table_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads entities from the original storage and saves them to a temporary one.
|
||||
*
|
||||
* @param array &$sandbox
|
||||
* The sandbox array from a hook_update_N() implementation.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* Thrown in case of an error during the entity save process.
|
||||
*/
|
||||
protected function copyData(array &$sandbox) {
|
||||
/** @var \Drupal\Core\Entity\Sql\TemporaryTableMapping $temporary_table_mapping */
|
||||
$temporary_table_mapping = $sandbox['temporary_table_mapping'];
|
||||
$temporary_entity_type = $sandbox['temporary_entity_type'];
|
||||
$original_table_mapping = $sandbox['original_table_mapping'];
|
||||
$original_entity_type = $sandbox['original_entity_type'];
|
||||
|
||||
$original_base_table = $original_entity_type->getBaseTable();
|
||||
|
||||
$revision_id_key = $temporary_entity_type->getKey('revision');
|
||||
|
||||
// If 'progress' is not set, then this will be the first run of the batch.
|
||||
if (!isset($sandbox['progress'])) {
|
||||
$sandbox['progress'] = 0;
|
||||
$sandbox['current_id'] = 0;
|
||||
$sandbox['max'] = $this->database->select($original_base_table)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
}
|
||||
|
||||
$id = $original_entity_type->getKey('id');
|
||||
|
||||
// Define the step size.
|
||||
$step_size = Settings::get('entity_update_batch_size', 50);
|
||||
|
||||
// Get the next entity IDs to migrate.
|
||||
$entity_ids = $this->database->select($original_base_table)
|
||||
->fields($original_base_table, [$id])
|
||||
->condition($id, $sandbox['current_id'], '>')
|
||||
->orderBy($id, 'ASC')
|
||||
->range(0, $step_size)
|
||||
->execute()
|
||||
->fetchAllKeyed(0, 0);
|
||||
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($temporary_entity_type->id());
|
||||
$storage->setEntityType($original_entity_type);
|
||||
$storage->setTableMapping($original_table_mapping);
|
||||
|
||||
$entities = $storage->loadMultiple($entity_ids);
|
||||
|
||||
// Now inject the temporary entity type definition and table mapping in the
|
||||
// storage and re-save the entities.
|
||||
$storage->setEntityType($temporary_entity_type);
|
||||
$storage->setTableMapping($temporary_table_mapping);
|
||||
|
||||
foreach ($entities as $entity_id => $entity) {
|
||||
try {
|
||||
// Set the revision ID to be same as the entity ID.
|
||||
$entity->set($revision_id_key, $entity_id);
|
||||
|
||||
// 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.
|
||||
$storage->save($entity);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// In case of an error during the save process, we need to roll back the
|
||||
// original entity type and field storage definitions and clean up the
|
||||
// temporary tables.
|
||||
$this->restoreOriginalDefinitions($sandbox);
|
||||
|
||||
foreach ($temporary_table_mapping->getTableNames() as $table_name) {
|
||||
$this->database->schema()->dropTable($table_name);
|
||||
}
|
||||
|
||||
// Re-throw the original exception with a helpful message.
|
||||
throw new EntityStorageException("The entity update process failed while processing the entity {$original_entity_type->id()}:$entity_id.", $e->getCode(), $e);
|
||||
}
|
||||
|
||||
$sandbox['progress']++;
|
||||
$sandbox['current_id'] = $entity_id;
|
||||
}
|
||||
|
||||
// If we're not in maintenance mode, the number of entities could change at
|
||||
// any time so make sure that we always use the latest record count.
|
||||
$sandbox['max'] = $this->database->select($original_base_table)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
|
||||
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates field definitions to be revisionable.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
|
||||
* A content entity type definition.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
|
||||
* An array of field storage definitions.
|
||||
* @param array $fields_to_update
|
||||
* (optional) An array of field names for which to enable revision support.
|
||||
* Defaults to an empty array.
|
||||
* @param bool $update_cached_definitions
|
||||
* (optional) Whether to update the cached field storage definitions in the
|
||||
* entity definition update manager. Defaults to TRUE.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
|
||||
* An array of updated field storage definitions.
|
||||
*/
|
||||
protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = [], $update_cached_definitions = TRUE) {
|
||||
$updated_storage_definitions = array_map(function ($storage_definition) { return clone $storage_definition; }, $storage_definitions);
|
||||
|
||||
// Update the 'langcode' field manually, as it is configured in the base
|
||||
// content entity field definitions.
|
||||
if ($entity_type->hasKey('langcode')) {
|
||||
$fields_to_update = array_merge([$entity_type->getKey('langcode')], $fields_to_update);
|
||||
}
|
||||
|
||||
foreach ($fields_to_update as $field_name) {
|
||||
if (!$updated_storage_definitions[$field_name]->isRevisionable()) {
|
||||
$updated_storage_definitions[$field_name]->setRevisionable(TRUE);
|
||||
|
||||
if ($update_cached_definitions) {
|
||||
$this->entityDefinitionUpdateManager->updateFieldStorageDefinition($updated_storage_definitions[$field_name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the revision ID field.
|
||||
$revision_field = BaseFieldDefinition::create('integer')
|
||||
->setName($entity_type->getKey('revision'))
|
||||
->setTargetEntityTypeId($entity_type->id())
|
||||
->setTargetBundle(NULL)
|
||||
->setLabel(new TranslatableMarkup('Revision ID'))
|
||||
->setReadOnly(TRUE)
|
||||
->setSetting('unsigned', TRUE);
|
||||
|
||||
if ($update_cached_definitions) {
|
||||
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_field);
|
||||
}
|
||||
|
||||
$updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field;
|
||||
|
||||
return $updated_storage_definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the original definitions of an entity type and its fields.
|
||||
*
|
||||
* @param array &$sandbox
|
||||
* A sandbox array from a hook_update_N() implementation.
|
||||
*/
|
||||
protected function collectOriginalDefinitions(array &$sandbox) {
|
||||
$original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($this->entityTypeId);
|
||||
$original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
|
||||
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
|
||||
$storage->setEntityType($original_entity_type);
|
||||
$original_table_mapping = $storage->getTableMapping($original_storage_definitions);
|
||||
|
||||
$sandbox['original_entity_type'] = $original_entity_type;
|
||||
$sandbox['original_storage_definitions'] = $original_storage_definitions;
|
||||
$sandbox['original_table_mapping'] = $original_table_mapping;
|
||||
|
||||
$sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
|
||||
foreach ($original_storage_definitions as $storage_definition) {
|
||||
$sandbox['original_field_schema_data'][$storage_definition->getName()] = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the entity type, field storage definitions and their schema data.
|
||||
*
|
||||
* @param array $sandbox
|
||||
* The sandbox array from a hook_update_N() implementation.
|
||||
*/
|
||||
protected function restoreOriginalDefinitions(array $sandbox) {
|
||||
$original_entity_type = $sandbox['original_entity_type'];
|
||||
$original_storage_definitions = $sandbox['original_storage_definitions'];
|
||||
$original_entity_schema_data = $sandbox['original_entity_schema_data'];
|
||||
$original_field_schema_data = $sandbox['original_field_schema_data'];
|
||||
|
||||
$this->lastInstalledSchemaRepository->setLastInstalledDefinition($original_entity_type);
|
||||
$this->lastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($original_entity_type->id(), $original_storage_definitions);
|
||||
|
||||
$this->installedStorageSchema->set($original_entity_type->id() . '.entity_schema_data', $original_entity_schema_data);
|
||||
foreach ($original_field_schema_data as $field_name => $field_schema_data) {
|
||||
$this->installedStorageSchema->set($original_entity_type->id() . '.field_schema_data.' . $field_name, $field_schema_data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates temporary entity type, field storage and table mapping objects.
|
||||
*
|
||||
* @param array &$sandbox
|
||||
* A sandbox array from a hook_update_N() implementation.
|
||||
* @param string[] $fields_to_update
|
||||
* (optional) An array of field names that should be converted to be
|
||||
* revisionable. Note that the 'langcode' field, if present, is updated
|
||||
* automatically. Defaults to an empty array.
|
||||
*/
|
||||
protected function createTemporaryDefinitions(array &$sandbox, array $fields_to_update) {
|
||||
// Make sure to get the latest entity type definition from code.
|
||||
$this->entityTypeManager->useCaches(FALSE);
|
||||
$actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
|
||||
|
||||
$temporary_entity_type = clone $actual_entity_type;
|
||||
$temporary_entity_type->set('base_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getBaseTable()));
|
||||
$temporary_entity_type->set('revision_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionTable()));
|
||||
if ($temporary_entity_type->isTranslatable()) {
|
||||
$temporary_entity_type->set('data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getDataTable()));
|
||||
$temporary_entity_type->set('revision_data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionDataTable()));
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
|
||||
$storage->setTemporary(TRUE);
|
||||
$storage->setEntityType($temporary_entity_type);
|
||||
|
||||
$updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE);
|
||||
$temporary_table_mapping = $storage->getTableMapping($updated_storage_definitions);
|
||||
|
||||
$sandbox['temporary_entity_type'] = $temporary_entity_type;
|
||||
$sandbox['temporary_table_mapping'] = $temporary_table_mapping;
|
||||
$sandbox['updated_storage_definitions'] = $updated_storage_definitions;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Entity\Sql;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
|
||||
/**
|
||||
* Defines a temporary table mapping class.
|
||||
*/
|
||||
class TemporaryTableMapping extends DefaultTableMapping {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
|
||||
return static::getTempTableName(parent::generateFieldTableName($storage_definition, $revision));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a temporary table name.
|
||||
*
|
||||
* The method accounts for a maximum table name length of 64 characters.
|
||||
*
|
||||
* @param string $table_name
|
||||
* The initial table name.
|
||||
* @param string $prefix
|
||||
* (optional) The prefix to use for the new table name. Defaults to 'tmp_'.
|
||||
*
|
||||
* @return string
|
||||
* The final table name.
|
||||
*/
|
||||
public static function getTempTableName($table_name, $prefix = 'tmp_') {
|
||||
$tmp_table_name = $prefix . $table_name;
|
||||
|
||||
// Limit the string to 48 characters, keeping a 16 characters margin for db
|
||||
// prefixes.
|
||||
if (strlen($table_name) > 48) {
|
||||
$short_table_name = substr($table_name, 0, 34);
|
||||
$table_hash = substr(hash('sha256', $table_name), 0, 10);
|
||||
|
||||
$tmp_table_name = $prefix . $short_table_name . $table_hash;
|
||||
}
|
||||
return $tmp_table_name;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\system\Tests\Entity\Update;
|
||||
|
||||
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
|
||||
use Drupal\system\Tests\Entity\EntityDefinitionTestTrait;
|
||||
use Drupal\system\Tests\Update\UpdatePathTestBase;
|
||||
|
||||
/**
|
||||
* Tests updating an entity type with existing data to be revisionable.
|
||||
*
|
||||
* @group Entity
|
||||
* @group Update
|
||||
*/
|
||||
class SqlContentEntityStorageSchemaConverterTest extends UpdatePathTestBase {
|
||||
|
||||
use EntityDefinitionTestTrait;
|
||||
|
||||
/**
|
||||
* The entity manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* The entity definition update manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
|
||||
*/
|
||||
protected $entityDefinitionUpdateManager;
|
||||
|
||||
/**
|
||||
* The last installed schema repository service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
|
||||
*/
|
||||
protected $lastInstalledSchemaRepository;
|
||||
|
||||
/**
|
||||
* The key-value collection for tracking installed storage schema.
|
||||
*
|
||||
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
|
||||
*/
|
||||
protected $installedStorageSchema;
|
||||
|
||||
/**
|
||||
* The state service.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->entityManager = \Drupal::entityManager();
|
||||
$this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
|
||||
$this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository');
|
||||
$this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
|
||||
$this->state = \Drupal::state();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setDatabaseDumpFiles() {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz',
|
||||
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the conversion of an entity type to revisionable.
|
||||
*/
|
||||
public function testMakeRevisionable() {
|
||||
// Check that entity type is not revisionable prior to running the update
|
||||
// process.
|
||||
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
|
||||
$this->assertFalse($entity_test_update->isRevisionable());
|
||||
|
||||
// Make the entity type revisionable and translatable and run the updates.
|
||||
$this->updateEntityTypeToRevisionableAndTranslatable();
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */
|
||||
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
|
||||
$this->assertTrue($entity_test_update->isRevisionable());
|
||||
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
|
||||
$this->assertEqual(count($storage->loadMultiple()), 102, 'All test entities were found.');
|
||||
|
||||
// Check that each field value was copied correctly to the revision tables.
|
||||
for ($i = 1; $i <= 102; $i++) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $storage->loadRevision($i);
|
||||
|
||||
$this->assertEqual($i, $revision->id());
|
||||
$this->assertEqual($i, $revision->getRevisionId());
|
||||
|
||||
$this->assertEqual($i . ' - test single property', $revision->test_single_property->value);
|
||||
|
||||
$this->assertEqual($i . ' - test multiple properties - value1', $revision->test_multiple_properties->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties - value2', $revision->test_multiple_properties->value2);
|
||||
|
||||
$this->assertEqual($i . ' - test single property multiple values 0', $revision->test_single_property_multiple_values->value);
|
||||
$this->assertEqual($i . ' - test single property multiple values 1', $revision->test_single_property_multiple_values[1]->value);
|
||||
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value1 0', $revision->test_multiple_properties_multiple_values[0]->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value2 0', $revision->test_multiple_properties_multiple_values[0]->value2);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value1 1', $revision->test_multiple_properties_multiple_values[1]->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value2 1', $revision->test_multiple_properties_multiple_values[1]->value2);
|
||||
|
||||
$this->assertEqual($i . ' - field test configurable field - value1 0', $revision->field_test_configurable_field[0]->value1);
|
||||
$this->assertEqual($i . ' - field test configurable field - value2 0', $revision->field_test_configurable_field[0]->value2);
|
||||
$this->assertEqual($i . ' - field test configurable field - value1 1', $revision->field_test_configurable_field[1]->value1);
|
||||
$this->assertEqual($i . ' - field test configurable field - value2 1', $revision->field_test_configurable_field[1]->value2);
|
||||
|
||||
$this->assertEqual($i . ' - test entity base field info', $revision->test_entity_base_field_info->value);
|
||||
|
||||
// Do the same checks for translated field values.
|
||||
$translation = $revision->getTranslation('ro');
|
||||
|
||||
$this->assertEqual($i . ' - test single property - ro', $translation->test_single_property->value);
|
||||
|
||||
$this->assertEqual($i . ' - test multiple properties - value1 - ro', $translation->test_multiple_properties->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties - value2 - ro', $translation->test_multiple_properties->value2);
|
||||
|
||||
$this->assertEqual($i . ' - test single property multiple values 0 - ro', $translation->test_single_property_multiple_values[0]->value);
|
||||
$this->assertEqual($i . ' - test single property multiple values 1 - ro', $translation->test_single_property_multiple_values[1]->value);
|
||||
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value1 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value2 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value2);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value1 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value1);
|
||||
$this->assertEqual($i . ' - test multiple properties multiple values - value2 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value2);
|
||||
|
||||
$this->assertEqual($i . ' - field test configurable field - value1 0 - ro', $translation->field_test_configurable_field[0]->value1);
|
||||
$this->assertEqual($i . ' - field test configurable field - value2 0 - ro', $translation->field_test_configurable_field[0]->value2);
|
||||
$this->assertEqual($i . ' - field test configurable field - value1 1 - ro', $translation->field_test_configurable_field[1]->value1);
|
||||
$this->assertEqual($i . ' - field test configurable field - value2 1 - ro', $translation->field_test_configurable_field[1]->value2);
|
||||
|
||||
$this->assertEqual($i . ' - test entity base field info - ro', $translation->test_entity_base_field_info->value);
|
||||
}
|
||||
|
||||
// Check that temporary tables have been removed at the end of the process.
|
||||
$schema = \Drupal::database()->schema();
|
||||
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
|
||||
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
|
||||
}
|
||||
|
||||
// Check that backup tables have been removed at the end of the process.
|
||||
$schema = \Drupal::database()->schema();
|
||||
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
|
||||
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name, 'old_')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a failed "make revisionable" update preserves the existing data.
|
||||
*/
|
||||
public function testMakeRevisionableErrorHandling() {
|
||||
$original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
|
||||
$original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
|
||||
|
||||
$original_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
|
||||
foreach ($original_storage_definitions as $storage_definition) {
|
||||
$original_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
|
||||
}
|
||||
|
||||
// Check that entity type is not revisionable prior to running the update
|
||||
// process.
|
||||
$this->assertFalse($original_entity_type->isRevisionable());
|
||||
|
||||
// Make the update throw an exception during the entity save process.
|
||||
\Drupal::state()->set('entity_test_update.throw_exception', TRUE);
|
||||
|
||||
// Since the update process is interrupted by the exception thrown above,
|
||||
// we can not do the full post update testing offered by UpdatePathTestBase.
|
||||
$this->checkFailedUpdates = FALSE;
|
||||
|
||||
// Make the entity type revisionable and run the updates.
|
||||
$this->updateEntityTypeToRevisionableAndTranslatable();
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
// Check that the update failed.
|
||||
$this->assertRaw('<strong>' . t('Failed:') . '</strong>');
|
||||
|
||||
// Check that the last installed entity type definition is kept as
|
||||
// non-revisionable.
|
||||
$new_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
|
||||
$this->assertFalse($new_entity_type->isRevisionable(), 'The entity type is kept unchanged.');
|
||||
|
||||
// Check that the last installed field storage definitions did not change by
|
||||
// looking at the 'langcode' field, which is updated automatically.
|
||||
$new_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
|
||||
$langcode_key = $original_entity_type->getKey('langcode');
|
||||
$this->assertEqual($original_storage_definitions[$langcode_key]->isRevisionable(), $new_storage_definitions[$langcode_key]->isRevisionable(), "The 'langcode' field is kept unchanged.");
|
||||
|
||||
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
|
||||
|
||||
// Check that installed storage schema did not change.
|
||||
$new_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
|
||||
$this->assertEqual($original_entity_schema_data, $new_entity_schema_data);
|
||||
|
||||
foreach ($new_storage_definitions as $storage_definition) {
|
||||
$new_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
|
||||
}
|
||||
$this->assertEqual($original_field_schema_data, $new_field_schema_data);
|
||||
|
||||
// Check that temporary tables have been removed.
|
||||
$schema = \Drupal::database()->schema();
|
||||
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
|
||||
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
|
||||
}
|
||||
|
||||
// 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'));
|
||||
|
||||
$base_table_count = \Drupal::database()->select('entity_test_update')
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
$this->assertEqual($base_table_count, 102);
|
||||
|
||||
$data_table_count = \Drupal::database()->select('entity_test_update_data')
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
// There are two records for each entity, one for English and one for
|
||||
// Romanian.
|
||||
$this->assertEqual($data_table_count, 204);
|
||||
|
||||
$base_table_row = \Drupal::database()->select('entity_test_update')
|
||||
->fields('entity_test_update')
|
||||
->condition('id', 1, '=')
|
||||
->condition('langcode', 'en', '=')
|
||||
->execute()
|
||||
->fetchAllAssoc('id');
|
||||
$this->assertEqual('843e9ac7-3351-4cc1-a202-2dbffffae21c', $base_table_row[1]->uuid);
|
||||
|
||||
$data_table_row = \Drupal::database()->select('entity_test_update_data')
|
||||
->fields('entity_test_update_data')
|
||||
->condition('id', 1, '=')
|
||||
->condition('langcode', 'en', '=')
|
||||
->execute()
|
||||
->fetchAllAssoc('id');
|
||||
$this->assertEqual('1 - test single property', $data_table_row[1]->test_single_property);
|
||||
$this->assertEqual('1 - test multiple properties - value1', $data_table_row[1]->test_multiple_properties__value1);
|
||||
$this->assertEqual('1 - test multiple properties - value2', $data_table_row[1]->test_multiple_properties__value2);
|
||||
$this->assertEqual('1 - test entity base field info', $data_table_row[1]->test_entity_base_field_info);
|
||||
}
|
||||
|
||||
}
|
||||
36
core/modules/system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php
vendored
Normal file
36
core/modules/system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
// @codingStandardsIgnoreFile
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
$connection = Database::getConnection();
|
||||
|
||||
// Set the schema version.
|
||||
$connection->merge('key_value')
|
||||
->fields([
|
||||
'value' => 'i:8000;',
|
||||
'name' => 'entity_test_schema_converter',
|
||||
'collection' => 'system.schema',
|
||||
])
|
||||
->condition('collection', 'system.schema')
|
||||
->condition('name', 'entity_test_schema_converter')
|
||||
->execute();
|
||||
|
||||
// Update core.extension.
|
||||
$extensions = $connection->select('config')
|
||||
->fields('config', ['data'])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.extension')
|
||||
->execute()
|
||||
->fetchField();
|
||||
$extensions = unserialize($extensions);
|
||||
$extensions['module']['entity_test_schema_converter'] = 8000;
|
||||
$connection->update('config')
|
||||
->fields([
|
||||
'data' => serialize($extensions),
|
||||
'collection' => '',
|
||||
'name' => 'core.extension',
|
||||
])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.extension')
|
||||
->execute();
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
name: 'Entity Schema Converter Test'
|
||||
type: module
|
||||
description: 'Provides testing for the entity schema converter.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- entity_test_update
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update functions for entity_test_schema_converter.
|
||||
*/
|
||||
|
||||
use \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchemaConverter;
|
||||
|
||||
/**
|
||||
* @addtogroup updates-8.4.x
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update entity_test_update to be revisionable.
|
||||
*/
|
||||
function entity_test_schema_converter_post_update_make_revisionable(&$sandbox) {
|
||||
$revisionableSchemaConverter = new SqlContentEntityStorageSchemaConverter(
|
||||
'entity_test_update',
|
||||
\Drupal::entityTypeManager(),
|
||||
\Drupal::entityDefinitionUpdateManager(),
|
||||
\Drupal::service('entity.last_installed_schema.repository'),
|
||||
\Drupal::keyValue('entity.storage_schema.sql'),
|
||||
\Drupal::database()
|
||||
);
|
||||
|
||||
$revisionableSchemaConverter->convertToRevisionable(
|
||||
$sandbox,
|
||||
[
|
||||
'test_single_property',
|
||||
'test_multiple_properties',
|
||||
'test_single_property_multiple_values',
|
||||
'test_multiple_properties_multiple_values',
|
||||
'test_entity_base_field_info',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-8.4.x".
|
||||
*/
|
||||
Loading…
Reference in New Issue