Newer
Older
catch
committed
<?php
catch
committed
namespace Drupal\Core\Entity\Sql;
catch
committed
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
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;
Francesco Placella
committed
use Drupal\Core\Entity\ContentEntityTypeInterface;
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\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;
/**
* Whether this storage should use the temporary table mapping.
*
* @var bool
*/
protected $temporary = FALSE;
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'),
$container->get('entity.memory_cache')
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.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
* The memory cache backend to be used.
catch
committed
*/
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);
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;
Francesco Placella
committed
$table_mapping = $this->getTableMapping();
$this->baseTable = $table_mapping->getBaseTable();
$revisionable = $this->entityType->isRevisionable();
if ($revisionable) {
$this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
Francesco Placella
committed
$this->revisionTable = $table_mapping->getRevisionTable();
}
Alex Pott
committed
$translatable = $this->entityType->isTranslatable();
if ($translatable) {
Francesco Placella
committed
$this->dataTable = $table_mapping->getDataTable();
$this->langcodeKey = $this->entityType->getKey('langcode');
Alex Pott
committed
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
Francesco Placella
committed
$this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
}
/**
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()}");
/**
* Sets the wrapped table mapping definition.
*
* @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
* The table mapping.
*
* @internal Only to be used internally by Entity API. Expected to be removed
* by https://www.drupal.org/node/2554235.
*/
public function setTableMapping(TableMappingInterface $table_mapping) {
$this->tableMapping = $table_mapping;
Francesco Placella
committed
$this->baseTable = $table_mapping->getBaseTable();
$this->revisionTable = $table_mapping->getRevisionTable();
$this->dataTable = $table_mapping->getDataTable();
$this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
/**
* Changes the temporary state of the storage.
*
* @param bool $temporary
* Whether to use a temporary table mapping or not.
*
* @internal Only to be used internally by Entity API.
*/
public function setTemporary($temporary) {
$this->temporary = $temporary;
}
/**
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
Francesco Placella
committed
// 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
Francesco Placella
committed
// case, we can statically cache the computed table mapping.
if (!isset($this->tableMapping)) {
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
$this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
catch
committed
}
Francesco Placella
committed
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);
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 = [];
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 or revision
* ID.
Alex Pott
committed
* @param bool $load_from_revision
* (optional) Flag to indicate whether revisions should be loaded or not.
* Defaults to FALSE.
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 [];
Alex Pott
committed
}
Francesco Placella
committed
// 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 = [];
catch
committed
foreach ($records as $id => $record) {
$values[$id] = [];
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.
Francesco Placella
committed
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});
}
}
catch
committed
}
Francesco Placella
committed
// Handle field types that store only one property.
catch
committed
else {
Francesco Placella
committed
$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});
}
catch
committed
}
catch
committed
}
Francesco Placella
committed
// 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;
}
catch
committed
}
Alex Pott
committed
Alex Pott
committed
// Initialize translations array.
$translations = array_fill_keys(array_keys($values), []);
Alex Pott
committed
// Load values from shared and dedicated tables.
$this->loadFromSharedTables($values, $translations, $load_from_revision);
Alex Pott
committed
$this->loadFromDedicatedTables($values, $load_from_revision);
$entities = [];
Alex Pott
committed
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 or the
* revision ID.
Alex Pott
committed
* @param array &$translations
* List of translations, keyed on the entity ID.
* @param bool $load_from_revision
* Flag to indicate whether revisions should be loaded or not.
catch
committed
*/
protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
$record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
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, ['fetch' => \PDO::FETCH_ASSOC])
Alex Pott
committed
->fields($alias)
->condition($alias . '.' . $record_key, array_keys($values), 'IN')
->orderBy($alias . '.' . $record_key);
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), [$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) {
Alex Pott
committed
// \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.
$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 = [];
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[$record_key];
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) {
@trigger_error('"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
$revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
return !empty($revisions) ? reset($revisions) : NULL;
}
/**
* {@inheritdoc}
*/
protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
$revisions = [];
catch
committed
// Sanitize IDs. Before feeding ID array into buildQuery, check whether
// it is empty as this would load all entity revisions.
$revision_ids = $this->cleanIds($revision_ids, 'revision');
if (!empty($revision_ids)) {
// Build and execute the query.
$query_result = $this->buildQuery(NULL, $revision_ids)->execute();
$records = $query_result->fetchAllAssoc($this->revisionKey);
// Map the loaded records into entity objects and according fields.
if ($records) {
$revisions = $this->mapFromStorageRecords($records, TRUE);
}
catch
committed
}
return $revisions;
catch
committed
}
/**
* {@inheritdoc}
catch
committed
*/
protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
$this->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
if ($this->revisionDataTable) {
$this->database->delete($this->revisionDataTable)
->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 array|bool $revision_ids
* The IDs of the revisions to load, or FALSE if this query is asking for
* the default revisions. Defaults to FALSE.
catch
committed
*
Alex Pott
committed
* @return \Drupal\Core\Database\Query\Select
catch
committed
* A SelectQuery object for loading the entity.
*/
protected function buildQuery($ids, $revision_ids = FALSE) {
Francesco Placella
committed
$query = $this->database->select($this->baseTable, 'base');
catch
committed
$query->addTag($this->entityTypeId . '_load_multiple');
catch
committed
if ($revision_ids) {
if (!is_array($revision_ids)) {
@trigger_error('Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
}
$query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
catch
committed
}
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
Francesco Placella
committed
$this->database->delete($this->baseTable)
->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
}
}
// 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) {
$id = $record->{$this->idKey};
// Remove the ID from the record to enable updates on SQL variants
// that prevent updating serial columns, for example, mssql.
unset($record->{$this->idKey});
$this->database
->update($this->baseTable)
->fields((array) $record)
->condition($this->idKey, $id)
->execute();
}
if ($this->revisionTable) {
if ($full_save) {
$entity->{$this->revisionKey} = $this->saveRevision($entity);
}
else {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
// Remove the revision ID from the record to enable updates on SQL
// variants that prevent updating serial columns, for example,
// mssql.
unset($record->{$this->revisionKey});
$entity->preSaveRevision($this, $record);
$this->database
->update($this->revisionTable)
->fields((array) $record)
->condition($this->revisionKey, $entity->getRevisionId())
->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, ['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() : [];
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;
}