diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index 3fa34d8..f4b23ca 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -161,11 +161,24 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa } /** + * {@inheritdoc} + */ + public function hasData() { + // We cannot use an entity query as it relies on the entity type definition, + // which is not available while updating the entity schema. + return (bool) $this->database->select($this->baseTable) + ->countQuery() + ->range(0, 1) + ->execute() + ->fetchField(); + } + + /** * Initializes table name variables. */ protected function initTableLayout() { - // Reset table field values to ensure changes in the entity type deifnition - // are correctly reflected int the table layout. + // Reset table field values to ensure changes in the entity type definition + // are correctly reflected in the table layout. $this->tableMapping = NULL; $this->revisionKey = NULL; $this->revisionTable = NULL; @@ -1423,13 +1436,6 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function getEntitySchemaHash(ContentEntityTypeInterface $definition) { - return $this->schemaHandler()->getEntitySchemaHash($definition); - } - - /** - * {@inheritdoc} - */ public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original); } diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 97db1e2..71bd56d 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -16,6 +16,7 @@ use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\State\StateInterface; /** * Defines a schema handler that supports revisionable, translatable entities. @@ -73,6 +74,13 @@ class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface, protected $database; /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** * Constructs a ContentEntitySchemaHandler. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager @@ -93,10 +101,13 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit } /** - * {@inheritdoc} + * @return \Drupal\Core\State\StateInterface */ - public function getEntitySchemaHash(ContentEntityTypeInterface $definition) { - return hash('sha256', serialize($this->getEntitySchema($definition, TRUE))); + protected function state() { + if (!isset($this->state)) { + $this->state = \Drupal::state(); + } + return $this->state; } /** @@ -106,7 +117,9 @@ public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definiti return !$original || $original->getStorageClass() != $definition->getStorageClass() || $original->isRevisionable() != $definition->isRevisionable() || - $original->isTranslatable() != $definition->isTranslatable(); + $original->isTranslatable() != $definition->isTranslatable() || + // Detect changes in key or index definitions. + $this->loadEntitySchemaData($original) != $this->getEntitySchemaData($definition, $this->getEntitySchema($definition, TRUE)); } /** @@ -130,7 +143,7 @@ public function requiresEntityDataMigration(ContentEntityTypeInterface $definiti // revisionable to non revisionable, as in that case we just need to drop // revision tables. return $original->getStorageClass() != $definition->getStorageClass() || - !$original->isRevisionable() && $definition->isRevisionable() || + $original->isRevisionable() != $definition->isRevisionable() || $original->isTranslatable() != $definition->isTranslatable(); } @@ -164,11 +177,13 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $defi public function createEntitySchema(ContentEntityTypeInterface $entity_type) { $this->checkEntityType($entity_type); $schema_handler = $this->database->schema(); - foreach ($this->getEntitySchema($entity_type, TRUE) as $table_name => $table_schema) { + $schema = $this->getEntitySchema($entity_type, TRUE); + foreach ($schema as $table_name => $table_schema) { if (!$schema_handler->tableExists($table_name)) { $schema_handler->createTable($table_name, $table_schema); } } + $this->saveEntitySchemaData($entity_type, $schema); } /** @@ -199,32 +214,70 @@ public function updateEntitySchema(ContentEntityTypeInterface $entity_type, Cont $this->checkEntityType($entity_type); $this->checkEntityType($original); - if ($this->database->supportsTransactionalDDL()) { - // If the database supports transactional DDL, we can go ahead and rely - // on it. If not, we will have to rollback manually if something fails. - $transaction = $this->database->startTransaction(); - } - try { - $this->dropEntitySchema($original); - $this->createEntitySchema($entity_type); + // If we have no data just recreate the entity schema from scratch. + if (!$this->storage->hasData()) { + if ($this->database->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = $this->database->startTransaction(); + } + try { + $this->dropEntitySchema($original); + $this->createEntitySchema($entity_type); - // Update dedicated table revision schema. - if ($original->isRevisionable() && !$entity_type->isRevisionable()) { - $this->deleteDedicatedTableRevisionSchema(); + // Update dedicated table revision schema. + if ($original->isRevisionable() && !$entity_type->isRevisionable()) { + $this->dropDedicatedTableRevisionSchema(); + } + elseif (!$original->isRevisionable() && $entity_type->isRevisionable()) { + $this->createDedicatedTableRevisionSchema($entity_type); + } } - elseif (!$original->isRevisionable() && $entity_type->isRevisionable()) { - $this->createDedicatedTableRevisionSchema($entity_type); + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate original schema. + $this->createEntitySchema($original); + } + throw $e; } } - catch (\Exception $e) { - if ($this->database->supportsTransactionalDDL()) { - $transaction->rollback(); + else { + $schema_handler = $this->database->schema(); + + // Drop original indexes and unique keys. + foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) { + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->dropIndex($table_name, $name); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->dropUniqueKey($table_name, $name); + } + } } - else { - // Recreate original schema. - $this->createEntitySchema($original); + + // Create new indexes and unique keys. + $entity_schema = $this->getEntitySchema($entity_type, TRUE); + foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) { + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $name => $specifier) { + $schema_handler->addIndex($table_name, $name, $specifier); + } + } + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $name => $specifier) { + $schema_handler->addUniqueKey($table_name, $name, $specifier); + } + } } - throw $e; + + // Store the updated entity schema. + $this->saveEntitySchemaData($entity_type, $entity_schema); } } @@ -249,7 +302,7 @@ protected function createDedicatedTableRevisionSchema(ContentEntityTypeInterface /** * Deletes revision tables for the specified entity type. */ - protected function deleteDedicatedTableRevisionSchema() { + protected function dropDedicatedTableRevisionSchema() { $table_mapping = $this->storage->getTableMapping(); $schema_manager = $this->database->schema(); foreach ($this->fieldStorageDefinitions as $definition) { @@ -271,6 +324,8 @@ protected function deleteDedicatedTableRevisionSchema() { * @return array * A Schema API array describing the entity schema, excluding dedicated * field tables. + * + * @throws \Drupal\Core\Field\FieldException */ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { $this->checkEntityType($entity_type); @@ -382,6 +437,70 @@ protected function getEntitySchemaTables() { } /** + * Returns entity schema definitions for index and key definitions. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param array $schema + * The entity schema array. + * + * @return array + * A stripped down version of the $schema Schema API array containing, for + * each table, only the key and index definitions not derived from field + * storage definitions. + */ + protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) { + $schema_data = array(); + $entity_type_id = $entity_type->id(); + $keys = array('indexes', 'unique keys'); + $unused_keys = array_flip(array('description', 'fields', 'foreign keys')); + + foreach ($schema as $table_name => $table_schema) { + $table_schema = array_diff_key($table_schema, $unused_keys); + foreach ($keys as $key) { + // Exclude data generated from field storage definitions, we will check + // that separately. + if (!empty($table_schema[$key])) { + $data_keys = array_keys($table_schema[$key]); + $entity_keys = array_filter($data_keys, function ($key) use ($entity_type_id) { + return strpos($key, $entity_type_id . '_field_') !== 0; + }); + $table_schema[$key] = array_intersect_key($table_schema[$key], array_flip($entity_keys)); + } + } + $schema_data[$table_name] = array_filter($table_schema); + } + + return $schema_data; + } + + /** + * Loads stored schema data for the given entity type definition. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * + * @return array + * The entity schema data array. + */ + protected function loadEntitySchemaData(ContentEntityTypeInterface $entity_type) { + return $this->state()->get('entity.schema.handler.' . $entity_type->id() . '.schema_data') ?: array(); + } + + /** + * Stores schema data for the given entity type definition. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param array $schema + * The entity schema data array. + */ + protected function saveEntitySchemaData(ContentEntityTypeInterface $entity_type, $schema) { + $data = $this->getEntitySchemaData($entity_type, $schema); + $this->state()->set('entity.schema.handler.' . $entity_type->id() . '.schema_data', $data); + } + + /** * Initializes common information for a base table. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type @@ -1345,10 +1464,6 @@ protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $ /** * Generates an index name for a field data table. * - * @private Calling this function circumvents the entity system and is - * strongly discouraged. This function is not considered part of the public - * API and modules relying on it might break even in minor releases. - * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field storage definition. * @param string $index diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php index 606fabf..85d23cd 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php @@ -148,9 +148,9 @@ public function getChangeList($entity_type_id = NULL) { // Check whether there are changes in the entity type definition that // would affect entity schema. $original = $this->loadEntityTypeDefinition($entity_type_id); - if ($storage->requiresEntitySchemaChanges($original, $definition) || $storage->getEntitySchemaHash($definition) != $this->loadEntitySchemaHash($entity_type_id)) { + if ($storage->requiresEntitySchemaChanges($definition, $original)) { $change_list[$entity_type_id]['entity_type'] = static::ENTITY_TYPE_UPDATED; - if ($storage->requiresEntityDataMigration($original, $definition)) { + if ($storage->requiresEntityDataMigration($definition, $original)) { $change_list[$entity_type_id]['data_migration'] = TRUE; } } @@ -328,19 +328,6 @@ protected function loadEntityTypeDefinition($entity_type_id) { } /** - * Returns the stored schema hash for the specified entity type. - * - * @param string $entity_type_id - * The entity type identifier. - * - * @return \Drupal\Core\Entity\ContentEntityTypeInterface - * A stored entity type definition. - */ - protected function loadEntitySchemaHash($entity_type_id) { - return $this->state->get('entity.schema.manager.' . $entity_type_id . '.schema_hash'); - } - - /** * Stores the specified stored entity type definition. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition @@ -349,23 +336,6 @@ protected function loadEntitySchemaHash($entity_type_id) { protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) { $entity_type_id = $definition->id(); $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $definition); - $storage = $this->entityManager->getStorage($entity_type_id); - if ($storage instanceof ContentEntitySchemaProviderInterface) { - $hash = $storage->getEntitySchemaHash($definition); - $this->saveEntitySchemaHash($entity_type_id, $hash); - } - } - - /** - * Stores the schema hash for the specified entity type. - * - * @param string $entity_type_id - * The entity type identifier. - * @param string $hash - * The entity schema hash. - */ - protected function saveEntitySchemaHash($entity_type_id, $hash) { - $this->state->set('entity.schema.manager.' . $entity_type_id . '.schema_hash', $hash); } /** @@ -376,17 +346,6 @@ protected function saveEntitySchemaHash($entity_type_id, $hash) { */ protected function deleteEntityTypeDefinition($entity_type_id) { $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type'); - $this->deleteEntitySchemaHash($entity_type_id); - } - - /** - * Deletes the stored schema hash for the specified entity type. - * - * @param string $entity_type_id - * The entity type definition identifier. - */ - protected function deleteEntitySchemaHash($entity_type_id) { - $this->state->delete('entity.schema.manager.' . $entity_type_id . '.schema_hash'); } /** diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php index 6160632..d1df87d 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaProviderInterface.php @@ -16,17 +16,6 @@ interface ContentEntitySchemaProviderInterface { /** - * Returns an hash for the schema generated from the specified entity type. - * - * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition - * The updated entity type definition. - * - * @return string - * The entity schema hash. - */ - public function getEntitySchemaHash(ContentEntityTypeInterface $definition); - - /** * Checks whether the definition changes imply entity schema changes. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index 0cd6bee..41372e8 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -270,7 +270,6 @@ public function providerTestGetRevisionDataTable() { * * @covers ::__construct() * @covers ::onEntityTypeDefinitionCreate() - * @covers ::schemaHandler() * @covers ::getTableMapping() */ public function testOnEntityDefinitionCreate() { @@ -336,7 +335,18 @@ public function testOnEntityDefinitionCreate() { ->method('schema') ->will($this->returnValue($schema_handler)); - $this->entityStorage->onEntityTypeDefinitionCreate(); + $state = $this->getMock('Drupal\Core\State\StateInterface'); + $storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache)) + ->setMethods(array('schemaHandler')) + ->getMock(); + $schema_handler = new ContentEntitySchemaHandler($this->entityManager, $this->entityType, $storage, $this->connection, $state); + $storage + ->expects($this->any()) + ->method('schemaHandler') + ->will($this->returnValue($schema_handler)); + + $storage->onEntityTypeDefinitionCreate(); } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php index 3c29923..9366d3c 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -1027,12 +1027,15 @@ protected function setUpEntitySchemaHandler(array $expected = array()) { ->method('schema') ->will($this->returnValue($db_schema_handler)); - $this->schemaHandler = new ContentEntitySchemaHandler( - $this->entityManager, - $this->entityType, - $this->storage, - $connection - ); + $state = $this->getMock('Drupal\Core\State\StateInterface'); + $this->schemaHandler = $this->getMockBuilder('Drupal\Core\Entity\Schema\ContentEntitySchemaHandler') + ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection)) + ->setMethods(array('state')) + ->getMock(); + $this->schemaHandler + ->expects($this->any()) + ->method('state') + ->will($this->returnValue($state)); } /**