Newer
Older
catch
committed
<?php
catch
committed
namespace Drupal\Core\Entity\Sql;
catch
committed
use Drupal\Core\Cache\CacheBackendInterface;
catch
committed
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
Alex Bronstein
committed
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
catch
committed
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
Angie Byron
committed
use Drupal\Core\Entity\EntityBundleListenerInterface;
catch
committed
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
catch
committed
use Drupal\Core\Entity\Query\QueryInterface;
Angie Byron
committed
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
Alex Pott
committed
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Language\LanguageManagerInterface;
catch
committed
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
Alex Pott
committed
* A content entity database storage implementation.
catch
committed
*
Alex Pott
committed
* This class can be used as-is by most content entity types. Entity types
catch
committed
* requiring special handling can extend the class.
*
catch
committed
* The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
* internally in order to automatically generate the database schema based on
* the defined base fields. Entity types can override the schema handler to
* customize the generated schema; e.g., to add additional indexes.
*
* @ingroup entity_api
catch
committed
*/
Alex Pott
committed
class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
/**
* The mapping of field columns to SQL tables.
*
* @var \Drupal\Core\Entity\Sql\TableMappingInterface
*/
protected $tableMapping;
catch
committed
/**
* Name of entity's revision database table field, if it supports revisions.
*
* Has the value FALSE if this entity does not use revisions.
*
* @var string
*/
protected $revisionKey = FALSE;
/**
* The entity langcode key.
*
* @var string|bool
*/
protected $langcodeKey = FALSE;
Alex Pott
committed
/**
* The default language entity key.
*
* @var string
*/
protected $defaultLangcodeKey = FALSE;
/**
* The base table of the entity.
*
* @var string
*/
protected $baseTable;
catch
committed
/**
* The table that stores revisions, if the entity supports revisions.
*
* @var string
*/
protected $revisionTable;
/**
* The table that stores properties, if the entity has multilingual support.
*
* @var string
*/
protected $dataTable;
/**
* The table that stores revision field data if the entity supports revisions.
*
* @var string
*/
protected $revisionDataTable;
/**
* Active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The entity type's storage schema object.
*
Alex Pott
committed
* @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
*/
protected $storageSchema;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
catch
committed
/**
* {@inheritdoc}
*/
Alex Pott
committed
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
catch
committed
return new static(
Alex Pott
committed
$entity_type,
catch
committed
$container->get('database'),
$container->get('entity.manager'),
$container->get('cache.entity'),
$container->get('language_manager')
catch
committed
);
}
Angie Byron
committed
/**
* Gets the base field definitions for a content entity type.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* The array of base field definitions for the entity type, keyed by field
* name.
*/
public function getFieldStorageDefinitions() {
return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
}
catch
committed
/**
catch
committed
* Constructs a SqlContentEntityStorage object.
catch
committed
*
Alex Pott
committed
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
catch
committed
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
Alex Pott
committed
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to be used.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
catch
committed
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
parent::__construct($entity_type, $entity_manager, $cache);
catch
committed
$this->database = $database;
$this->languageManager = $language_manager;
$this->initTableLayout();
}
/**
* Initializes table name variables.
*/
protected function initTableLayout() {
// 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;
$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;
$revisionable = $this->entityType->isRevisionable();
if ($revisionable) {
$this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
$this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
}
Alex Pott
committed
$translatable = $this->entityType->isTranslatable();
if ($translatable) {
$this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
$this->langcodeKey = $this->entityType->getKey('langcode');
Alex Pott
committed
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
$this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
}
}
/**
Alex Pott
committed
* Gets the base table name.
*
* @return string
* The table name.
*/
public function getBaseTable() {
return $this->baseTable;
}
/**
Alex Pott
committed
* Gets the revision table name.
*
* @return string|false
* The table name or FALSE if it is not available.
*/
public function getRevisionTable() {
return $this->revisionTable;
}
/**
Alex Pott
committed
* Gets the data table name.
*
* @return string|false
* The table name or FALSE if it is not available.
*/
public function getDataTable() {
return $this->dataTable;
}
/**
Alex Pott
committed
* Gets the revision data table name.
*
* @return string|false
* The table name or FALSE if it is not available.
*/
public function getRevisionDataTable() {
return $this->revisionDataTable;
}
catch
committed
/**
Alex Pott
committed
* Gets the entity type's storage schema object.
*
catch
committed
* @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
*/
protected function getStorageSchema() {
if (!isset($this->storageSchema)) {
$class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
$this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
catch
committed
}
return $this->storageSchema;
}
catch
committed
/**
* Updates the wrapped entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The update entity type.
*
* @internal Only to be used internally by Entity API. Expected to be
* removed by https://www.drupal.org/node/2274017.
*/
public function setEntityType(EntityTypeInterface $entity_type) {
if ($this->entityType->id() == $entity_type->id()) {
$this->entityType = $entity_type;
$this->initTableLayout();
}
else {
catch
committed
throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
/**
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
$table_mapping = $this->tableMapping;
// 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) {
$definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
$table_mapping = new DefaultTableMapping($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(array($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()) : [];
Alex Pott
committed
$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 = array($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.
->setFieldNames($this->baseTable, $key_fields)
Alex Pott
committed
->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($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.
Alex Pott
committed
$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, array($this->uuidKey), $revision_metadata_fields));
Alex Pott
committed
$table_mapping->setFieldNames($this->dataTable, $data_fields);
$revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields);
$table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
$revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey);
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey));
Alex Pott
committed
$table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
catch
committed
}
// Add dedicated tables.
$dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->requiresDedicatedTableStorage($definition);
});
$extra_columns = array(
'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, array($field_name));
$table_mapping->setExtraColumns($table_name, $extra_columns);
}
}
// Cache the computed table mapping only if we are using our internal
// storage definitions.
if (!$storage_definitions) {
$this->tableMapping = $table_mapping;
}
catch
committed
}
return $table_mapping;
catch
committed
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
protected function doLoadMultiple(array $ids = NULL) {
// Attempt to load entities from the persistent cache. This will remove IDs
// that were loaded from $ids.
$entities_from_cache = $this->getFromPersistentCache($ids);
// Load any remaining entities from the database.
if ($entities_from_storage = $this->getFromStorage($ids)) {
$this->invokeStorageLoadHook($entities_from_storage);
$this->setPersistentCache($entities_from_storage);
}
return $entities_from_cache + $entities_from_storage;
}
/**
* Gets entities from the storage.
*
* @param array|null $ids
* If not empty, return entities that match these IDs. Return all entities
* when NULL.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of entities from the storage.
*/
protected function getFromStorage(array $ids = NULL) {
$entities = array();
if (!empty($ids)) {
// Sanitize IDs. Before feeding ID array into buildQuery, check whether
// it is empty as this would load all entities.
$ids = $this->cleanIds($ids);
}
if ($ids === NULL || $ids) {
// Build and execute the query.
$query_result = $this->buildQuery($ids)->execute();
$records = $query_result->fetchAllAssoc($this->idKey);
// Map the loaded records into entity objects and according fields.
if ($records) {
$entities = $this->mapFromStorageRecords($records);
}
}
return $entities;
}
catch
committed
/**
* Maps from storage records to entity objects, and attaches fields.
Alex Pott
committed
*
catch
committed
* @param array $records
* Associative array of query results, keyed on the entity ID.
Alex Pott
committed
* @param bool $load_from_revision
* Flag to indicate whether revisions should be loaded or not.
catch
committed
*
* @return array
* An array of entity objects implementing the EntityInterface.
*/
Alex Pott
committed
protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
Alex Pott
committed
if (!$records) {
return array();
}
Alex Pott
committed
$values = array();
catch
committed
foreach ($records as $id => $record) {
Alex Pott
committed
$values[$id] = array();
catch
committed
// 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.
catch
committed
foreach ($record as $name => $value) {
catch
committed
// 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);
Alex Pott
committed
$values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
catch
committed
}
else {
// Handle columns named directly after the field (e.g if the field
// type only stores one property).
Alex Pott
committed
$values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
catch
committed
}
catch
committed
}
}
Alex Pott
committed
Alex Pott
committed
// Initialize translations array.
$translations = array_fill_keys(array_keys($values), array());
// Load values from shared and dedicated tables.
$this->loadFromSharedTables($values, $translations);
$this->loadFromDedicatedTables($values, $load_from_revision);
$entities = array();
foreach ($values as $id => $entity_values) {
$bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
// Turn the record into an entity class.
$entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
}
Alex Pott
committed
catch
committed
return $entities;
}
/**
Alex Pott
committed
* Loads values for fields stored in the shared data tables.
catch
committed
*
Alex Pott
committed
* @param array &$values
* Associative array of entities values, keyed on the entity ID.
* @param array &$translations
* List of translations, keyed on the entity ID.
catch
committed
*/
Alex Pott
committed
protected function loadFromSharedTables(array &$values, array &$translations) {
catch
committed
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->revisionDataTable ?: $this->dataTable;
Alex Pott
committed
$alias = $this->revisionDataTable ? 'revision' : 'data';
$query = $this->database->select($table, $alias, array('fetch' => \PDO::FETCH_ASSOC))
->fields($alias)
Alex Pott
committed
->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
Alex Pott
committed
->orderBy($alias . '.' . $this->idKey);
catch
committed
Alex Pott
committed
$table_mapping = $this->getTableMapping();
catch
committed
if ($this->revisionDataTable) {
Alex Pott
committed
// Find revisioned fields that are not entity keys. Exclude the langcode
// key as the base table holds only the default language.
$base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), array($this->langcodeKey));
$revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
Alex Pott
committed
// Find fields that are not revisioned or entity keys. Data fields have
// the same value regardless of entity revision.
$data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
// If there are no data fields then only revisioned fields are needed
// else both data fields and revisioned fields are needed to map the
// entity values.
$all_fields = $revisioned_fields;
Alex Pott
committed
if ($data_fields) {
$all_fields = array_merge($revisioned_fields, $data_fields);
$query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
$column_names = [];
// 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()
// 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.
$column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
}
$query->fields('data', $column_names);
Alex Pott
committed
}
Alex Pott
committed
// Get the revision IDs.
$revision_ids = array();
Alex Pott
committed
foreach ($values as $entity_values) {
$revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
catch
committed
}
Alex Pott
committed
$query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
catch
committed
}
else {
$all_fields = $table_mapping->getFieldNames($this->dataTable);
catch
committed
}
Alex Pott
committed
$result = $query->execute();
foreach ($result as $row) {
$id = $row[$this->idKey];
catch
committed
// Field values in default language are stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
Alex Pott
committed
$langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
catch
committed
$translations[$id][$langcode] = TRUE;
foreach ($all_fields as $field_name) {
$columns = $table_mapping->getColumnNames($field_name);
// Do not key single-column fields by property name.
if (count($columns) == 1) {
Alex Pott
committed
$values[$id][$field_name][$langcode] = $row[reset($columns)];
catch
committed
}
else {
foreach ($columns as $property_name => $column_name) {
Alex Pott
committed
$values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
catch
committed
}
catch
committed
}
}
}
}
}
/**
* {@inheritdoc}
catch
committed
*/
protected function doLoadRevisionFieldItems($revision_id) {
$revision = NULL;
catch
committed
// Build and execute the query.
$query_result = $this->buildQuery(array(), $revision_id)->execute();
Alex Pott
committed
$records = $query_result->fetchAllAssoc($this->idKey);
catch
committed
Alex Pott
committed
if (!empty($records)) {
// Convert the raw records to entity objects.
Alex Pott
committed
$entities = $this->mapFromStorageRecords($records, TRUE);
$revision = reset($entities) ?: NULL;
catch
committed
}
return $revision;
catch
committed
}
/**
* {@inheritdoc}
catch
committed
*/
protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
$this->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->deleteRevisionFromDedicatedTables($revision);
catch
committed
}
/**
Angie Byron
committed
* {@inheritdoc}
catch
committed
*/
protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
if ($this->dataTable) {
// @todo We should not be using a condition to specify whether conditions
// apply to the default language. See
// https://www.drupal.org/node/1866330.
catch
committed
// Default to the original entity language if not explicitly specified
// otherwise.
Alex Pott
committed
if (!array_key_exists($this->defaultLangcodeKey, $values)) {
$values[$this->defaultLangcodeKey] = 1;
catch
committed
}
// If the 'default_langcode' flag is explicitly not set, we do not care
// whether the queried values are in the original entity language or not.
Alex Pott
committed
elseif ($values[$this->defaultLangcodeKey] === NULL) {
unset($values[$this->defaultLangcodeKey]);
catch
committed
}
}
Angie Byron
committed
parent::buildPropertyQuery($entity_query, $values);
catch
committed
}
/**
* Builds the query to load the entity.
*
* This has full revision support. For entities requiring special queries,
* the class can be extended, and the default query can be constructed by
* calling parent::buildQuery(). This is usually necessary when the object
* being loaded needs to be augmented with additional data from another
* table, such as loading node type into comments or vocabulary machine name
* into terms, however it can also support $conditions on different tables.
catch
committed
* See Drupal\comment\CommentStorage::buildQuery() for an example.
catch
committed
*
* @param array|null $ids
* An array of entity IDs, or NULL to load all entities.
* @param $revision_id
* The ID of the revision to load, or FALSE if this query is asking for the
* most current revision(s).
*
Alex Pott
committed
* @return \Drupal\Core\Database\Query\Select
catch
committed
* A SelectQuery object for loading the entity.
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = $this->database->select($this->entityType->getBaseTable(), 'base');
catch
committed
$query->addTag($this->entityTypeId . '_load_multiple');
catch
committed
if ($revision_id) {
$query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id));
}
elseif ($this->revisionTable) {
$query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
}
// Add fields from the {entity} table.
$table_mapping = $this->getTableMapping();
$entity_fields = $table_mapping->getAllColumns($this->baseTable);
catch
committed
if ($this->revisionTable) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
catch
committed
$entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
catch
committed
// 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.
Alex Pott
committed
$query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
catch
committed
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
return $query;
}
/**
* {@inheritdoc}
catch
committed
*/
public function delete(array $entities) {
if (!$entities) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$transaction = $this->database->startTransaction();
try {
Alex Pott
committed
parent::delete($entities);
catch
committed
// Ignore replica server temporarily.
db_ignore_replica();
catch
committed
}
catch (\Exception $e) {
$transaction->rollBack();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteFieldItems($entities) {
Alex Pott
committed
$ids = array_keys($entities);
catch
committed
Alex Pott
committed
$this->database->delete($this->entityType->getBaseTable())
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
catch
committed
Alex Pott
committed
if ($this->revisionTable) {
$this->database->delete($this->revisionTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
if ($this->dataTable) {
$this->database->delete($this->dataTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
if ($this->revisionDataTable) {
$this->database->delete($this->revisionDataTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
foreach ($entities as $entity) {
Alex Pott
committed
$this->deleteFromDedicatedTables($entity);
Alex Pott
committed
}
}
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
$transaction = $this->database->startTransaction();
try {
$return = parent::save($entity);
catch
committed
// Ignore replica server temporarily.
db_ignore_replica();
catch
committed
return $return;
}
catch (\Exception $e) {
$transaction->rollBack();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
$full_save = empty($names);
$update = !$full_save || !$entity->isNew();
Alex Pott
committed
if ($full_save) {
$shared_table_fields = TRUE;
$dedicated_table_fields = TRUE;
Alex Pott
committed
}
else {
$table_mapping = $this->getTableMapping();
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
$shared_table_fields = FALSE;
$dedicated_table_fields = [];
// Collect the name of fields to be written in dedicated tables and check
// whether shared table records need to be updated.
foreach ($names as $name) {
$storage_definition = $storage_definitions[$name];
if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
$shared_table_fields = TRUE;
}
elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$dedicated_table_fields[] = $name;
}
Alex Pott
committed
}
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
}
// Update shared table records if necessary.
if ($shared_table_fields) {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
// Create the storage record to be saved.
if ($update) {
$default_revision = $entity->isDefaultRevision();
if ($default_revision) {
$this->database
->update($this->baseTable)
->fields((array) $record)
->condition($this->idKey, $record->{$this->idKey})
->execute();
}
if ($this->revisionTable) {
if ($full_save) {
$entity->{$this->revisionKey} = $this->saveRevision($entity);
}
else {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
$entity->preSaveRevision($this, $record);
$this->database
->update($this->revisionTable)
->fields((array) $record)
->condition($this->revisionKey, $record->{$this->revisionKey})
->execute();
}
}
if ($default_revision && $this->dataTable) {
$this->saveToSharedTables($entity);
}
if ($this->revisionDataTable) {
$new_revision = $full_save && $entity->isNewRevision();
$this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
}
Alex Pott
committed
}
else {
$insert_id = $this->database
->insert($this->baseTable, array('return' => Database::RETURN_INSERT_ID))
->fields((array) $record)
->execute();
// Even if this is a new entity the ID key might have been set, in which
// case we should not override the provided ID. An ID key that is not set
// to any value is interpreted as NULL (or DEFAULT) and thus overridden.
if (!isset($record->{$this->idKey})) {
$record->{$this->idKey} = $insert_id;
}
$entity->{$this->idKey} = (string) $record->{$this->idKey};
if ($this->revisionTable) {
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
if ($this->dataTable) {
$this->saveToSharedTables($entity);
}
if ($this->revisionDataTable) {
$this->saveToSharedTables($entity, $this->revisionDataTable);
}
Alex Pott
committed
}
}
// Update dedicated table records if necessary.
if ($dedicated_table_fields) {
$names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
$this->saveToDedicatedTables($entity, $update, $names);
Alex Pott
committed
}
/**
* {@inheritdoc}
*/
protected function has($id, EntityInterface $entity) {
return !$entity->isNew();
}
catch
committed
/**
Alex Pott
committed
* Saves fields that use the shared tables.
catch
committed
*
Alex Pott
committed
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
catch
committed
* The entity object.
* @param string $table_name
* (optional) The table name to save to. Defaults to the data table.
* @param bool $new_revision
* (optional) Whether we are dealing with a new revision. By default fetches
* the information from the entity object.
catch
committed
*/
protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
if (!isset($table_name)) {
$table_name = $this->dataTable;
}
if (!isset($new_revision)) {
$new_revision = $entity->isNewRevision();
}
$revision = $table_name != $this->dataTable;
catch
committed
if (!$revision || !$new_revision) {
catch
committed
$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($table_name);
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
$record = $this->mapToDataStorageRecord($translation, $table_name);
catch
committed
$values = (array) $record;
$query
->fields(array_keys($values))
->values($values);
}
$query->execute();
}
/**
* Maps from an entity object to the storage record.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
catch
committed
* The entity object.
* @param string $table_name
* (optional) The table name to map records to. Defaults to the base table.
catch
committed
*
* @return \stdClass
* The record to store.
*/
protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
if (!isset($table_name)) {
$table_name = $this->baseTable;
}
catch
committed
$record = new \stdClass();
$table_mapping = $this->getTableMapping();
foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
catch
committed
Angie Byron
committed
if (empty($this->getFieldStorageDefinitions()[$field_name])) {
catch
committed
throw new EntityStorageException("Table mapping contains invalid field $field_name.");
catch
committed
}
Angie Byron
committed
$definition = $this->getFieldStorageDefinitions()[$field_name];
$columns = $table_mapping->getColumnNames($field_name);
foreach ($columns as $column_name => $schema_name) {
// If there is no main property and only a single column, get all
// properties from the first field item and assume that they will be
// stored serialized.
// @todo Give field types more control over this behavior in
// https://www.drupal.org/node/2232427.
if (!$definition->getMainPropertyName() && count($columns) == 1) {
$value = ($item = $entity->$field_name->first()) ? $item->getValue() : array();
else {
$value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
$value = serialize($value);
catch
committed
}
Dries Buytaert
committed
// Do not set serial fields if we do not have a value. This supports all
// SQL database drivers.
// @see https://www.drupal.org/node/2279395
$value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
Dries Buytaert
committed
$record->$schema_name = $value;
}
catch
committed
}
}
return $record;
}
/**
* Checks whether a field column should be treated as serial.
*
* @param $table_name
* The name of the table the field column belongs to.
* @param $schema_name
* The schema name of the field column.
*
* @return bool
* TRUE if the column is serial, FALSE otherwise.
*
catch
committed
* @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
* @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
*/
protected function isColumnSerial($table_name, $schema_name) {
$result = FALSE;
switch ($table_name) {
case $this->baseTable:
$result = $schema_name == $this->idKey;
break;
case $this->revisionTable:
$result = $schema_name == $this->revisionKey;
break;
}
return $result;
}
catch
committed
/**
* Maps from an entity object to the storage record of the field data.