diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index bbf1ae0..d1b9e7b 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -291,7 +291,7 @@ protected function buildQuery($ids, $revision_id = FALSE) { if ($this->revisionKey) { // Add all fields from the {entity_revision} table. $entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->entityInfo['revision_table'])); - // The id field is provided by entity, so remove it. + // The ID field is provided by entity, so remove it. unset($entity_revision_fields[$this->idKey]); // Remove all fields from the base table that are also fields by the same @@ -304,7 +304,7 @@ protected function buildQuery($ids, $revision_id = FALSE) { } $query->fields('revision', $entity_revision_fields); - // Compare revision id of the base and revision table, if equal then this + // Compare revision ID of the base and revision table, if equal then this // is the default revision. $query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision'); } diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index ad361d7..d3fc7eb 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -51,6 +51,13 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { protected $dataTable; /** + * The table that stores revision field data if the entity supports revisions. + * + * @var string + */ + protected $revisionDataTable; + + /** * Overrides DatabaseStorageController::__construct(). */ public function __construct($entity_type, array $entity_info, Connection $database) { @@ -61,6 +68,10 @@ public function __construct($entity_type, array $entity_info, Connection $databa // Check if the entity type has a dedicated table for properties. if (!empty($this->entityInfo['data_table'])) { $this->dataTable = $this->entityInfo['data_table']; + // TODO + if ($this->revisionTable) { + $this->revisionDataTable = $this->entityInfo['revision_data_table']; + } } // Work-a-round to let load() get stdClass storage records without having to @@ -162,55 +173,6 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value } /** - * {@inheritdoc} - */ - protected function buildQuery($ids, $revision_id = FALSE) { - $query = $this->database->select($this->entityInfo['base_table'], 'base'); - $is_revision_query = $this->revisionKey && ($revision_id || !$this->dataTable); - - $query->addTag($this->entityType . '_load_multiple'); - - if ($revision_id) { - $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id)); - } - elseif ($is_revision_query) { - $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}"); - } - - // Add fields from the {entity} table. - $entity_fields = drupal_schema_fields_sql($this->entityInfo['base_table']); - - if ($is_revision_query) { - // Add all fields from the {entity_revision} table. - $entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->entityInfo['revision_table'])); - // The ID field is provided by entity, so remove it. - unset($entity_revision_fields[$this->idKey]); - - // Remove all fields from the base table that are also fields by the same - // name in the revision table. - $entity_field_keys = array_flip($entity_fields); - foreach ($entity_revision_fields as $name) { - if (isset($entity_field_keys[$name])) { - unset($entity_fields[$entity_field_keys[$name]]); - } - } - $query->fields('revision', $entity_revision_fields); - - // Compare revision ID of the base and revision table, if equal then this - // is the default revision. - $query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision'); - } - - $query->fields('base', $entity_fields); - - if ($ids) { - $query->condition("base.{$this->idKey}", $ids, 'IN'); - } - - return $query; - } - - /** * Overrides DatabaseStorageController::attachLoad(). * * Added mapping from storage records to entities. @@ -266,13 +228,13 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { if ($this->dataTable) { // If a revision table is available, we need all the properties of the // latest revision. Otherwise we fall back to the data table. - $table = $this->revisionTable ?: $this->dataTable; + $table = $this->revisionDataTable ?: $this->dataTable; $query = $this->database->select($table, 'data', array('fetch' => PDO::FETCH_ASSOC)) ->fields('data') ->condition($this->idKey, array_keys($entities)) ->orderBy('data.' . $this->idKey); - if ($this->revisionTable) { + if ($this->revisionDataTable) { if ($revision_id) { $query->condition($this->revisionKey, $revision_id); } @@ -289,8 +251,8 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { $data = $query->execute(); $field_definition = \Drupal::entityManager()->getFieldDefinitions($this->entityType); $translations = array(); - if ($this->revisionTable) { - $data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_table']), drupal_schema_fields_sql($this->entityInfo['base_table']))); + if ($this->revisionDataTable) { + $data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_data_table']), drupal_schema_fields_sql($this->entityInfo['base_table']))); } else { $data_fields = array_flip(drupal_schema_fields_sql($this->entityInfo['data_table'])); @@ -305,11 +267,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { $translations[$id][$langcode] = TRUE; foreach ($field_definition as $name => $definition) { - // Set only translatable properties, unless we are dealing with a - // revisable entity, in which case we did not load the untranslatable - // data before. - $translatable = !empty($definition['translatable']); - if (isset($data_fields[$name]) && ($this->revisionTable || $translatable)) { + if (isset($data_fields[$name])) { $entities[$id][$name][$langcode] = $values[$name]; } } @@ -364,6 +322,9 @@ public function save(EntityInterface $entity) { if ($this->dataTable) { $this->savePropertyData($entity); } + if ($this->revisionDataTable) { + $this->savePropertyData($entity, TRUE); + } $this->resetCache(array($entity->id())); $entity->postSave($this, TRUE); $this->invokeFieldMethod('update', $entity); @@ -382,6 +343,9 @@ public function save(EntityInterface $entity) { if ($this->dataTable) { $this->savePropertyData($entity); } + if ($this->revisionDataTable) { + $this->savePropertyData($entity, TRUE); + } // Reset general caches, but keep caches specific to certain entities. $this->resetCache(array()); @@ -415,53 +379,34 @@ public function save(EntityInterface $entity) { * The revision id. */ protected function saveRevision(EntityInterface $entity) { - $return = $entity->id(); - $default_langcode = $entity->getUntranslated()->language()->id; + $record = $this->mapToRevisionStorageRecord($entity); - if (!$entity->isNewRevision()) { - // Delete to handle removed values. - $this->database->delete($this->revisionTable) - ->condition($this->idKey, $entity->id()) - ->condition($this->revisionKey, $entity->getRevisionId()) - ->execute(); + // When saving a new revision, set any existing revision ID to NULL so as + // to ensure that a new revision will actually be created. + if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) { + $record->{$this->revisionKey} = NULL; } - $languages = $this->dataTable ? $entity->getTranslationLanguages() : array($default_langcode => $entity->language()); - foreach ($languages as $langcode => $language) { - $translation = $entity->getTranslation($langcode); - $record = $this->mapToRevisionStorageRecord($translation); - $record->langcode = $langcode; - $record->default_langcode = $langcode == $default_langcode; - - // When saving a new revision, set any existing revision ID to NULL so as - // to ensure that a new revision will actually be created. - if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) { - $record->{$this->revisionKey} = NULL; - } + $entity->preSaveRevision($this, $record); - $entity->preSaveRevision($this, $record); - - if ($entity->isNewRevision()) { - drupal_write_record($this->revisionTable, $record); - if ($entity->isDefaultRevision()) { - $this->database->update($this->entityInfo['base_table']) - ->fields(array($this->revisionKey => $record->{$this->revisionKey})) - ->condition($this->idKey, $record->{$this->idKey}) - ->execute(); - } - $entity->setNewRevision(FALSE); - } - else { - // @todo Use multiple insertions to improve performance. - drupal_write_record($this->revisionTable, $record); + if ($entity->isNewRevision()) { + drupal_write_record($this->revisionTable, $record); + if ($entity->isDefaultRevision()) { + $this->database->update($this->entityInfo['base_table']) + ->fields(array($this->revisionKey => $record->{$this->revisionKey})) + ->condition($this->idKey, $record->{$this->idKey}) + ->execute(); } - - // Make sure to update the new revision key for the entity. - $entity->{$this->revisionKey}->value = $record->{$this->revisionKey}; - $return = $record->{$this->revisionKey}; + $entity->setNewRevision(FALSE); } + else { + drupal_write_record($this->revisionTable, $record); + } + + // Make sure to update the new revision key for the entity. + $entity->{$this->revisionKey}->value = $record->{$this->revisionKey}; - return $return; + return $record->{$this->revisionKey}; } /** @@ -470,16 +415,24 @@ protected function saveRevision(EntityInterface $entity) { * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. */ - protected function savePropertyData(EntityInterface $entity) { - // Delete and insert to handle removed values. - $this->database->delete($this->dataTable) - ->condition($this->idKey, $entity->id()) - ->execute(); + protected function savePropertyData(EntityInterface $entity, $revision = FALSE) { + $table_key = $revision ? 'revision_data_table' : 'data_table'; + $table_name = $revision ? $this->revisionDataTable : $this->dataTable; + + if (!$revision || !$entity->isNewRevision()) { + $key = $revision ? $this->revisionKey : $this->idKey; + $value = $revision ? $entity->getRevisionId() : $entity->id(); + // Delete and insert to handle removed values. + $this->database->delete($table_name) + ->condition($key, $value) + ->execute(); + } - $query = $this->database->insert($this->dataTable); + $query = $this->database->insert($table_name); foreach ($entity->getTranslationLanguages() as $langcode => $language) { - $record = $this->mapToDataStorageRecord($entity, $langcode); + $translation = $entity->getTranslation($langcode); + $record = $this->mapToDataStorageRecord($translation, $table_key); $values = (array) $record; $query ->fields(array_keys($values)) @@ -490,18 +443,24 @@ protected function savePropertyData(EntityInterface $entity) { } /** - * Maps from an entity object to the storage record of the base table. + * Maps from an entity object to the storage record. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. + * @param string $table_key + * (optional) The table to prepare the record for. Defaults to the base + * table. * * @return \stdClass * The record to store. */ - protected function mapToStorageRecord(EntityInterface $entity) { + protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'base_table') { $record = new \stdClass(); - foreach (drupal_schema_fields_sql($this->entityInfo['base_table']) as $name) { - $record->$name = $entity->$name->value; + $definitions = $entity->getPropertyDefinitions(); + foreach (drupal_schema_fields_sql($this->entityInfo[$table_key]) as $name) { + if (isset($definitions[$name]) && isset($entity->$name->value)) { + $record->$name = $entity->$name->value; + } } return $record; } @@ -516,48 +475,53 @@ protected function mapToStorageRecord(EntityInterface $entity) { * The record to store. */ protected function mapToRevisionStorageRecord(EntityInterface $entity) { - $record = new \stdClass(); - $definitions = $entity->getPropertyDefinitions(); - foreach (drupal_schema_fields_sql($this->entityInfo['revision_table']) as $name) { - if (isset($definitions[$name]) && isset($entity->$name->value)) { - $record->$name = $entity->$name->value; - } - } - return $record; + return $this->mapToStorageRecord($entity, 'revision_table'); } /** - * Maps from an entity object to the storage record of the data table. + * Maps from an entity object to the storage record of the field date. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. - * @param $langcode - * The language code of the translation to get. + * @param string $table_key + * (optional) The table to prepare the record for. Defaults to the base + * table. * * @return \stdClass * The record to store. */ - protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) { + protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') { + $langcode = $entity->language()->id; $default_langcode = $entity->getUntranslated()->language()->id; - // Don't use strict mode, this way there's no need to do checks here, as - // non-translatable properties are replicated for each language. - $translation = $entity->getTranslation($langcode); - $definitions = $translation->getPropertyDefinitions(); - $schema = drupal_get_schema($this->entityInfo['data_table']); + $definitions = $entity->getPropertyDefinitions(); + $schema = drupal_get_schema($this->entityInfo[$table_key]); $record = new \stdClass(); - foreach (drupal_schema_fields_sql($this->entityInfo['data_table']) as $name) { + foreach (drupal_schema_fields_sql($this->entityInfo[$table_key]) as $name) { $info = $schema['fields'][$name]; - $value = isset($definitions[$name]) && isset($translation->$name->value) ? $translation->$name->value : NULL; + $value = isset($definitions[$name]) && isset($entity->$name->value) ? $entity->$name->value : NULL; $record->$name = drupal_schema_get_field_value($info, $value); } $record->langcode = $langcode; - $record->default_langcode = intval($default_langcode == $langcode); + $record->default_langcode = $default_langcode == $langcode; return $record; } /** + * Maps from an entity object to the storage record of the data table. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + * + * @return \stdClass + * The record to store. + */ + protected function mapToRevisionDataStorageRecord(EntityInterface $entity) { + return $this->mapToDataStorageRecord($entity, 'revision_data_table'); + } + + /** * Overwrites \Drupal\Core\Entity\DatabaseStorageController::delete(). */ public function delete(array $entities) { @@ -597,6 +561,12 @@ public function delete(array $entities) { ->execute(); } + if ($this->revisionDataTable) { + $this->database->delete($this->revisionDataTable) + ->condition($this->idKey, $ids) + ->execute(); + } + // Reset the cache as soon as the changes have been applied. $this->resetCache($ids); diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php index 0452e99..a534110 100644 --- a/core/modules/node/lib/Drupal/node/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php @@ -35,7 +35,8 @@ * }, * base_table = "node", * data_table = "node_field_data", - * revision_table = "node_field_revision", + * revision_table = "node_revision", + * revision_data_table = "node_field_revision", * uri_callback = "node_uri", * fieldable = TRUE, * translatable = TRUE, diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index 1a874fe..500e9bd 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -99,9 +99,9 @@ protected function invokeHook($hook, EntityInterface $node) { /** * {@inheritdoc} */ - protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) { + protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = 'data_table') { // @todo Remove this once comment is a regular entity field. - $record = parent::mapToDataStorageRecord($entity, $langcode); + $record = parent::mapToDataStorageRecord($entity, $table_name); $record->comment = isset($record->comment) ? intval($record->comment) : 0; return $record; } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index 7b42475..d84b05a 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -43,13 +43,6 @@ function node_schema() { 'not null' => TRUE, 'default' => '', ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this node.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), // @todo Remove the following columns when removing the legacy Content // Translation module. See http://drupal.org/node/1952062. 'tnid' => array( @@ -84,13 +77,73 @@ function node_schema() { 'primary key' => array('nid'), ); + $schema['node_revision'] = array( + 'description' => 'Stores information about each saved version of a {node}.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node} this version belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'vid' => array( + 'description' => 'The primary identifier for this version.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this version.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'log' => array( + 'description' => 'The log entry explaining the changes in this version.', + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + ), + 'revision_timestamp' => array( + 'description' => 'The Unix timestamp when the version was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'revision_uid' => array( + 'description' => 'The {users}.uid that created this version.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'nid' => array('nid'), + 'revision_uid' => array('revision_uid'), + 'node_langcode' => array('langcode'), + ), + 'foreign keys' => array( + 'versioned_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + 'version_author' => array( + 'table' => 'users', + 'columns' => array('revision_uid' => 'uid'), + ), + ), + 'primary key' => array('vid'), + ); + // Node field storage. $schema['node_field_data'] = array( - 'description' => 'Base table for node properties.', + 'description' => 'Data table for node base fields.', 'fields' => array( 'nid' => array( 'description' => 'The primary identifier for a node.', - 'type' => 'serial', + 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), @@ -180,7 +233,6 @@ function node_schema() { 'node_status_type' => array('status', 'type', 'nid'), 'node_title_type' => array('title', array('type', 4)), 'node_type' => array(array('type', 4)), - 'nid' => array('nid'), 'vid' => array('vid'), 'uid' => array('uid'), ), @@ -194,11 +246,11 @@ function node_schema() { 'columns' => array('uid' => 'uid'), ), ), - 'primary key' => array('nid', 'vid', 'langcode'), + 'primary key' => array('nid', 'langcode'), ); $schema['node_field_revision'] = array( - 'description' => 'Stores information about each saved version of a {node}.', + 'description' => 'Revision table for node base fields.', 'fields' => array( 'nid' => array( 'description' => 'The {node} this version belongs to.', @@ -208,7 +260,7 @@ function node_schema() { ), 'vid' => array( 'description' => 'The primary identifier for this version.', - 'type' => 'serial', + 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), @@ -225,25 +277,6 @@ function node_schema() { 'not null' => TRUE, 'default' => 1, ), - 'log' => array( - 'description' => 'The log entry explaining the changes in this version.', - 'type' => 'text', - 'not null' => FALSE, - 'size' => 'big', - ), - 'revision_timestamp' => array( - 'description' => 'The Unix timestamp when the version was created.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'revision_uid' => array( - 'description' => 'The {users}.uid that created this version.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), 'title' => array( 'description' => 'The title of this version, always treated as non-markup plain text.', 'type' => 'varchar', @@ -296,10 +329,7 @@ function node_schema() { ), ), 'indexes' => array( - 'nid' => array('nid'), 'uid' => array('uid'), - 'revision_uid' => array('revision_uid'), - 'vid' => array('vid'), 'node_default_langcode' => array('default_langcode'), 'node_langcode' => array('langcode'), ), @@ -308,12 +338,12 @@ function node_schema() { 'table' => 'node', 'columns' => array('nid' => 'nid'), ), - 'version_author' => array( + 'node_author' => array( 'table' => 'users', 'columns' => array('uid' => 'uid'), ), ), - 'primary key' => array('nid', 'vid', 'langcode'), + 'primary key' => array('vid', 'langcode'), ); $schema['node_access'] = array( diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php index 5ad480a..805f1db 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMulRev.php @@ -28,7 +28,8 @@ * }, * base_table = "entity_test_mulrev", * data_table = "entity_test_mulrev_property_data", - * revision_table = "entity_test_mulrev_property_revision", + * revision_table = "entity_test_mulrev_revision", + * revision_data_table = "entity_test_mulrev_property_revision", * fieldable = TRUE, * translatable = TRUE, * entity_keys = {