namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityBundleListenerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
$container->get('database'),
$container->get('entity.manager'),
$container->get('cache.entity'),
- $container->get('language_manager')
+ $container->get('language_manager'),
+ $container->get('entity.memory_cache')
);
}
* The cache backend to be used.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
+ * The memory cache backend to be used.
*/
- public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
- parent::__construct($entity_type, $entity_manager, $cache);
+ public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
+ parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
$this->database = $database;
$this->languageManager = $language_manager;
$this->initTableLayout();
$this->dataTable = NULL;
$this->revisionDataTable = NULL;
- // @todo Remove table names from the entity type definition in
- // https://www.drupal.org/node/2232465.
- $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
+ $table_mapping = $this->getTableMapping();
+ $this->baseTable = $table_mapping->getBaseTable();
$revisionable = $this->entityType->isRevisionable();
if ($revisionable) {
$this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
- $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
+ $this->revisionTable = $table_mapping->getRevisionTable();
}
$translatable = $this->entityType->isTranslatable();
if ($translatable) {
- $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
+ $this->dataTable = $table_mapping->getDataTable();
$this->langcodeKey = $this->entityType->getKey('langcode');
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
- $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
+ $this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
}
*/
public function setTableMapping(TableMappingInterface $table_mapping) {
$this->tableMapping = $table_mapping;
+
+ $this->baseTable = $table_mapping->getBaseTable();
+ $this->revisionTable = $table_mapping->getRevisionTable();
+ $this->dataTable = $table_mapping->getDataTable();
+ $this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
/**
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
- $table_mapping = $this->tableMapping;
+ // If a new set of field storage definitions is passed, for instance when
+ // comparing old and new storage schema, we compute the table mapping
+ // without caching.
+ if ($storage_definitions) {
+ return $this->getCustomTableMapping($this->entityType, $storage_definitions);
+ }
// If we are using our internal storage definitions, which is our main use
- // case, we can statically cache the computed table mapping. If a new set
- // of field storage definitions is passed, for instance when comparing old
- // and new storage schema, we compute the table mapping without caching.
- // @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);
- /** @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);
- });
-
- $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
- $all_fields = array_keys($shared_table_definitions);
- $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
- return $definition->isRevisionable();
- }));
- // Make sure the key fields come first in the list of fields.
- $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
-
- // If the entity is revisionable, gather the fields that need to be put
- // in the revision table.
- $revisionable = $this->entityType->isRevisionable();
- $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
-
- $translatable = $this->entityType->isTranslatable();
- if (!$revisionable && !$translatable) {
- // The base layout stores all the base field values in the base table.
- $table_mapping->setFieldNames($this->baseTable, $all_fields);
- }
- elseif ($revisionable && !$translatable) {
- // The revisionable layout stores all the base field values in the base
- // table, except for revision metadata fields. Revisionable fields
- // denormalized in the base table but also stored in the revision table
- // together with the entity ID and the revision ID as identifiers.
- $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
- $revision_key_fields = [$this->idKey, $this->revisionKey];
- $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
- }
- elseif (!$revisionable && $translatable) {
- // Multilingual layouts store key field values in the base table. The
- // other base field values are stored in the data table, no matter
- // whether they are translatable or not. The data table holds also a
- // denormalized copy of the bundle field value to allow for more
- // performant queries. This means that only the UUID is not stored on
- // the data table.
- $table_mapping
- ->setFieldNames($this->baseTable, $key_fields)
- ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
- }
- elseif ($revisionable && $translatable) {
- // The revisionable multilingual layout stores key field values in the
- // base table, except for language, which is stored in the revision
- // table along with revision metadata. The revision data table holds
- // data field values for all the revisionable fields and the data table
- // holds the data field values for all non-revisionable fields. The data
- // field values of revisionable fields are denormalized in the data
- // table, as well.
- $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
-
- // Like in the multilingual, non-revisionable case the UUID is not
- // in the data table. Additionally, do not store revision metadata
- // fields in the data table.
- $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
- $table_mapping->setFieldNames($this->dataTable, $data_fields);
-
- $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
- $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
-
- $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
- $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
- $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
- }
-
- // Add dedicated tables.
- $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
- return $table_mapping->requiresDedicatedTableStorage($definition);
- });
- $extra_columns = [
- 'bundle',
- 'deleted',
- 'entity_id',
- 'revision_id',
- 'langcode',
- 'delta',
- ];
- foreach ($dedicated_table_definitions as $field_name => $definition) {
- $tables = [$table_mapping->getDedicatedDataTableName($definition)];
- if ($revisionable && $definition->isRevisionable()) {
- $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
- }
- foreach ($tables as $table_name) {
- $table_mapping->setFieldNames($table_name, [$field_name]);
- $table_mapping->setExtraColumns($table_name, $extra_columns);
- }
- }
+ // case, we can statically cache the computed table mapping.
+ if (!isset($this->tableMapping)) {
+ $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- // Cache the computed table mapping only if we are using our internal
- // storage definitions.
- if (!$storage_definitions) {
- $this->tableMapping = $table_mapping;
- }
+ $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
}
- return $table_mapping;
+ return $this->tableMapping;
+ }
+
+ /**
+ * Gets a table mapping for the specified entity type and storage definitions.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+ * An entity type definition.
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+ * An array of field storage definitions to be used to compute the table
+ * mapping.
+ *
+ * @return \Drupal\Core\Entity\Sql\TableMappingInterface
+ * A table mapping object for the entity's tables.
+ *
+ * @internal
+ */
+ public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
+ $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
+ return $table_mapping_class::create($entity_type, $storage_definitions);
}
/**
return [];
}
+ // Get the names of the fields that are stored in the base table and, if
+ // applicable, the revision table. Other entity data will be loaded in
+ // loadFromSharedTables() and loadFromDedicatedTables().
+ $field_names = $this->tableMapping->getFieldNames($this->baseTable);
+ if ($this->revisionTable) {
+ $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable)));
+ }
+
$values = [];
foreach ($records as $id => $record) {
$values[$id] = [];
// Skip the item delta and item value levels (if possible) but let the
// field assign the value as suiting. This avoids unnecessary array
// hierarchies and saves memory here.
- foreach ($record as $name => $value) {
- // Handle columns named [field_name]__[column_name] (e.g for field types
- // that store several properties).
- if ($field_name = strstr($name, '__', TRUE)) {
- $property_name = substr($name, strpos($name, '__') + 2);
- $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
+ foreach ($field_names as $field_name) {
+ $field_columns = $this->tableMapping->getColumnNames($field_name);
+ // Handle field types that store several properties.
+ if (count($field_columns) > 1) {
+ foreach ($field_columns as $property_name => $column_name) {
+ if (property_exists($record, $column_name)) {
+ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $record->{$column_name};
+ unset($record->{$column_name});
+ }
+ }
}
+ // Handle field types that store only one property.
else {
- // Handle columns named directly after the field (e.g if the field
- // type only stores one property).
- $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
+ $column_name = reset($field_columns);
+ if (property_exists($record, $column_name)) {
+ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $record->{$column_name};
+ unset($record->{$column_name});
+ }
}
}
+
+ // Handle additional record entries that are not provided by an entity
+ // field, such as 'isDefaultRevision'.
+ foreach ($record as $name => $value) {
+ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
+ }
}
// Initialize translations array.
// Some fields can have more then one columns in the data table so
// column names are needed.
foreach ($data_fields as $data_field) {
- // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
+ // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
// returns an array keyed by property names so remove the keys
// before array_merge() to avoid losing data with fields having the
// same columns i.e. value.
* A SelectQuery object for loading the entity.
*/
protected function buildQuery($ids, $revision_ids = FALSE) {
- $query = $this->database->select($this->entityType->getBaseTable(), 'base');
+ $query = $this->database->select($this->baseTable, 'base');
$query->addTag($this->entityTypeId . '_load_multiple');
protected function doDeleteFieldItems($entities) {
$ids = array_keys($entities);
- $this->database->delete($this->entityType->getBaseTable())
+ $this->database->delete($this->baseTable)
->condition($this->idKey, $ids, 'IN')
->execute();
$record->{$this->revisionKey} = $insert_id;
}
if ($entity->isDefaultRevision()) {
- $this->database->update($this->entityType->getBaseTable())
+ $this->database->update($this->baseTable)
->fields([$this->revisionKey => $record->{$this->revisionKey}])
->condition($this->idKey, $record->{$this->idKey})
->execute();
* {@inheritdoc}
*/
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
- // If we are adding a field stored in a shared table we need to recompute
- // the table mapping.
- // @todo This does not belong here. Remove it once we are able to generate a
- // fresh table mapping in the schema handler. See
- // https://www.drupal.org/node/2274017.
- if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
- $this->tableMapping = NULL;
- }
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
// Ascertain the table this field is mapped too.
$field_name = $storage_definition->getName();
- try {
- $table_name = $table_mapping->getFieldTableName($field_name);
- }
- catch (SqlContentEntityStorageException $e) {
- // This may happen when changing field storage schema, since we are not
- // able to use a table mapping matching the passed storage definition.
- // @todo Revisit this once we are able to instantiate the table mapping
- // properly. See https://www.drupal.org/node/2274017.
- $table_name = $this->dataTable ?: $this->baseTable;
- }
+ $table_name = $table_mapping->getFieldTableName($field_name);
$query = $this->database->select($table_name, 't');
$or = $query->orConditionGroup();
foreach (array_keys($storage_definition->getColumns()) as $property_name) {