3 namespace Drupal\Core\Entity\Sql;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\Database;
8 use Drupal\Core\Database\DatabaseExceptionWrapper;
9 use Drupal\Core\Database\SchemaException;
10 use Drupal\Core\Entity\ContentEntityInterface;
11 use Drupal\Core\Entity\ContentEntityStorageBase;
12 use Drupal\Core\Entity\EntityBundleListenerInterface;
13 use Drupal\Core\Entity\EntityInterface;
14 use Drupal\Core\Entity\EntityManagerInterface;
15 use Drupal\Core\Entity\EntityStorageException;
16 use Drupal\Core\Entity\EntityTypeInterface;
17 use Drupal\Core\Entity\Query\QueryInterface;
18 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
19 use Drupal\Core\Field\FieldDefinitionInterface;
20 use Drupal\Core\Field\FieldStorageDefinitionInterface;
21 use Drupal\Core\Language\LanguageInterface;
22 use Drupal\Core\Language\LanguageManagerInterface;
23 use Symfony\Component\DependencyInjection\ContainerInterface;
26 * A content entity database storage implementation.
28 * This class can be used as-is by most content entity types. Entity types
29 * requiring special handling can extend the class.
31 * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
32 * internally in order to automatically generate the database schema based on
33 * the defined base fields. Entity types can override the schema handler to
34 * customize the generated schema; e.g., to add additional indexes.
38 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
41 * The mapping of field columns to SQL tables.
43 * @var \Drupal\Core\Entity\Sql\TableMappingInterface
45 protected $tableMapping;
48 * Name of entity's revision database table field, if it supports revisions.
50 * Has the value FALSE if this entity does not use revisions.
54 protected $revisionKey = FALSE;
57 * The entity langcode key.
61 protected $langcodeKey = FALSE;
64 * The default language entity key.
68 protected $defaultLangcodeKey = FALSE;
71 * The base table of the entity.
78 * The table that stores revisions, if the entity supports revisions.
82 protected $revisionTable;
85 * The table that stores properties, if the entity has multilingual support.
92 * The table that stores revision field data if the entity supports revisions.
96 protected $revisionDataTable;
99 * Active database connection.
101 * @var \Drupal\Core\Database\Connection
106 * The entity type's storage schema object.
108 * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
110 protected $storageSchema;
113 * The language manager.
115 * @var \Drupal\Core\Language\LanguageManagerInterface
117 protected $languageManager;
120 * Whether this storage should use the temporary table mapping.
124 protected $temporary = FALSE;
129 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
132 $container->get('database'),
133 $container->get('entity.manager'),
134 $container->get('cache.entity'),
135 $container->get('language_manager')
140 * Gets the base field definitions for a content entity type.
142 * @return \Drupal\Core\Field\FieldDefinitionInterface[]
143 * The array of base field definitions for the entity type, keyed by field
146 public function getFieldStorageDefinitions() {
147 return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
151 * Constructs a SqlContentEntityStorage object.
153 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
154 * The entity type definition.
155 * @param \Drupal\Core\Database\Connection $database
156 * The database connection to be used.
157 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
158 * The entity manager.
159 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
160 * The cache backend to be used.
161 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
162 * The language manager.
164 public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
165 parent::__construct($entity_type, $entity_manager, $cache);
166 $this->database = $database;
167 $this->languageManager = $language_manager;
168 $this->initTableLayout();
172 * Initializes table name variables.
174 protected function initTableLayout() {
175 // Reset table field values to ensure changes in the entity type definition
176 // are correctly reflected in the table layout.
177 $this->tableMapping = NULL;
178 $this->revisionKey = NULL;
179 $this->revisionTable = NULL;
180 $this->dataTable = NULL;
181 $this->revisionDataTable = NULL;
183 // @todo Remove table names from the entity type definition in
184 // https://www.drupal.org/node/2232465.
185 $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
186 $revisionable = $this->entityType->isRevisionable();
188 $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
189 $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
191 $translatable = $this->entityType->isTranslatable();
193 $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
194 $this->langcodeKey = $this->entityType->getKey('langcode');
195 $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
197 if ($revisionable && $translatable) {
198 $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
203 * Gets the base table name.
208 public function getBaseTable() {
209 return $this->baseTable;
213 * Gets the revision table name.
215 * @return string|false
216 * The table name or FALSE if it is not available.
218 public function getRevisionTable() {
219 return $this->revisionTable;
223 * Gets the data table name.
225 * @return string|false
226 * The table name or FALSE if it is not available.
228 public function getDataTable() {
229 return $this->dataTable;
233 * Gets the revision data table name.
235 * @return string|false
236 * The table name or FALSE if it is not available.
238 public function getRevisionDataTable() {
239 return $this->revisionDataTable;
243 * Gets the entity type's storage schema object.
245 * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
248 protected function getStorageSchema() {
249 if (!isset($this->storageSchema)) {
250 $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
251 $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
253 return $this->storageSchema;
257 * Updates the wrapped entity type definition.
259 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
260 * The update entity type.
262 * @internal Only to be used internally by Entity API. Expected to be
263 * removed by https://www.drupal.org/node/2274017.
265 public function setEntityType(EntityTypeInterface $entity_type) {
266 if ($this->entityType->id() == $entity_type->id()) {
267 $this->entityType = $entity_type;
268 $this->initTableLayout();
271 throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
276 * Sets the wrapped table mapping definition.
278 * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
281 * @internal Only to be used internally by Entity API. Expected to be removed
282 * by https://www.drupal.org/node/2554235.
284 public function setTableMapping(TableMappingInterface $table_mapping) {
285 $this->tableMapping = $table_mapping;
289 * Changes the temporary state of the storage.
291 * @param bool $temporary
292 * Whether to use a temporary table mapping or not.
294 * @internal Only to be used internally by Entity API.
296 public function setTemporary($temporary) {
297 $this->temporary = $temporary;
303 public function getTableMapping(array $storage_definitions = NULL) {
304 $table_mapping = $this->tableMapping;
306 // If we are using our internal storage definitions, which is our main use
307 // case, we can statically cache the computed table mapping. If a new set
308 // of field storage definitions is passed, for instance when comparing old
309 // and new storage schema, we compute the table mapping without caching.
310 // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
311 // easily instantiate a new table mapping whenever needed.
312 if (!isset($this->tableMapping) || $storage_definitions) {
313 $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
314 $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
315 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
316 $table_mapping = new $table_mapping_class($this->entityType, $definitions);
318 $shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
319 return $table_mapping->allowsSharedTableStorage($definition);
322 $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
323 $all_fields = array_keys($shared_table_definitions);
324 $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
325 return $definition->isRevisionable();
327 // Make sure the key fields come first in the list of fields.
328 $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
330 // If the entity is revisionable, gather the fields that need to be put
331 // in the revision table.
332 $revisionable = $this->entityType->isRevisionable();
333 $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
335 $translatable = $this->entityType->isTranslatable();
336 if (!$revisionable && !$translatable) {
337 // The base layout stores all the base field values in the base table.
338 $table_mapping->setFieldNames($this->baseTable, $all_fields);
340 elseif ($revisionable && !$translatable) {
341 // The revisionable layout stores all the base field values in the base
342 // table, except for revision metadata fields. Revisionable fields
343 // denormalized in the base table but also stored in the revision table
344 // together with the entity ID and the revision ID as identifiers.
345 $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
346 $revision_key_fields = [$this->idKey, $this->revisionKey];
347 $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
349 elseif (!$revisionable && $translatable) {
350 // Multilingual layouts store key field values in the base table. The
351 // other base field values are stored in the data table, no matter
352 // whether they are translatable or not. The data table holds also a
353 // denormalized copy of the bundle field value to allow for more
354 // performant queries. This means that only the UUID is not stored on
357 ->setFieldNames($this->baseTable, $key_fields)
358 ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
360 elseif ($revisionable && $translatable) {
361 // The revisionable multilingual layout stores key field values in the
362 // base table, except for language, which is stored in the revision
363 // table along with revision metadata. The revision data table holds
364 // data field values for all the revisionable fields and the data table
365 // holds the data field values for all non-revisionable fields. The data
366 // field values of revisionable fields are denormalized in the data
368 $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
370 // Like in the multilingual, non-revisionable case the UUID is not
371 // in the data table. Additionally, do not store revision metadata
372 // fields in the data table.
373 $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
374 $table_mapping->setFieldNames($this->dataTable, $data_fields);
376 $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
377 $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
379 $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
380 $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
381 $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
384 // Add dedicated tables.
385 $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
386 return $table_mapping->requiresDedicatedTableStorage($definition);
396 foreach ($dedicated_table_definitions as $field_name => $definition) {
397 $tables = [$table_mapping->getDedicatedDataTableName($definition)];
398 if ($revisionable && $definition->isRevisionable()) {
399 $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
401 foreach ($tables as $table_name) {
402 $table_mapping->setFieldNames($table_name, [$field_name]);
403 $table_mapping->setExtraColumns($table_name, $extra_columns);
407 // Cache the computed table mapping only if we are using our internal
408 // storage definitions.
409 if (!$storage_definitions) {
410 $this->tableMapping = $table_mapping;
414 return $table_mapping;
420 protected function doLoadMultiple(array $ids = NULL) {
421 // Attempt to load entities from the persistent cache. This will remove IDs
422 // that were loaded from $ids.
423 $entities_from_cache = $this->getFromPersistentCache($ids);
425 // Load any remaining entities from the database.
426 if ($entities_from_storage = $this->getFromStorage($ids)) {
427 $this->invokeStorageLoadHook($entities_from_storage);
428 $this->setPersistentCache($entities_from_storage);
431 return $entities_from_cache + $entities_from_storage;
435 * Gets entities from the storage.
437 * @param array|null $ids
438 * If not empty, return entities that match these IDs. Return all entities
441 * @return \Drupal\Core\Entity\ContentEntityInterface[]
442 * Array of entities from the storage.
444 protected function getFromStorage(array $ids = NULL) {
448 // Sanitize IDs. Before feeding ID array into buildQuery, check whether
449 // it is empty as this would load all entities.
450 $ids = $this->cleanIds($ids);
453 if ($ids === NULL || $ids) {
454 // Build and execute the query.
455 $query_result = $this->buildQuery($ids)->execute();
456 $records = $query_result->fetchAllAssoc($this->idKey);
458 // Map the loaded records into entity objects and according fields.
460 $entities = $this->mapFromStorageRecords($records);
468 * Maps from storage records to entity objects, and attaches fields.
470 * @param array $records
471 * Associative array of query results, keyed on the entity ID or revision
473 * @param bool $load_from_revision
474 * (optional) Flag to indicate whether revisions should be loaded or not.
478 * An array of entity objects implementing the EntityInterface.
480 protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
486 foreach ($records as $id => $record) {
488 // Skip the item delta and item value levels (if possible) but let the
489 // field assign the value as suiting. This avoids unnecessary array
490 // hierarchies and saves memory here.
491 foreach ($record as $name => $value) {
492 // Handle columns named [field_name]__[column_name] (e.g for field types
493 // that store several properties).
494 if ($field_name = strstr($name, '__', TRUE)) {
495 $property_name = substr($name, strpos($name, '__') + 2);
496 $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
499 // Handle columns named directly after the field (e.g if the field
500 // type only stores one property).
501 $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
506 // Initialize translations array.
507 $translations = array_fill_keys(array_keys($values), []);
509 // Load values from shared and dedicated tables.
510 $this->loadFromSharedTables($values, $translations, $load_from_revision);
511 $this->loadFromDedicatedTables($values, $load_from_revision);
514 foreach ($values as $id => $entity_values) {
515 $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
516 // Turn the record into an entity class.
517 $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
524 * Loads values for fields stored in the shared data tables.
526 * @param array &$values
527 * Associative array of entities values, keyed on the entity ID or the
529 * @param array &$translations
530 * List of translations, keyed on the entity ID.
531 * @param bool $load_from_revision
532 * Flag to indicate whether revisions should be loaded or not.
534 protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
535 $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
536 if ($this->dataTable) {
537 // If a revision table is available, we need all the properties of the
538 // latest revision. Otherwise we fall back to the data table.
539 $table = $this->revisionDataTable ?: $this->dataTable;
540 $alias = $this->revisionDataTable ? 'revision' : 'data';
541 $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
543 ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
544 ->orderBy($alias . '.' . $record_key);
546 $table_mapping = $this->getTableMapping();
547 if ($this->revisionDataTable) {
548 // Find revisioned fields that are not entity keys. Exclude the langcode
549 // key as the base table holds only the default language.
550 $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
551 $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
553 // Find fields that are not revisioned or entity keys. Data fields have
554 // the same value regardless of entity revision.
555 $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
556 // If there are no data fields then only revisioned fields are needed
557 // else both data fields and revisioned fields are needed to map the
559 $all_fields = $revisioned_fields;
561 $all_fields = array_merge($revisioned_fields, $data_fields);
562 $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
564 // Some fields can have more then one columns in the data table so
565 // column names are needed.
566 foreach ($data_fields as $data_field) {
567 // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
568 // returns an array keyed by property names so remove the keys
569 // before array_merge() to avoid losing data with fields having the
570 // same columns i.e. value.
571 $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
573 $query->fields('data', $column_names);
576 // Get the revision IDs.
578 foreach ($values as $entity_values) {
579 $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
581 $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
584 $all_fields = $table_mapping->getFieldNames($this->dataTable);
587 $result = $query->execute();
588 foreach ($result as $row) {
589 $id = $row[$record_key];
591 // Field values in default language are stored with
592 // LanguageInterface::LANGCODE_DEFAULT as key.
593 $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
595 $translations[$id][$langcode] = TRUE;
597 foreach ($all_fields as $field_name) {
598 $columns = $table_mapping->getColumnNames($field_name);
599 // Do not key single-column fields by property name.
600 if (count($columns) == 1) {
601 $values[$id][$field_name][$langcode] = $row[reset($columns)];
604 foreach ($columns as $property_name => $column_name) {
605 $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
616 protected function doLoadRevisionFieldItems($revision_id) {
617 @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);
619 $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
621 return !empty($revisions) ? reset($revisions) : NULL;
627 protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
630 // Sanitize IDs. Before feeding ID array into buildQuery, check whether
631 // it is empty as this would load all entity revisions.
632 $revision_ids = $this->cleanIds($revision_ids, 'revision');
634 if (!empty($revision_ids)) {
635 // Build and execute the query.
636 $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
637 $records = $query_result->fetchAllAssoc($this->revisionKey);
639 // Map the loaded records into entity objects and according fields.
641 $revisions = $this->mapFromStorageRecords($records, TRUE);
651 protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
652 $this->database->delete($this->revisionTable)
653 ->condition($this->revisionKey, $revision->getRevisionId())
656 if ($this->revisionDataTable) {
657 $this->database->delete($this->revisionDataTable)
658 ->condition($this->revisionKey, $revision->getRevisionId())
662 $this->deleteRevisionFromDedicatedTables($revision);
668 protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
669 if ($this->dataTable) {
670 // @todo We should not be using a condition to specify whether conditions
671 // apply to the default language. See
672 // https://www.drupal.org/node/1866330.
673 // Default to the original entity language if not explicitly specified
675 if (!array_key_exists($this->defaultLangcodeKey, $values)) {
676 $values[$this->defaultLangcodeKey] = 1;
678 // If the 'default_langcode' flag is explicitly not set, we do not care
679 // whether the queried values are in the original entity language or not.
680 elseif ($values[$this->defaultLangcodeKey] === NULL) {
681 unset($values[$this->defaultLangcodeKey]);
685 parent::buildPropertyQuery($entity_query, $values);
689 * Builds the query to load the entity.
691 * This has full revision support. For entities requiring special queries,
692 * the class can be extended, and the default query can be constructed by
693 * calling parent::buildQuery(). This is usually necessary when the object
694 * being loaded needs to be augmented with additional data from another
695 * table, such as loading node type into comments or vocabulary machine name
696 * into terms, however it can also support $conditions on different tables.
697 * See Drupal\comment\CommentStorage::buildQuery() for an example.
699 * @param array|null $ids
700 * An array of entity IDs, or NULL to load all entities.
701 * @param array|bool $revision_ids
702 * The IDs of the revisions to load, or FALSE if this query is asking for
703 * the default revisions. Defaults to FALSE.
705 * @return \Drupal\Core\Database\Query\Select
706 * A SelectQuery object for loading the entity.
708 protected function buildQuery($ids, $revision_ids = FALSE) {
709 $query = $this->database->select($this->entityType->getBaseTable(), 'base');
711 $query->addTag($this->entityTypeId . '_load_multiple');
714 if (!is_array($revision_ids)) {
715 @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);
717 $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
719 elseif ($this->revisionTable) {
720 $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
723 // Add fields from the {entity} table.
724 $table_mapping = $this->getTableMapping();
725 $entity_fields = $table_mapping->getAllColumns($this->baseTable);
727 if ($this->revisionTable) {
728 // Add all fields from the {entity_revision} table.
729 $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
730 $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
731 // The ID field is provided by entity, so remove it.
732 unset($entity_revision_fields[$this->idKey]);
734 // Remove all fields from the base table that are also fields by the same
735 // name in the revision table.
736 $entity_field_keys = array_flip($entity_fields);
737 foreach ($entity_revision_fields as $name) {
738 if (isset($entity_field_keys[$name])) {
739 unset($entity_fields[$entity_field_keys[$name]]);
742 $query->fields('revision', $entity_revision_fields);
744 // Compare revision ID of the base and revision table, if equal then this
745 // is the default revision.
746 $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
749 $query->fields('base', $entity_fields);
752 $query->condition("base.{$this->idKey}", $ids, 'IN');
761 public function delete(array $entities) {
763 // If no IDs or invalid IDs were passed, do nothing.
767 $transaction = $this->database->startTransaction();
769 parent::delete($entities);
771 // Ignore replica server temporarily.
774 catch (\Exception $e) {
775 $transaction->rollBack();
776 watchdog_exception($this->entityTypeId, $e);
777 throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
784 protected function doDeleteFieldItems($entities) {
785 $ids = array_keys($entities);
787 $this->database->delete($this->entityType->getBaseTable())
788 ->condition($this->idKey, $ids, 'IN')
791 if ($this->revisionTable) {
792 $this->database->delete($this->revisionTable)
793 ->condition($this->idKey, $ids, 'IN')
797 if ($this->dataTable) {
798 $this->database->delete($this->dataTable)
799 ->condition($this->idKey, $ids, 'IN')
803 if ($this->revisionDataTable) {
804 $this->database->delete($this->revisionDataTable)
805 ->condition($this->idKey, $ids, 'IN')
809 foreach ($entities as $entity) {
810 $this->deleteFromDedicatedTables($entity);
817 public function save(EntityInterface $entity) {
818 $transaction = $this->database->startTransaction();
820 $return = parent::save($entity);
822 // Ignore replica server temporarily.
826 catch (\Exception $e) {
827 $transaction->rollBack();
828 watchdog_exception($this->entityTypeId, $e);
829 throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
836 protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
837 $full_save = empty($names);
838 $update = !$full_save || !$entity->isNew();
841 $shared_table_fields = TRUE;
842 $dedicated_table_fields = TRUE;
845 $table_mapping = $this->getTableMapping();
846 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
847 $shared_table_fields = FALSE;
848 $dedicated_table_fields = [];
850 // Collect the name of fields to be written in dedicated tables and check
851 // whether shared table records need to be updated.
852 foreach ($names as $name) {
853 $storage_definition = $storage_definitions[$name];
854 if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
855 $shared_table_fields = TRUE;
857 elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
858 $dedicated_table_fields[] = $name;
863 // Update shared table records if necessary.
864 if ($shared_table_fields) {
865 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
866 // Create the storage record to be saved.
868 $default_revision = $entity->isDefaultRevision();
869 if ($default_revision) {
871 ->update($this->baseTable)
872 ->fields((array) $record)
873 ->condition($this->idKey, $record->{$this->idKey})
876 if ($this->revisionTable) {
878 $entity->{$this->revisionKey} = $this->saveRevision($entity);
881 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
882 $entity->preSaveRevision($this, $record);
884 ->update($this->revisionTable)
885 ->fields((array) $record)
886 ->condition($this->revisionKey, $record->{$this->revisionKey})
890 if ($default_revision && $this->dataTable) {
891 $this->saveToSharedTables($entity);
893 if ($this->revisionDataTable) {
894 $new_revision = $full_save && $entity->isNewRevision();
895 $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
899 $insert_id = $this->database
900 ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
901 ->fields((array) $record)
903 // Even if this is a new entity the ID key might have been set, in which
904 // case we should not override the provided ID. An ID key that is not set
905 // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
906 if (!isset($record->{$this->idKey})) {
907 $record->{$this->idKey} = $insert_id;
909 $entity->{$this->idKey} = (string) $record->{$this->idKey};
910 if ($this->revisionTable) {
911 $record->{$this->revisionKey} = $this->saveRevision($entity);
913 if ($this->dataTable) {
914 $this->saveToSharedTables($entity);
916 if ($this->revisionDataTable) {
917 $this->saveToSharedTables($entity, $this->revisionDataTable);
922 // Update dedicated table records if necessary.
923 if ($dedicated_table_fields) {
924 $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
925 $this->saveToDedicatedTables($entity, $update, $names);
932 protected function has($id, EntityInterface $entity) {
933 return !$entity->isNew();
937 * Saves fields that use the shared tables.
939 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
941 * @param string $table_name
942 * (optional) The table name to save to. Defaults to the data table.
943 * @param bool $new_revision
944 * (optional) Whether we are dealing with a new revision. By default fetches
945 * the information from the entity object.
947 protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
948 if (!isset($table_name)) {
949 $table_name = $this->dataTable;
951 if (!isset($new_revision)) {
952 $new_revision = $entity->isNewRevision();
954 $revision = $table_name != $this->dataTable;
956 if (!$revision || !$new_revision) {
957 $key = $revision ? $this->revisionKey : $this->idKey;
958 $value = $revision ? $entity->getRevisionId() : $entity->id();
959 // Delete and insert to handle removed values.
960 $this->database->delete($table_name)
961 ->condition($key, $value)
965 $query = $this->database->insert($table_name);
967 foreach ($entity->getTranslationLanguages() as $langcode => $language) {
968 $translation = $entity->getTranslation($langcode);
969 $record = $this->mapToDataStorageRecord($translation, $table_name);
970 $values = (array) $record;
972 ->fields(array_keys($values))
980 * Maps from an entity object to the storage record.
982 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
984 * @param string $table_name
985 * (optional) The table name to map records to. Defaults to the base table.
988 * The record to store.
990 protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
991 if (!isset($table_name)) {
992 $table_name = $this->baseTable;
995 $record = new \stdClass();
996 $table_mapping = $this->getTableMapping();
997 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
999 if (empty($this->getFieldStorageDefinitions()[$field_name])) {
1000 throw new EntityStorageException("Table mapping contains invalid field $field_name.");
1002 $definition = $this->getFieldStorageDefinitions()[$field_name];
1003 $columns = $table_mapping->getColumnNames($field_name);
1005 foreach ($columns as $column_name => $schema_name) {
1006 // If there is no main property and only a single column, get all
1007 // properties from the first field item and assume that they will be
1008 // stored serialized.
1009 // @todo Give field types more control over this behavior in
1010 // https://www.drupal.org/node/2232427.
1011 if (!$definition->getMainPropertyName() && count($columns) == 1) {
1012 $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
1015 $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
1017 if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
1018 $value = serialize($value);
1021 // Do not set serial fields if we do not have a value. This supports all
1022 // SQL database drivers.
1023 // @see https://www.drupal.org/node/2279395
1024 $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
1025 if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
1026 $record->$schema_name = $value;
1035 * Checks whether a field column should be treated as serial.
1037 * @param $table_name
1038 * The name of the table the field column belongs to.
1039 * @param $schema_name
1040 * The schema name of the field column.
1043 * TRUE if the column is serial, FALSE otherwise.
1045 * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1046 * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1048 protected function isColumnSerial($table_name, $schema_name) {
1051 switch ($table_name) {
1052 case $this->baseTable:
1053 $result = $schema_name == $this->idKey;
1056 case $this->revisionTable:
1057 $result = $schema_name == $this->revisionKey;
1065 * Maps from an entity object to the storage record of the field data.
1067 * @param \Drupal\Core\Entity\EntityInterface $entity
1068 * The entity object.
1069 * @param string $table_name
1070 * (optional) The table name to map records to. Defaults to the data table.
1073 * The record to store.
1075 protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1076 if (!isset($table_name)) {
1077 $table_name = $this->dataTable;
1079 $record = $this->mapToStorageRecord($entity, $table_name);
1084 * Saves an entity revision.
1086 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1087 * The entity object.
1092 protected function saveRevision(ContentEntityInterface $entity) {
1093 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1095 $entity->preSaveRevision($this, $record);
1097 if ($entity->isNewRevision()) {
1098 $insert_id = $this->database
1099 ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1100 ->fields((array) $record)
1102 // Even if this is a new revision, the revision ID key might have been
1103 // set in which case we should not override the provided revision ID.
1104 if (!isset($record->{$this->revisionKey})) {
1105 $record->{$this->revisionKey} = $insert_id;
1107 if ($entity->isDefaultRevision()) {
1108 $this->database->update($this->entityType->getBaseTable())
1109 ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1110 ->condition($this->idKey, $record->{$this->idKey})
1116 ->update($this->revisionTable)
1117 ->fields((array) $record)
1118 ->condition($this->revisionKey, $record->{$this->revisionKey})
1122 // Make sure to update the new revision key for the entity.
1123 $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1125 return $record->{$this->revisionKey};
1131 protected function getQueryServiceName() {
1132 return 'entity.query.sql';
1136 * Loads values of fields stored in dedicated tables for a group of entities.
1138 * @param array &$values
1139 * An array of values keyed by entity ID.
1140 * @param bool $load_from_revision
1141 * Flag to indicate whether revisions should be loaded or not.
1143 protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1144 if (empty($values)) {
1148 // Collect entities ids, bundles and languages.
1151 $default_langcodes = [];
1152 foreach ($values as $key => $entity_values) {
1153 $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
1154 $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
1155 if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
1156 $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
1160 // Collect impacted fields.
1161 $storage_definitions = [];
1163 $table_mapping = $this->getTableMapping();
1164 foreach ($bundles as $bundle => $v) {
1165 $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
1166 foreach ($definitions[$bundle] as $field_name => $field_definition) {
1167 $storage_definition = $field_definition->getFieldStorageDefinition();
1168 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1169 $storage_definitions[$field_name] = $storage_definition;
1175 $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
1176 foreach ($storage_definitions as $field_name => $storage_definition) {
1177 $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
1179 // Ensure that only values having valid languages are retrieved. Since we
1180 // are loading values for multiple entities, we cannot limit the query to
1181 // the available translations.
1182 $results = $this->database->select($table, 't')
1184 ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1185 ->condition('deleted', 0)
1186 ->condition('langcode', $langcodes, 'IN')
1190 foreach ($results as $row) {
1191 $bundle = $row->bundle;
1193 $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
1194 // Field values in default language are stored with
1195 // LanguageInterface::LANGCODE_DEFAULT as key.
1196 $langcode = LanguageInterface::LANGCODE_DEFAULT;
1197 if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
1198 $langcode = $row->langcode;
1201 if (!isset($values[$value_key][$field_name][$langcode])) {
1202 $values[$value_key][$field_name][$langcode] = [];
1205 // Ensure that records for non-translatable fields having invalid
1206 // languages are skipped.
1207 if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
1208 if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
1210 // For each column declared by the field, populate the item from the
1211 // prefixed database column.
1212 foreach ($storage_definition->getColumns() as $column => $attributes) {
1213 $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1214 // Unserialize the value if specified in the column schema.
1215 $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
1218 // Add the item to the field values for the entity.
1219 $values[$value_key][$field_name][$langcode][] = $item;
1227 * Saves values of fields that use dedicated tables.
1229 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1231 * @param bool $update
1232 * TRUE if the entity is being updated, FALSE if it is being inserted.
1233 * @param string[] $names
1234 * (optional) The names of the fields to be stored. Defaults to all the
1237 protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
1238 $vid = $entity->getRevisionId();
1239 $id = $entity->id();
1240 $bundle = $entity->bundle();
1241 $entity_type = $entity->getEntityTypeId();
1242 $default_langcode = $entity->getUntranslated()->language()->getId();
1243 $translation_langcodes = array_keys($entity->getTranslationLanguages());
1244 $table_mapping = $this->getTableMapping();
1250 $original = !empty($entity->original) ? $entity->original : NULL;
1252 // Determine which fields should be actually stored.
1253 $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1255 $definitions = array_intersect_key($definitions, array_flip($names));
1258 foreach ($definitions as $field_name => $field_definition) {
1259 $storage_definition = $field_definition->getFieldStorageDefinition();
1260 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1264 // When updating an existing revision, keep the existing records if the
1265 // field values did not change.
1266 if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
1270 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1271 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1273 // Delete and insert, rather than update, in case a value was added.
1275 // Only overwrite the field's base table if saving the default revision
1277 if ($entity->isDefaultRevision()) {
1278 $this->database->delete($table_name)
1279 ->condition('entity_id', $id)
1282 if ($this->entityType->isRevisionable()) {
1283 $this->database->delete($revision_name)
1284 ->condition('entity_id', $id)
1285 ->condition('revision_id', $vid)
1290 // Prepare the multi-insert query.
1292 $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
1293 foreach ($storage_definition->getColumns() as $column => $attributes) {
1294 $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
1296 $query = $this->database->insert($table_name)->fields($columns);
1297 if ($this->entityType->isRevisionable()) {
1298 $revision_query = $this->database->insert($revision_name)->fields($columns);
1301 $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1302 foreach ($langcodes as $langcode) {
1304 $items = $entity->getTranslation($langcode)->get($field_name);
1305 $items->filterEmptyItems();
1306 foreach ($items as $delta => $item) {
1307 // We now know we have something to insert.
1311 'revision_id' => $vid,
1312 'bundle' => $bundle,
1314 'langcode' => $langcode,
1316 foreach ($storage_definition->getColumns() as $column => $attributes) {
1317 $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1318 // Serialize the value if specified in the column schema.
1319 $value = $item->$column;
1320 if (!empty($attributes['serialize'])) {
1321 $value = serialize($value);
1323 $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1325 $query->values($record);
1326 if ($this->entityType->isRevisionable()) {
1327 $revision_query->values($record);
1330 if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1336 // Execute the query if we have values to insert.
1338 // Only overwrite the field's base table if saving the default revision
1340 if ($entity->isDefaultRevision()) {
1343 if ($this->entityType->isRevisionable()) {
1344 $revision_query->execute();
1351 * Deletes values of fields in dedicated tables for all revisions.
1353 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1356 protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
1357 $table_mapping = $this->getTableMapping();
1358 foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1359 $storage_definition = $field_definition->getFieldStorageDefinition();
1360 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1363 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1364 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1365 $this->database->delete($table_name)
1366 ->condition('entity_id', $entity->id())
1368 if ($this->entityType->isRevisionable()) {
1369 $this->database->delete($revision_name)
1370 ->condition('entity_id', $entity->id())
1377 * Deletes values of fields in dedicated tables for all revisions.
1379 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1380 * The entity. It must have a revision ID.
1382 protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1383 $vid = $entity->getRevisionId();
1385 $table_mapping = $this->getTableMapping();
1386 foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1387 $storage_definition = $field_definition->getFieldStorageDefinition();
1388 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1391 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1392 $this->database->delete($revision_name)
1393 ->condition('entity_id', $entity->id())
1394 ->condition('revision_id', $vid)
1403 public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1404 return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1410 public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1411 return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1417 public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1418 return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1424 public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1425 return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1431 public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1432 $this->wrapSchemaException(function () use ($entity_type) {
1433 $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1440 public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1441 // Ensure we have an updated entity type definition.
1442 $this->entityType = $entity_type;
1443 // The table layout may have changed depending on the new entity type
1445 $this->initTableLayout();
1446 // Let the schema handler adapt to possible table layout changes.
1447 $this->wrapSchemaException(function () use ($entity_type, $original) {
1448 $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
1455 public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1456 $this->wrapSchemaException(function () use ($entity_type) {
1457 $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1464 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1465 // If we are adding a field stored in a shared table we need to recompute
1466 // the table mapping.
1467 // @todo This does not belong here. Remove it once we are able to generate a
1468 // fresh table mapping in the schema handler. See
1469 // https://www.drupal.org/node/2274017.
1470 if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
1471 $this->tableMapping = NULL;
1473 $this->wrapSchemaException(function () use ($storage_definition) {
1474 $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1481 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1482 $this->wrapSchemaException(function () use ($storage_definition, $original) {
1483 $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1490 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1491 $table_mapping = $this->getTableMapping(
1492 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1495 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1496 // Mark all data associated with the field for deletion.
1497 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1498 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1499 $this->database->update($table)
1500 ->fields(['deleted' => 1])
1502 if ($this->entityType->isRevisionable()) {
1503 $this->database->update($revision_table)
1504 ->fields(['deleted' => 1])
1509 // Update the field schema.
1510 $this->wrapSchemaException(function () use ($storage_definition) {
1511 $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1516 * Wraps a database schema exception into an entity storage exception.
1518 * @param callable $callback
1519 * The callback to be executed.
1521 * @throws \Drupal\Core\Entity\EntityStorageException
1522 * When a database schema exception is thrown.
1524 protected function wrapSchemaException(callable $callback) {
1525 $message = 'Exception thrown while performing a schema update.';
1529 catch (SchemaException $e) {
1530 $message .= ' ' . $e->getMessage();
1531 throw new EntityStorageException($message, 0, $e);
1533 catch (DatabaseExceptionWrapper $e) {
1534 $message .= ' ' . $e->getMessage();
1535 throw new EntityStorageException($message, 0, $e);
1542 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
1543 $table_mapping = $this->getTableMapping();
1544 $storage_definition = $field_definition->getFieldStorageDefinition();
1545 // Mark field data as deleted.
1546 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1547 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1548 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1549 $this->database->update($table_name)
1550 ->fields(['deleted' => 1])
1551 ->condition('bundle', $field_definition->getTargetBundle())
1553 if ($this->entityType->isRevisionable()) {
1554 $this->database->update($revision_name)
1555 ->fields(['deleted' => 1])
1556 ->condition('bundle', $field_definition->getTargetBundle())
1565 public function onBundleCreate($bundle, $entity_type_id) {}
1570 public function onBundleDelete($bundle, $entity_type_id) {}
1575 protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1576 // Check whether the whole field storage definition is gone, or just some
1578 $storage_definition = $field_definition->getFieldStorageDefinition();
1579 $table_mapping = $this->getTableMapping();
1580 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1582 // Get the entities which we want to purge first.
1583 $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
1584 $or = $entity_query->orConditionGroup();
1585 foreach ($storage_definition->getColumns() as $column_name => $data) {
1586 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1590 ->fields('t', ['entity_id'])
1591 ->condition('bundle', $field_definition->getTargetBundle())
1592 ->range(0, $batch_size);
1594 // Create a map of field data table column names to field column names.
1596 foreach ($storage_definition->getColumns() as $column_name => $data) {
1597 $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1601 $items_by_entity = [];
1602 foreach ($entity_query->execute() as $row) {
1603 $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1605 ->condition('entity_id', $row['entity_id'])
1606 ->condition('deleted', 1)
1609 foreach ($item_query->execute() as $item_row) {
1610 if (!isset($entities[$item_row['revision_id']])) {
1611 // Create entity with the right revision id and entity id combination.
1612 $item_row['entity_type'] = $this->entityTypeId;
1613 // @todo: Replace this by an entity object created via an entity
1614 // factory, see https://www.drupal.org/node/1867228.
1615 $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
1618 foreach ($column_map as $db_column => $field_column) {
1619 $item[$field_column] = $item_row[$db_column];
1621 $items_by_entity[$item_row['revision_id']][] = $item;
1625 // Create field item objects and return.
1626 foreach ($items_by_entity as $revision_id => $values) {
1627 $entity_adapter = $entities[$revision_id]->getTypedData();
1628 $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
1630 return $items_by_entity;
1636 protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
1637 $storage_definition = $field_definition->getFieldStorageDefinition();
1638 $is_deleted = $storage_definition->isDeleted();
1639 $table_mapping = $this->getTableMapping();
1640 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1641 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1642 $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
1643 $this->database->delete($table_name)
1644 ->condition('revision_id', $revision_id)
1645 ->condition('deleted', 1)
1647 if ($this->entityType->isRevisionable()) {
1648 $this->database->delete($revision_name)
1649 ->condition('revision_id', $revision_id)
1650 ->condition('deleted', 1)
1658 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1659 $this->getStorageSchema()->finalizePurge($storage_definition);
1665 public function countFieldData($storage_definition, $as_bool = FALSE) {
1666 // The table mapping contains stale data during a request when a field
1667 // storage definition is added, so bypass the internal storage definitions
1668 // and fetch the table mapping using the passed in storage definition.
1669 // @todo Fix this in https://www.drupal.org/node/2705205.
1670 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
1671 $storage_definitions[$storage_definition->getName()] = $storage_definition;
1672 $table_mapping = $this->getTableMapping($storage_definitions);
1674 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1675 $is_deleted = $storage_definition->isDeleted();
1676 if ($this->entityType->isRevisionable()) {
1677 $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1680 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1682 $query = $this->database->select($table_name, 't');
1683 $or = $query->orConditionGroup();
1684 foreach ($storage_definition->getColumns() as $column_name => $data) {
1685 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1687 $query->condition($or);
1690 ->fields('t', ['entity_id'])
1694 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1695 // Ascertain the table this field is mapped too.
1696 $field_name = $storage_definition->getName();
1698 $table_name = $table_mapping->getFieldTableName($field_name);
1700 catch (SqlContentEntityStorageException $e) {
1701 // This may happen when changing field storage schema, since we are not
1702 // able to use a table mapping matching the passed storage definition.
1703 // @todo Revisit this once we are able to instantiate the table mapping
1704 // properly. See https://www.drupal.org/node/2274017.
1705 $table_name = $this->dataTable ?: $this->baseTable;
1707 $query = $this->database->select($table_name, 't');
1708 $or = $query->orConditionGroup();
1709 foreach (array_keys($storage_definition->getColumns()) as $property_name) {
1710 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
1712 $query->condition($or);
1715 ->fields('t', [$this->idKey])
1720 // @todo Find a way to count field data also for fields having custom
1721 // storage. See https://www.drupal.org/node/2337753.
1723 if (isset($query)) {
1724 // If we are performing the query just to check if the field has data
1725 // limit the number of rows.
1729 ->addExpression('1');
1732 // Otherwise count the number of rows.
1733 $query = $query->countQuery();
1735 $count = $query->execute()->fetchField();
1737 return $as_bool ? (bool) $count : (int) $count;
1741 * Determines whether the passed field has been already deleted.
1743 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1744 * The field storage definition.
1747 * Whether the field has been already deleted.
1749 * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
1750 * \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
1752 * @see https://www.drupal.org/node/2907785
1754 protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1755 return $storage_definition->isDeleted();