3 namespace Drupal\Core\Entity\Sql;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7 use Drupal\Core\Database\Connection;
8 use Drupal\Core\Database\Database;
9 use Drupal\Core\Database\DatabaseExceptionWrapper;
10 use Drupal\Core\Database\SchemaException;
11 use Drupal\Core\Entity\ContentEntityInterface;
12 use Drupal\Core\Entity\ContentEntityStorageBase;
13 use Drupal\Core\Entity\ContentEntityTypeInterface;
14 use Drupal\Core\Entity\EntityBundleListenerInterface;
15 use Drupal\Core\Entity\EntityInterface;
16 use Drupal\Core\Entity\EntityManagerInterface;
17 use Drupal\Core\Entity\EntityStorageException;
18 use Drupal\Core\Entity\EntityTypeInterface;
19 use Drupal\Core\Entity\Query\QueryInterface;
20 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
21 use Drupal\Core\Field\FieldDefinitionInterface;
22 use Drupal\Core\Field\FieldStorageDefinitionInterface;
23 use Drupal\Core\Language\LanguageInterface;
24 use Drupal\Core\Language\LanguageManagerInterface;
25 use Symfony\Component\DependencyInjection\ContainerInterface;
28 * A content entity database storage implementation.
30 * This class can be used as-is by most content entity types. Entity types
31 * requiring special handling can extend the class.
33 * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
34 * internally in order to automatically generate the database schema based on
35 * the defined base fields. Entity types can override the schema handler to
36 * customize the generated schema; e.g., to add additional indexes.
40 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
43 * The mapping of field columns to SQL tables.
45 * @var \Drupal\Core\Entity\Sql\TableMappingInterface
47 protected $tableMapping;
50 * Name of entity's revision database table field, if it supports revisions.
52 * Has the value FALSE if this entity does not use revisions.
56 protected $revisionKey = FALSE;
59 * The entity langcode key.
63 protected $langcodeKey = FALSE;
66 * The default language entity key.
70 protected $defaultLangcodeKey = FALSE;
73 * The base table of the entity.
80 * The table that stores revisions, if the entity supports revisions.
84 protected $revisionTable;
87 * The table that stores properties, if the entity has multilingual support.
94 * The table that stores revision field data if the entity supports revisions.
98 protected $revisionDataTable;
101 * Active database connection.
103 * @var \Drupal\Core\Database\Connection
108 * The entity type's storage schema object.
110 * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
112 protected $storageSchema;
115 * The language manager.
117 * @var \Drupal\Core\Language\LanguageManagerInterface
119 protected $languageManager;
122 * Whether this storage should use the temporary table mapping.
126 protected $temporary = FALSE;
131 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
134 $container->get('database'),
135 $container->get('entity.manager'),
136 $container->get('cache.entity'),
137 $container->get('language_manager'),
138 $container->get('entity.memory_cache')
143 * Gets the base field definitions for a content entity type.
145 * @return \Drupal\Core\Field\FieldDefinitionInterface[]
146 * The array of base field definitions for the entity type, keyed by field
149 public function getFieldStorageDefinitions() {
150 return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
154 * Constructs a SqlContentEntityStorage object.
156 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
157 * The entity type definition.
158 * @param \Drupal\Core\Database\Connection $database
159 * The database connection to be used.
160 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
161 * The entity manager.
162 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
163 * The cache backend to be used.
164 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
165 * The language manager.
166 * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
167 * The memory cache backend to be used.
169 public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
170 parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
171 $this->database = $database;
172 $this->languageManager = $language_manager;
173 $this->initTableLayout();
177 * Initializes table name variables.
179 protected function initTableLayout() {
180 // Reset table field values to ensure changes in the entity type definition
181 // are correctly reflected in the table layout.
182 $this->tableMapping = NULL;
183 $this->revisionKey = NULL;
184 $this->revisionTable = NULL;
185 $this->dataTable = NULL;
186 $this->revisionDataTable = NULL;
188 $table_mapping = $this->getTableMapping();
189 $this->baseTable = $table_mapping->getBaseTable();
190 $revisionable = $this->entityType->isRevisionable();
192 $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
193 $this->revisionTable = $table_mapping->getRevisionTable();
195 $translatable = $this->entityType->isTranslatable();
197 $this->dataTable = $table_mapping->getDataTable();
198 $this->langcodeKey = $this->entityType->getKey('langcode');
199 $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
201 if ($revisionable && $translatable) {
202 $this->revisionDataTable = $table_mapping->getRevisionDataTable();
207 * Gets the base table name.
212 public function getBaseTable() {
213 return $this->baseTable;
217 * Gets the revision table name.
219 * @return string|false
220 * The table name or FALSE if it is not available.
222 public function getRevisionTable() {
223 return $this->revisionTable;
227 * Gets the data table name.
229 * @return string|false
230 * The table name or FALSE if it is not available.
232 public function getDataTable() {
233 return $this->dataTable;
237 * Gets the revision data table name.
239 * @return string|false
240 * The table name or FALSE if it is not available.
242 public function getRevisionDataTable() {
243 return $this->revisionDataTable;
247 * Gets the entity type's storage schema object.
249 * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
252 protected function getStorageSchema() {
253 if (!isset($this->storageSchema)) {
254 $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
255 $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
257 return $this->storageSchema;
261 * Updates the wrapped entity type definition.
263 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
264 * The update entity type.
266 * @internal Only to be used internally by Entity API. Expected to be
267 * removed by https://www.drupal.org/node/2274017.
269 public function setEntityType(EntityTypeInterface $entity_type) {
270 if ($this->entityType->id() == $entity_type->id()) {
271 $this->entityType = $entity_type;
272 $this->initTableLayout();
275 throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
280 * Sets the wrapped table mapping definition.
282 * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
285 * @internal Only to be used internally by Entity API. Expected to be removed
286 * by https://www.drupal.org/node/2554235.
288 public function setTableMapping(TableMappingInterface $table_mapping) {
289 $this->tableMapping = $table_mapping;
291 $this->baseTable = $table_mapping->getBaseTable();
292 $this->revisionTable = $table_mapping->getRevisionTable();
293 $this->dataTable = $table_mapping->getDataTable();
294 $this->revisionDataTable = $table_mapping->getRevisionDataTable();
298 * Changes the temporary state of the storage.
300 * @param bool $temporary
301 * Whether to use a temporary table mapping or not.
303 * @internal Only to be used internally by Entity API.
305 public function setTemporary($temporary) {
306 $this->temporary = $temporary;
312 public function getTableMapping(array $storage_definitions = NULL) {
313 // If a new set of field storage definitions is passed, for instance when
314 // comparing old and new storage schema, we compute the table mapping
316 if ($storage_definitions) {
317 return $this->getCustomTableMapping($this->entityType, $storage_definitions);
320 // If we are using our internal storage definitions, which is our main use
321 // case, we can statically cache the computed table mapping.
322 if (!isset($this->tableMapping)) {
323 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
325 $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
328 return $this->tableMapping;
332 * Gets a table mapping for the specified entity type and storage definitions.
334 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
335 * An entity type definition.
336 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
337 * An array of field storage definitions to be used to compute the table
340 * @return \Drupal\Core\Entity\Sql\TableMappingInterface
341 * A table mapping object for the entity's tables.
345 public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
346 $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
347 return $table_mapping_class::create($entity_type, $storage_definitions);
353 protected function doLoadMultiple(array $ids = NULL) {
354 // Attempt to load entities from the persistent cache. This will remove IDs
355 // that were loaded from $ids.
356 $entities_from_cache = $this->getFromPersistentCache($ids);
358 // Load any remaining entities from the database.
359 if ($entities_from_storage = $this->getFromStorage($ids)) {
360 $this->invokeStorageLoadHook($entities_from_storage);
361 $this->setPersistentCache($entities_from_storage);
364 return $entities_from_cache + $entities_from_storage;
368 * Gets entities from the storage.
370 * @param array|null $ids
371 * If not empty, return entities that match these IDs. Return all entities
374 * @return \Drupal\Core\Entity\ContentEntityInterface[]
375 * Array of entities from the storage.
377 protected function getFromStorage(array $ids = NULL) {
381 // Sanitize IDs. Before feeding ID array into buildQuery, check whether
382 // it is empty as this would load all entities.
383 $ids = $this->cleanIds($ids);
386 if ($ids === NULL || $ids) {
387 // Build and execute the query.
388 $query_result = $this->buildQuery($ids)->execute();
389 $records = $query_result->fetchAllAssoc($this->idKey);
391 // Map the loaded records into entity objects and according fields.
393 $entities = $this->mapFromStorageRecords($records);
401 * Maps from storage records to entity objects, and attaches fields.
403 * @param array $records
404 * Associative array of query results, keyed on the entity ID or revision
406 * @param bool $load_from_revision
407 * (optional) Flag to indicate whether revisions should be loaded or not.
411 * An array of entity objects implementing the EntityInterface.
413 protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
418 // Get the names of the fields that are stored in the base table and, if
419 // applicable, the revision table. Other entity data will be loaded in
420 // loadFromSharedTables() and loadFromDedicatedTables().
421 $field_names = $this->tableMapping->getFieldNames($this->baseTable);
422 if ($this->revisionTable) {
423 $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable)));
427 foreach ($records as $id => $record) {
429 // Skip the item delta and item value levels (if possible) but let the
430 // field assign the value as suiting. This avoids unnecessary array
431 // hierarchies and saves memory here.
432 foreach ($field_names as $field_name) {
433 $field_columns = $this->tableMapping->getColumnNames($field_name);
434 // Handle field types that store several properties.
435 if (count($field_columns) > 1) {
436 foreach ($field_columns as $property_name => $column_name) {
437 if (property_exists($record, $column_name)) {
438 $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $record->{$column_name};
439 unset($record->{$column_name});
443 // Handle field types that store only one property.
445 $column_name = reset($field_columns);
446 if (property_exists($record, $column_name)) {
447 $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $record->{$column_name};
448 unset($record->{$column_name});
453 // Handle additional record entries that are not provided by an entity
454 // field, such as 'isDefaultRevision'.
455 foreach ($record as $name => $value) {
456 $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
460 // Initialize translations array.
461 $translations = array_fill_keys(array_keys($values), []);
463 // Load values from shared and dedicated tables.
464 $this->loadFromSharedTables($values, $translations, $load_from_revision);
465 $this->loadFromDedicatedTables($values, $load_from_revision);
468 foreach ($values as $id => $entity_values) {
469 $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
470 // Turn the record into an entity class.
471 $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
478 * Loads values for fields stored in the shared data tables.
480 * @param array &$values
481 * Associative array of entities values, keyed on the entity ID or the
483 * @param array &$translations
484 * List of translations, keyed on the entity ID.
485 * @param bool $load_from_revision
486 * Flag to indicate whether revisions should be loaded or not.
488 protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
489 $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
490 if ($this->dataTable) {
491 // If a revision table is available, we need all the properties of the
492 // latest revision. Otherwise we fall back to the data table.
493 $table = $this->revisionDataTable ?: $this->dataTable;
494 $alias = $this->revisionDataTable ? 'revision' : 'data';
495 $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
497 ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
498 ->orderBy($alias . '.' . $record_key);
500 $table_mapping = $this->getTableMapping();
501 if ($this->revisionDataTable) {
502 // Find revisioned fields that are not entity keys. Exclude the langcode
503 // key as the base table holds only the default language.
504 $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
505 $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
507 // Find fields that are not revisioned or entity keys. Data fields have
508 // the same value regardless of entity revision.
509 $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
510 // If there are no data fields then only revisioned fields are needed
511 // else both data fields and revisioned fields are needed to map the
513 $all_fields = $revisioned_fields;
515 $all_fields = array_merge($revisioned_fields, $data_fields);
516 $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
518 // Some fields can have more then one columns in the data table so
519 // column names are needed.
520 foreach ($data_fields as $data_field) {
521 // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
522 // returns an array keyed by property names so remove the keys
523 // before array_merge() to avoid losing data with fields having the
524 // same columns i.e. value.
525 $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
527 $query->fields('data', $column_names);
530 // Get the revision IDs.
532 foreach ($values as $entity_values) {
533 $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
535 $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
538 $all_fields = $table_mapping->getFieldNames($this->dataTable);
541 $result = $query->execute();
542 foreach ($result as $row) {
543 $id = $row[$record_key];
545 // Field values in default language are stored with
546 // LanguageInterface::LANGCODE_DEFAULT as key.
547 $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
549 $translations[$id][$langcode] = TRUE;
551 foreach ($all_fields as $field_name) {
552 $columns = $table_mapping->getColumnNames($field_name);
553 // Do not key single-column fields by property name.
554 if (count($columns) == 1) {
555 $values[$id][$field_name][$langcode] = $row[reset($columns)];
558 foreach ($columns as $property_name => $column_name) {
559 $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
570 protected function doLoadRevisionFieldItems($revision_id) {
571 @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);
573 $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
575 return !empty($revisions) ? reset($revisions) : NULL;
581 protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
584 // Sanitize IDs. Before feeding ID array into buildQuery, check whether
585 // it is empty as this would load all entity revisions.
586 $revision_ids = $this->cleanIds($revision_ids, 'revision');
588 if (!empty($revision_ids)) {
589 // Build and execute the query.
590 $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
591 $records = $query_result->fetchAllAssoc($this->revisionKey);
593 // Map the loaded records into entity objects and according fields.
595 $revisions = $this->mapFromStorageRecords($records, TRUE);
605 protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
606 $this->database->delete($this->revisionTable)
607 ->condition($this->revisionKey, $revision->getRevisionId())
610 if ($this->revisionDataTable) {
611 $this->database->delete($this->revisionDataTable)
612 ->condition($this->revisionKey, $revision->getRevisionId())
616 $this->deleteRevisionFromDedicatedTables($revision);
622 protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
623 if ($this->dataTable) {
624 // @todo We should not be using a condition to specify whether conditions
625 // apply to the default language. See
626 // https://www.drupal.org/node/1866330.
627 // Default to the original entity language if not explicitly specified
629 if (!array_key_exists($this->defaultLangcodeKey, $values)) {
630 $values[$this->defaultLangcodeKey] = 1;
632 // If the 'default_langcode' flag is explicitly not set, we do not care
633 // whether the queried values are in the original entity language or not.
634 elseif ($values[$this->defaultLangcodeKey] === NULL) {
635 unset($values[$this->defaultLangcodeKey]);
639 parent::buildPropertyQuery($entity_query, $values);
643 * Builds the query to load the entity.
645 * This has full revision support. For entities requiring special queries,
646 * the class can be extended, and the default query can be constructed by
647 * calling parent::buildQuery(). This is usually necessary when the object
648 * being loaded needs to be augmented with additional data from another
649 * table, such as loading node type into comments or vocabulary machine name
650 * into terms, however it can also support $conditions on different tables.
651 * See Drupal\comment\CommentStorage::buildQuery() for an example.
653 * @param array|null $ids
654 * An array of entity IDs, or NULL to load all entities.
655 * @param array|bool $revision_ids
656 * The IDs of the revisions to load, or FALSE if this query is asking for
657 * the default revisions. Defaults to FALSE.
659 * @return \Drupal\Core\Database\Query\Select
660 * A SelectQuery object for loading the entity.
662 protected function buildQuery($ids, $revision_ids = FALSE) {
663 $query = $this->database->select($this->baseTable, 'base');
665 $query->addTag($this->entityTypeId . '_load_multiple');
668 if (!is_array($revision_ids)) {
669 @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);
671 $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
673 elseif ($this->revisionTable) {
674 $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
677 // Add fields from the {entity} table.
678 $table_mapping = $this->getTableMapping();
679 $entity_fields = $table_mapping->getAllColumns($this->baseTable);
681 if ($this->revisionTable) {
682 // Add all fields from the {entity_revision} table.
683 $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
684 $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
685 // The ID field is provided by entity, so remove it.
686 unset($entity_revision_fields[$this->idKey]);
688 // Remove all fields from the base table that are also fields by the same
689 // name in the revision table.
690 $entity_field_keys = array_flip($entity_fields);
691 foreach ($entity_revision_fields as $name) {
692 if (isset($entity_field_keys[$name])) {
693 unset($entity_fields[$entity_field_keys[$name]]);
696 $query->fields('revision', $entity_revision_fields);
698 // Compare revision ID of the base and revision table, if equal then this
699 // is the default revision.
700 $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
703 $query->fields('base', $entity_fields);
706 $query->condition("base.{$this->idKey}", $ids, 'IN');
715 public function delete(array $entities) {
717 // If no IDs or invalid IDs were passed, do nothing.
721 $transaction = $this->database->startTransaction();
723 parent::delete($entities);
725 // Ignore replica server temporarily.
728 catch (\Exception $e) {
729 $transaction->rollBack();
730 watchdog_exception($this->entityTypeId, $e);
731 throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
738 protected function doDeleteFieldItems($entities) {
739 $ids = array_keys($entities);
741 $this->database->delete($this->baseTable)
742 ->condition($this->idKey, $ids, 'IN')
745 if ($this->revisionTable) {
746 $this->database->delete($this->revisionTable)
747 ->condition($this->idKey, $ids, 'IN')
751 if ($this->dataTable) {
752 $this->database->delete($this->dataTable)
753 ->condition($this->idKey, $ids, 'IN')
757 if ($this->revisionDataTable) {
758 $this->database->delete($this->revisionDataTable)
759 ->condition($this->idKey, $ids, 'IN')
763 foreach ($entities as $entity) {
764 $this->deleteFromDedicatedTables($entity);
771 public function save(EntityInterface $entity) {
772 $transaction = $this->database->startTransaction();
774 $return = parent::save($entity);
776 // Ignore replica server temporarily.
780 catch (\Exception $e) {
781 $transaction->rollBack();
782 watchdog_exception($this->entityTypeId, $e);
783 throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
790 protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
791 $full_save = empty($names);
792 $update = !$full_save || !$entity->isNew();
795 $shared_table_fields = TRUE;
796 $dedicated_table_fields = TRUE;
799 $table_mapping = $this->getTableMapping();
800 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
801 $shared_table_fields = FALSE;
802 $dedicated_table_fields = [];
804 // Collect the name of fields to be written in dedicated tables and check
805 // whether shared table records need to be updated.
806 foreach ($names as $name) {
807 $storage_definition = $storage_definitions[$name];
808 if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
809 $shared_table_fields = TRUE;
811 elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
812 $dedicated_table_fields[] = $name;
817 // Update shared table records if necessary.
818 if ($shared_table_fields) {
819 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
820 // Create the storage record to be saved.
822 $default_revision = $entity->isDefaultRevision();
823 if ($default_revision) {
824 // Remove the ID from the record to enable updates on SQL variants
825 // that prevent updating serial columns, for example, mssql.
826 unset($record->{$this->idKey});
828 ->update($this->baseTable)
829 ->fields((array) $record)
830 ->condition($this->idKey, $entity->get($this->idKey)->value)
833 if ($this->revisionTable) {
835 $entity->{$this->revisionKey} = $this->saveRevision($entity);
838 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
839 // Remove the revision ID from the record to enable updates on SQL
840 // variants that prevent updating serial columns, for example,
842 unset($record->{$this->revisionKey});
843 $entity->preSaveRevision($this, $record);
845 ->update($this->revisionTable)
846 ->fields((array) $record)
847 ->condition($this->revisionKey, $entity->getRevisionId())
851 if ($default_revision && $this->dataTable) {
852 $this->saveToSharedTables($entity);
854 if ($this->revisionDataTable) {
855 $new_revision = $full_save && $entity->isNewRevision();
856 $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
860 $insert_id = $this->database
861 ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
862 ->fields((array) $record)
864 // Even if this is a new entity the ID key might have been set, in which
865 // case we should not override the provided ID. An ID key that is not set
866 // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
867 if (!isset($record->{$this->idKey})) {
868 $record->{$this->idKey} = $insert_id;
870 $entity->{$this->idKey} = (string) $record->{$this->idKey};
871 if ($this->revisionTable) {
872 $record->{$this->revisionKey} = $this->saveRevision($entity);
874 if ($this->dataTable) {
875 $this->saveToSharedTables($entity);
877 if ($this->revisionDataTable) {
878 $this->saveToSharedTables($entity, $this->revisionDataTable);
883 // Update dedicated table records if necessary.
884 if ($dedicated_table_fields) {
885 $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
886 $this->saveToDedicatedTables($entity, $update, $names);
893 protected function has($id, EntityInterface $entity) {
894 return !$entity->isNew();
898 * Saves fields that use the shared tables.
900 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
902 * @param string $table_name
903 * (optional) The table name to save to. Defaults to the data table.
904 * @param bool $new_revision
905 * (optional) Whether we are dealing with a new revision. By default fetches
906 * the information from the entity object.
908 protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
909 if (!isset($table_name)) {
910 $table_name = $this->dataTable;
912 if (!isset($new_revision)) {
913 $new_revision = $entity->isNewRevision();
915 $revision = $table_name != $this->dataTable;
917 if (!$revision || !$new_revision) {
918 $key = $revision ? $this->revisionKey : $this->idKey;
919 $value = $revision ? $entity->getRevisionId() : $entity->id();
920 // Delete and insert to handle removed values.
921 $this->database->delete($table_name)
922 ->condition($key, $value)
926 $query = $this->database->insert($table_name);
928 foreach ($entity->getTranslationLanguages() as $langcode => $language) {
929 $translation = $entity->getTranslation($langcode);
930 $record = $this->mapToDataStorageRecord($translation, $table_name);
931 $values = (array) $record;
933 ->fields(array_keys($values))
941 * Maps from an entity object to the storage record.
943 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
945 * @param string $table_name
946 * (optional) The table name to map records to. Defaults to the base table.
949 * The record to store.
951 protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
952 if (!isset($table_name)) {
953 $table_name = $this->baseTable;
956 $record = new \stdClass();
957 $table_mapping = $this->getTableMapping();
958 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
960 if (empty($this->getFieldStorageDefinitions()[$field_name])) {
961 throw new EntityStorageException("Table mapping contains invalid field $field_name.");
963 $definition = $this->getFieldStorageDefinitions()[$field_name];
964 $columns = $table_mapping->getColumnNames($field_name);
966 foreach ($columns as $column_name => $schema_name) {
967 // If there is no main property and only a single column, get all
968 // properties from the first field item and assume that they will be
969 // stored serialized.
970 // @todo Give field types more control over this behavior in
971 // https://www.drupal.org/node/2232427.
972 if (!$definition->getMainPropertyName() && count($columns) == 1) {
973 $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
976 $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
978 if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
979 $value = serialize($value);
982 // Do not set serial fields if we do not have a value. This supports all
983 // SQL database drivers.
984 // @see https://www.drupal.org/node/2279395
985 $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
986 if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
987 $record->$schema_name = $value;
996 * Checks whether a field column should be treated as serial.
999 * The name of the table the field column belongs to.
1000 * @param $schema_name
1001 * The schema name of the field column.
1004 * TRUE if the column is serial, FALSE otherwise.
1006 * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1007 * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1009 protected function isColumnSerial($table_name, $schema_name) {
1012 switch ($table_name) {
1013 case $this->baseTable:
1014 $result = $schema_name == $this->idKey;
1017 case $this->revisionTable:
1018 $result = $schema_name == $this->revisionKey;
1026 * Maps from an entity object to the storage record of the field data.
1028 * @param \Drupal\Core\Entity\EntityInterface $entity
1029 * The entity object.
1030 * @param string $table_name
1031 * (optional) The table name to map records to. Defaults to the data table.
1034 * The record to store.
1036 protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1037 if (!isset($table_name)) {
1038 $table_name = $this->dataTable;
1040 $record = $this->mapToStorageRecord($entity, $table_name);
1045 * Saves an entity revision.
1047 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1048 * The entity object.
1053 protected function saveRevision(ContentEntityInterface $entity) {
1054 $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1056 $entity->preSaveRevision($this, $record);
1058 if ($entity->isNewRevision()) {
1059 $insert_id = $this->database
1060 ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1061 ->fields((array) $record)
1063 // Even if this is a new revision, the revision ID key might have been
1064 // set in which case we should not override the provided revision ID.
1065 if (!isset($record->{$this->revisionKey})) {
1066 $record->{$this->revisionKey} = $insert_id;
1068 if ($entity->isDefaultRevision()) {
1069 $this->database->update($this->baseTable)
1070 ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1071 ->condition($this->idKey, $record->{$this->idKey})
1074 // Make sure to update the new revision key for the entity.
1075 $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1078 // Remove the revision ID from the record to enable updates on SQL
1079 // variants that prevent updating serial columns, for example,
1081 unset($record->{$this->revisionKey});
1083 ->update($this->revisionTable)
1084 ->fields((array) $record)
1085 ->condition($this->revisionKey, $entity->getRevisionId())
1088 return $entity->getRevisionId();
1094 protected function getQueryServiceName() {
1095 return 'entity.query.sql';
1099 * Loads values of fields stored in dedicated tables for a group of entities.
1101 * @param array &$values
1102 * An array of values keyed by entity ID.
1103 * @param bool $load_from_revision
1104 * Flag to indicate whether revisions should be loaded or not.
1106 protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1107 if (empty($values)) {
1111 // Collect entities ids, bundles and languages.
1114 $default_langcodes = [];
1115 foreach ($values as $key => $entity_values) {
1116 $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
1117 $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
1118 if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
1119 $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
1123 // Collect impacted fields.
1124 $storage_definitions = [];
1126 $table_mapping = $this->getTableMapping();
1127 foreach ($bundles as $bundle => $v) {
1128 $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
1129 foreach ($definitions[$bundle] as $field_name => $field_definition) {
1130 $storage_definition = $field_definition->getFieldStorageDefinition();
1131 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1132 $storage_definitions[$field_name] = $storage_definition;
1138 $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
1139 foreach ($storage_definitions as $field_name => $storage_definition) {
1140 $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
1142 // Ensure that only values having valid languages are retrieved. Since we
1143 // are loading values for multiple entities, we cannot limit the query to
1144 // the available translations.
1145 $results = $this->database->select($table, 't')
1147 ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1148 ->condition('deleted', 0)
1149 ->condition('langcode', $langcodes, 'IN')
1153 foreach ($results as $row) {
1154 $bundle = $row->bundle;
1156 $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
1157 // Field values in default language are stored with
1158 // LanguageInterface::LANGCODE_DEFAULT as key.
1159 $langcode = LanguageInterface::LANGCODE_DEFAULT;
1160 if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
1161 $langcode = $row->langcode;
1164 if (!isset($values[$value_key][$field_name][$langcode])) {
1165 $values[$value_key][$field_name][$langcode] = [];
1168 // Ensure that records for non-translatable fields having invalid
1169 // languages are skipped.
1170 if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
1171 if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
1173 // For each column declared by the field, populate the item from the
1174 // prefixed database column.
1175 foreach ($storage_definition->getColumns() as $column => $attributes) {
1176 $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1177 // Unserialize the value if specified in the column schema.
1178 $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
1181 // Add the item to the field values for the entity.
1182 $values[$value_key][$field_name][$langcode][] = $item;
1190 * Saves values of fields that use dedicated tables.
1192 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1194 * @param bool $update
1195 * TRUE if the entity is being updated, FALSE if it is being inserted.
1196 * @param string[] $names
1197 * (optional) The names of the fields to be stored. Defaults to all the
1200 protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
1201 $vid = $entity->getRevisionId();
1202 $id = $entity->id();
1203 $bundle = $entity->bundle();
1204 $entity_type = $entity->getEntityTypeId();
1205 $default_langcode = $entity->getUntranslated()->language()->getId();
1206 $translation_langcodes = array_keys($entity->getTranslationLanguages());
1207 $table_mapping = $this->getTableMapping();
1213 $original = !empty($entity->original) ? $entity->original : NULL;
1215 // Determine which fields should be actually stored.
1216 $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1218 $definitions = array_intersect_key($definitions, array_flip($names));
1221 foreach ($definitions as $field_name => $field_definition) {
1222 $storage_definition = $field_definition->getFieldStorageDefinition();
1223 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1227 // When updating an existing revision, keep the existing records if the
1228 // field values did not change.
1229 if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
1233 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1234 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1236 // Delete and insert, rather than update, in case a value was added.
1238 // Only overwrite the field's base table if saving the default revision
1240 if ($entity->isDefaultRevision()) {
1241 $this->database->delete($table_name)
1242 ->condition('entity_id', $id)
1245 if ($this->entityType->isRevisionable()) {
1246 $this->database->delete($revision_name)
1247 ->condition('entity_id', $id)
1248 ->condition('revision_id', $vid)
1253 // Prepare the multi-insert query.
1255 $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
1256 foreach ($storage_definition->getColumns() as $column => $attributes) {
1257 $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
1259 $query = $this->database->insert($table_name)->fields($columns);
1260 if ($this->entityType->isRevisionable()) {
1261 $revision_query = $this->database->insert($revision_name)->fields($columns);
1264 $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1265 foreach ($langcodes as $langcode) {
1267 $items = $entity->getTranslation($langcode)->get($field_name);
1268 $items->filterEmptyItems();
1269 foreach ($items as $delta => $item) {
1270 // We now know we have something to insert.
1274 'revision_id' => $vid,
1275 'bundle' => $bundle,
1277 'langcode' => $langcode,
1279 foreach ($storage_definition->getColumns() as $column => $attributes) {
1280 $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1281 // Serialize the value if specified in the column schema.
1282 $value = $item->$column;
1283 if (!empty($attributes['serialize'])) {
1284 $value = serialize($value);
1286 $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1288 $query->values($record);
1289 if ($this->entityType->isRevisionable()) {
1290 $revision_query->values($record);
1293 if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1299 // Execute the query if we have values to insert.
1301 // Only overwrite the field's base table if saving the default revision
1303 if ($entity->isDefaultRevision()) {
1306 if ($this->entityType->isRevisionable()) {
1307 $revision_query->execute();
1314 * Deletes values of fields in dedicated tables for all revisions.
1316 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1319 protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
1320 $table_mapping = $this->getTableMapping();
1321 foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1322 $storage_definition = $field_definition->getFieldStorageDefinition();
1323 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1326 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1327 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1328 $this->database->delete($table_name)
1329 ->condition('entity_id', $entity->id())
1331 if ($this->entityType->isRevisionable()) {
1332 $this->database->delete($revision_name)
1333 ->condition('entity_id', $entity->id())
1340 * Deletes values of fields in dedicated tables for all revisions.
1342 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1343 * The entity. It must have a revision ID.
1345 protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1346 $vid = $entity->getRevisionId();
1348 $table_mapping = $this->getTableMapping();
1349 foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1350 $storage_definition = $field_definition->getFieldStorageDefinition();
1351 if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1354 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1355 $this->database->delete($revision_name)
1356 ->condition('entity_id', $entity->id())
1357 ->condition('revision_id', $vid)
1366 public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1367 return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1373 public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1374 return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1380 public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1381 return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1387 public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1388 return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1394 public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1395 $this->wrapSchemaException(function () use ($entity_type) {
1396 $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1403 public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1404 // Ensure we have an updated entity type definition.
1405 $this->entityType = $entity_type;
1406 // The table layout may have changed depending on the new entity type
1408 $this->initTableLayout();
1409 // Let the schema handler adapt to possible table layout changes.
1410 $this->wrapSchemaException(function () use ($entity_type, $original) {
1411 $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
1418 public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1419 $this->wrapSchemaException(function () use ($entity_type) {
1420 $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1427 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1428 $this->wrapSchemaException(function () use ($storage_definition) {
1429 $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1436 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1437 $this->wrapSchemaException(function () use ($storage_definition, $original) {
1438 $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1445 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1446 $table_mapping = $this->getTableMapping(
1447 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1450 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1451 // Mark all data associated with the field for deletion.
1452 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1453 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1454 $this->database->update($table)
1455 ->fields(['deleted' => 1])
1457 if ($this->entityType->isRevisionable()) {
1458 $this->database->update($revision_table)
1459 ->fields(['deleted' => 1])
1464 // Update the field schema.
1465 $this->wrapSchemaException(function () use ($storage_definition) {
1466 $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1471 * Wraps a database schema exception into an entity storage exception.
1473 * @param callable $callback
1474 * The callback to be executed.
1476 * @throws \Drupal\Core\Entity\EntityStorageException
1477 * When a database schema exception is thrown.
1479 protected function wrapSchemaException(callable $callback) {
1480 $message = 'Exception thrown while performing a schema update.';
1484 catch (SchemaException $e) {
1485 $message .= ' ' . $e->getMessage();
1486 throw new EntityStorageException($message, 0, $e);
1488 catch (DatabaseExceptionWrapper $e) {
1489 $message .= ' ' . $e->getMessage();
1490 throw new EntityStorageException($message, 0, $e);
1497 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
1498 $table_mapping = $this->getTableMapping();
1499 $storage_definition = $field_definition->getFieldStorageDefinition();
1500 // Mark field data as deleted.
1501 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1502 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1503 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1504 $this->database->update($table_name)
1505 ->fields(['deleted' => 1])
1506 ->condition('bundle', $field_definition->getTargetBundle())
1508 if ($this->entityType->isRevisionable()) {
1509 $this->database->update($revision_name)
1510 ->fields(['deleted' => 1])
1511 ->condition('bundle', $field_definition->getTargetBundle())
1520 public function onBundleCreate($bundle, $entity_type_id) {}
1525 public function onBundleDelete($bundle, $entity_type_id) {}
1530 protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1531 // Check whether the whole field storage definition is gone, or just some
1533 $storage_definition = $field_definition->getFieldStorageDefinition();
1534 $table_mapping = $this->getTableMapping();
1535 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1537 // Get the entities which we want to purge first.
1538 $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
1539 $or = $entity_query->orConditionGroup();
1540 foreach ($storage_definition->getColumns() as $column_name => $data) {
1541 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1545 ->fields('t', ['entity_id'])
1546 ->condition('bundle', $field_definition->getTargetBundle())
1547 ->range(0, $batch_size);
1549 // Create a map of field data table column names to field column names.
1551 foreach ($storage_definition->getColumns() as $column_name => $data) {
1552 $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1556 $items_by_entity = [];
1557 foreach ($entity_query->execute() as $row) {
1558 $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1560 ->condition('entity_id', $row['entity_id'])
1561 ->condition('deleted', 1)
1564 foreach ($item_query->execute() as $item_row) {
1565 if (!isset($entities[$item_row['revision_id']])) {
1566 // Create entity with the right revision id and entity id combination.
1567 $item_row['entity_type'] = $this->entityTypeId;
1568 // @todo: Replace this by an entity object created via an entity
1569 // factory, see https://www.drupal.org/node/1867228.
1570 $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
1573 foreach ($column_map as $db_column => $field_column) {
1574 $item[$field_column] = $item_row[$db_column];
1576 $items_by_entity[$item_row['revision_id']][] = $item;
1580 // Create field item objects and return.
1581 foreach ($items_by_entity as $revision_id => $values) {
1582 $entity_adapter = $entities[$revision_id]->getTypedData();
1583 $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
1585 return $items_by_entity;
1591 protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
1592 $storage_definition = $field_definition->getFieldStorageDefinition();
1593 $is_deleted = $storage_definition->isDeleted();
1594 $table_mapping = $this->getTableMapping();
1595 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1596 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1597 $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
1598 $this->database->delete($table_name)
1599 ->condition('revision_id', $revision_id)
1600 ->condition('deleted', 1)
1602 if ($this->entityType->isRevisionable()) {
1603 $this->database->delete($revision_name)
1604 ->condition('revision_id', $revision_id)
1605 ->condition('deleted', 1)
1613 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1614 $this->getStorageSchema()->finalizePurge($storage_definition);
1620 public function countFieldData($storage_definition, $as_bool = FALSE) {
1621 // The table mapping contains stale data during a request when a field
1622 // storage definition is added, so bypass the internal storage definitions
1623 // and fetch the table mapping using the passed in storage definition.
1624 // @todo Fix this in https://www.drupal.org/node/2705205.
1625 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
1626 $storage_definitions[$storage_definition->getName()] = $storage_definition;
1627 $table_mapping = $this->getTableMapping($storage_definitions);
1629 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1630 $is_deleted = $storage_definition->isDeleted();
1631 if ($this->entityType->isRevisionable()) {
1632 $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1635 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1637 $query = $this->database->select($table_name, 't');
1638 $or = $query->orConditionGroup();
1639 foreach ($storage_definition->getColumns() as $column_name => $data) {
1640 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1642 $query->condition($or);
1645 ->fields('t', ['entity_id'])
1649 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1650 // Ascertain the table this field is mapped too.
1651 $field_name = $storage_definition->getName();
1652 $table_name = $table_mapping->getFieldTableName($field_name);
1653 $query = $this->database->select($table_name, 't');
1654 $or = $query->orConditionGroup();
1655 foreach (array_keys($storage_definition->getColumns()) as $property_name) {
1656 $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
1658 $query->condition($or);
1661 ->fields('t', [$this->idKey])
1666 // @todo Find a way to count field data also for fields having custom
1667 // storage. See https://www.drupal.org/node/2337753.
1669 if (isset($query)) {
1670 // If we are performing the query just to check if the field has data
1671 // limit the number of rows.
1675 ->addExpression('1');
1678 // Otherwise count the number of rows.
1679 $query = $query->countQuery();
1681 $count = $query->execute()->fetchField();
1683 return $as_bool ? (bool) $count : (int) $count;
1687 * Determines whether the passed field has been already deleted.
1689 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1690 * The field storage definition.
1693 * Whether the field has been already deleted.
1695 * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
1696 * \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
1698 * @see https://www.drupal.org/node/2907785
1700 protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1701 return $storage_definition->isDeleted();