Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Sql / SqlContentEntityStorage.php
1 <?php
2
3 namespace Drupal\Core\Entity\Sql;
4
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;
26
27 /**
28  * A content entity database storage implementation.
29  *
30  * This class can be used as-is by most content entity types. Entity types
31  * requiring special handling can extend the class.
32  *
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.
37  *
38  * @ingroup entity_api
39  */
40 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
41
42   /**
43    * The mapping of field columns to SQL tables.
44    *
45    * @var \Drupal\Core\Entity\Sql\TableMappingInterface
46    */
47   protected $tableMapping;
48
49   /**
50    * Name of entity's revision database table field, if it supports revisions.
51    *
52    * Has the value FALSE if this entity does not use revisions.
53    *
54    * @var string
55    */
56   protected $revisionKey = FALSE;
57
58   /**
59    * The entity langcode key.
60    *
61    * @var string|bool
62    */
63   protected $langcodeKey = FALSE;
64
65   /**
66    * The default language entity key.
67    *
68    * @var string
69    */
70   protected $defaultLangcodeKey = FALSE;
71
72   /**
73    * The base table of the entity.
74    *
75    * @var string
76    */
77   protected $baseTable;
78
79   /**
80    * The table that stores revisions, if the entity supports revisions.
81    *
82    * @var string
83    */
84   protected $revisionTable;
85
86   /**
87    * The table that stores properties, if the entity has multilingual support.
88    *
89    * @var string
90    */
91   protected $dataTable;
92
93   /**
94    * The table that stores revision field data if the entity supports revisions.
95    *
96    * @var string
97    */
98   protected $revisionDataTable;
99
100   /**
101    * Active database connection.
102    *
103    * @var \Drupal\Core\Database\Connection
104    */
105   protected $database;
106
107   /**
108    * The entity type's storage schema object.
109    *
110    * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
111    */
112   protected $storageSchema;
113
114   /**
115    * The language manager.
116    *
117    * @var \Drupal\Core\Language\LanguageManagerInterface
118    */
119   protected $languageManager;
120
121   /**
122    * Whether this storage should use the temporary table mapping.
123    *
124    * @var bool
125    */
126   protected $temporary = FALSE;
127
128   /**
129    * {@inheritdoc}
130    */
131   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
132     return new static(
133       $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')
139     );
140   }
141
142   /**
143    * Gets the base field definitions for a content entity type.
144    *
145    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
146    *   The array of base field definitions for the entity type, keyed by field
147    *   name.
148    */
149   public function getFieldStorageDefinitions() {
150     return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
151   }
152
153   /**
154    * Constructs a SqlContentEntityStorage object.
155    *
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.
168    */
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();
174   }
175
176   /**
177    * Initializes table name variables.
178    */
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;
187
188     $table_mapping = $this->getTableMapping();
189     $this->baseTable = $table_mapping->getBaseTable();
190     $revisionable = $this->entityType->isRevisionable();
191     if ($revisionable) {
192       $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
193       $this->revisionTable = $table_mapping->getRevisionTable();
194     }
195     $translatable = $this->entityType->isTranslatable();
196     if ($translatable) {
197       $this->dataTable = $table_mapping->getDataTable();
198       $this->langcodeKey = $this->entityType->getKey('langcode');
199       $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
200     }
201     if ($revisionable && $translatable) {
202       $this->revisionDataTable = $table_mapping->getRevisionDataTable();
203     }
204   }
205
206   /**
207    * Gets the base table name.
208    *
209    * @return string
210    *   The table name.
211    */
212   public function getBaseTable() {
213     return $this->baseTable;
214   }
215
216   /**
217    * Gets the revision table name.
218    *
219    * @return string|false
220    *   The table name or FALSE if it is not available.
221    */
222   public function getRevisionTable() {
223     return $this->revisionTable;
224   }
225
226   /**
227    * Gets the data table name.
228    *
229    * @return string|false
230    *   The table name or FALSE if it is not available.
231    */
232   public function getDataTable() {
233     return $this->dataTable;
234   }
235
236   /**
237    * Gets the revision data table name.
238    *
239    * @return string|false
240    *   The table name or FALSE if it is not available.
241    */
242   public function getRevisionDataTable() {
243     return $this->revisionDataTable;
244   }
245
246   /**
247    * Gets the entity type's storage schema object.
248    *
249    * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
250    *   The schema object.
251    */
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);
256     }
257     return $this->storageSchema;
258   }
259
260   /**
261    * Updates the wrapped entity type definition.
262    *
263    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
264    *   The update entity type.
265    *
266    * @internal Only to be used internally by Entity API. Expected to be
267    *   removed by https://www.drupal.org/node/2274017.
268    */
269   public function setEntityType(EntityTypeInterface $entity_type) {
270     if ($this->entityType->id() == $entity_type->id()) {
271       $this->entityType = $entity_type;
272       $this->initTableLayout();
273     }
274     else {
275       throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
276     }
277   }
278
279   /**
280    * Sets the wrapped table mapping definition.
281    *
282    * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
283    *   The table mapping.
284    *
285    * @internal Only to be used internally by Entity API. Expected to be removed
286    *   by https://www.drupal.org/node/2554235.
287    */
288   public function setTableMapping(TableMappingInterface $table_mapping) {
289     $this->tableMapping = $table_mapping;
290
291     $this->baseTable = $table_mapping->getBaseTable();
292     $this->revisionTable = $table_mapping->getRevisionTable();
293     $this->dataTable = $table_mapping->getDataTable();
294     $this->revisionDataTable = $table_mapping->getRevisionDataTable();
295   }
296
297   /**
298    * Changes the temporary state of the storage.
299    *
300    * @param bool $temporary
301    *   Whether to use a temporary table mapping or not.
302    *
303    * @internal Only to be used internally by Entity API.
304    */
305   public function setTemporary($temporary) {
306     $this->temporary = $temporary;
307   }
308
309   /**
310    * {@inheritdoc}
311    */
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
315     // without caching.
316     if ($storage_definitions) {
317       return $this->getCustomTableMapping($this->entityType, $storage_definitions);
318     }
319
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);
324
325       $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
326     }
327
328     return $this->tableMapping;
329   }
330
331   /**
332    * Gets a table mapping for the specified entity type and storage definitions.
333    *
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
338    *   mapping.
339    *
340    * @return \Drupal\Core\Entity\Sql\TableMappingInterface
341    *   A table mapping object for the entity's tables.
342    *
343    * @internal
344    */
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);
348   }
349
350   /**
351    * {@inheritdoc}
352    */
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);
357
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);
362     }
363
364     return $entities_from_cache + $entities_from_storage;
365   }
366
367   /**
368    * Gets entities from the storage.
369    *
370    * @param array|null $ids
371    *   If not empty, return entities that match these IDs. Return all entities
372    *   when NULL.
373    *
374    * @return \Drupal\Core\Entity\ContentEntityInterface[]
375    *   Array of entities from the storage.
376    */
377   protected function getFromStorage(array $ids = NULL) {
378     $entities = [];
379
380     if (!empty($ids)) {
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);
384     }
385
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);
390
391       // Map the loaded records into entity objects and according fields.
392       if ($records) {
393         $entities = $this->mapFromStorageRecords($records);
394       }
395     }
396
397     return $entities;
398   }
399
400   /**
401    * Maps from storage records to entity objects, and attaches fields.
402    *
403    * @param array $records
404    *   Associative array of query results, keyed on the entity ID or revision
405    *   ID.
406    * @param bool $load_from_revision
407    *   (optional) Flag to indicate whether revisions should be loaded or not.
408    *   Defaults to FALSE.
409    *
410    * @return array
411    *   An array of entity objects implementing the EntityInterface.
412    */
413   protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
414     if (!$records) {
415       return [];
416     }
417
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)));
424     }
425
426     $values = [];
427     foreach ($records as $id => $record) {
428       $values[$id] = [];
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});
440             }
441           }
442         }
443         // Handle field types that store only one property.
444         else {
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});
449           }
450         }
451       }
452
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;
457       }
458     }
459
460     // Initialize translations array.
461     $translations = array_fill_keys(array_keys($values), []);
462
463     // Load values from shared and dedicated tables.
464     $this->loadFromSharedTables($values, $translations, $load_from_revision);
465     $this->loadFromDedicatedTables($values, $load_from_revision);
466
467     $entities = [];
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]));
472     }
473
474     return $entities;
475   }
476
477   /**
478    * Loads values for fields stored in the shared data tables.
479    *
480    * @param array &$values
481    *   Associative array of entities values, keyed on the entity ID or the
482    *   revision ID.
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.
487    */
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])
496         ->fields($alias)
497         ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
498         ->orderBy($alias . '.' . $record_key);
499
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);
506
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
512         // entity values.
513         $all_fields = $revisioned_fields;
514         if ($data_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)");
517           $column_names = [];
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)));
526           }
527           $query->fields('data', $column_names);
528         }
529
530         // Get the revision IDs.
531         $revision_ids = [];
532         foreach ($values as $entity_values) {
533           $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
534         }
535         $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
536       }
537       else {
538         $all_fields = $table_mapping->getFieldNames($this->dataTable);
539       }
540
541       $result = $query->execute();
542       foreach ($result as $row) {
543         $id = $row[$record_key];
544
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;
548
549         $translations[$id][$langcode] = TRUE;
550
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)];
556           }
557           else {
558             foreach ($columns as $property_name => $column_name) {
559               $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
560             }
561           }
562         }
563       }
564     }
565   }
566
567   /**
568    * {@inheritdoc}
569    */
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);
572
573     $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
574
575     return !empty($revisions) ? reset($revisions) : NULL;
576   }
577
578   /**
579    * {@inheritdoc}
580    */
581   protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
582     $revisions = [];
583
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');
587
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);
592
593       // Map the loaded records into entity objects and according fields.
594       if ($records) {
595         $revisions = $this->mapFromStorageRecords($records, TRUE);
596       }
597     }
598
599     return $revisions;
600   }
601
602   /**
603    * {@inheritdoc}
604    */
605   protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
606     $this->database->delete($this->revisionTable)
607       ->condition($this->revisionKey, $revision->getRevisionId())
608       ->execute();
609
610     if ($this->revisionDataTable) {
611       $this->database->delete($this->revisionDataTable)
612         ->condition($this->revisionKey, $revision->getRevisionId())
613         ->execute();
614     }
615
616     $this->deleteRevisionFromDedicatedTables($revision);
617   }
618
619   /**
620    * {@inheritdoc}
621    */
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
628       // otherwise.
629       if (!array_key_exists($this->defaultLangcodeKey, $values)) {
630         $values[$this->defaultLangcodeKey] = 1;
631       }
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]);
636       }
637     }
638
639     parent::buildPropertyQuery($entity_query, $values);
640   }
641
642   /**
643    * Builds the query to load the entity.
644    *
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.
652    *
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.
658    *
659    * @return \Drupal\Core\Database\Query\Select
660    *   A SelectQuery object for loading the entity.
661    */
662   protected function buildQuery($ids, $revision_ids = FALSE) {
663     $query = $this->database->select($this->baseTable, 'base');
664
665     $query->addTag($this->entityTypeId . '_load_multiple');
666
667     if ($revision_ids) {
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);
670       }
671       $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
672     }
673     elseif ($this->revisionTable) {
674       $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
675     }
676
677     // Add fields from the {entity} table.
678     $table_mapping = $this->getTableMapping();
679     $entity_fields = $table_mapping->getAllColumns($this->baseTable);
680
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]);
687
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]]);
694         }
695       }
696       $query->fields('revision', $entity_revision_fields);
697
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');
701     }
702
703     $query->fields('base', $entity_fields);
704
705     if ($ids) {
706       $query->condition("base.{$this->idKey}", $ids, 'IN');
707     }
708
709     return $query;
710   }
711
712   /**
713    * {@inheritdoc}
714    */
715   public function delete(array $entities) {
716     if (!$entities) {
717       // If no IDs or invalid IDs were passed, do nothing.
718       return;
719     }
720
721     $transaction = $this->database->startTransaction();
722     try {
723       parent::delete($entities);
724
725       // Ignore replica server temporarily.
726       db_ignore_replica();
727     }
728     catch (\Exception $e) {
729       $transaction->rollBack();
730       watchdog_exception($this->entityTypeId, $e);
731       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
732     }
733   }
734
735   /**
736    * {@inheritdoc}
737    */
738   protected function doDeleteFieldItems($entities) {
739     $ids = array_keys($entities);
740
741     $this->database->delete($this->baseTable)
742       ->condition($this->idKey, $ids, 'IN')
743       ->execute();
744
745     if ($this->revisionTable) {
746       $this->database->delete($this->revisionTable)
747         ->condition($this->idKey, $ids, 'IN')
748         ->execute();
749     }
750
751     if ($this->dataTable) {
752       $this->database->delete($this->dataTable)
753         ->condition($this->idKey, $ids, 'IN')
754         ->execute();
755     }
756
757     if ($this->revisionDataTable) {
758       $this->database->delete($this->revisionDataTable)
759         ->condition($this->idKey, $ids, 'IN')
760         ->execute();
761     }
762
763     foreach ($entities as $entity) {
764       $this->deleteFromDedicatedTables($entity);
765     }
766   }
767
768   /**
769    * {@inheritdoc}
770    */
771   public function save(EntityInterface $entity) {
772     $transaction = $this->database->startTransaction();
773     try {
774       $return = parent::save($entity);
775
776       // Ignore replica server temporarily.
777       db_ignore_replica();
778       return $return;
779     }
780     catch (\Exception $e) {
781       $transaction->rollBack();
782       watchdog_exception($this->entityTypeId, $e);
783       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
784     }
785   }
786
787   /**
788    * {@inheritdoc}
789    */
790   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
791     $full_save = empty($names);
792     $update = !$full_save || !$entity->isNew();
793
794     if ($full_save) {
795       $shared_table_fields = TRUE;
796       $dedicated_table_fields = TRUE;
797     }
798     else {
799       $table_mapping = $this->getTableMapping();
800       $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
801       $shared_table_fields = FALSE;
802       $dedicated_table_fields = [];
803
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;
810         }
811         elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
812           $dedicated_table_fields[] = $name;
813         }
814       }
815     }
816
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.
821       if ($update) {
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});
827           $this->database
828             ->update($this->baseTable)
829             ->fields((array) $record)
830             ->condition($this->idKey, $entity->get($this->idKey)->value)
831             ->execute();
832         }
833         if ($this->revisionTable) {
834           if ($full_save) {
835             $entity->{$this->revisionKey} = $this->saveRevision($entity);
836           }
837           else {
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,
841             // mssql.
842             unset($record->{$this->revisionKey});
843             $entity->preSaveRevision($this, $record);
844             $this->database
845               ->update($this->revisionTable)
846               ->fields((array) $record)
847               ->condition($this->revisionKey, $entity->getRevisionId())
848               ->execute();
849           }
850         }
851         if ($default_revision && $this->dataTable) {
852           $this->saveToSharedTables($entity);
853         }
854         if ($this->revisionDataTable) {
855           $new_revision = $full_save && $entity->isNewRevision();
856           $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
857         }
858       }
859       else {
860         $insert_id = $this->database
861           ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
862           ->fields((array) $record)
863           ->execute();
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;
869         }
870         $entity->{$this->idKey} = (string) $record->{$this->idKey};
871         if ($this->revisionTable) {
872           $record->{$this->revisionKey} = $this->saveRevision($entity);
873         }
874         if ($this->dataTable) {
875           $this->saveToSharedTables($entity);
876         }
877         if ($this->revisionDataTable) {
878           $this->saveToSharedTables($entity, $this->revisionDataTable);
879         }
880       }
881     }
882
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);
887     }
888   }
889
890   /**
891    * {@inheritdoc}
892    */
893   protected function has($id, EntityInterface $entity) {
894     return !$entity->isNew();
895   }
896
897   /**
898    * Saves fields that use the shared tables.
899    *
900    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
901    *   The entity object.
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.
907    */
908   protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
909     if (!isset($table_name)) {
910       $table_name = $this->dataTable;
911     }
912     if (!isset($new_revision)) {
913       $new_revision = $entity->isNewRevision();
914     }
915     $revision = $table_name != $this->dataTable;
916
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)
923         ->execute();
924     }
925
926     $query = $this->database->insert($table_name);
927
928     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
929       $translation = $entity->getTranslation($langcode);
930       $record = $this->mapToDataStorageRecord($translation, $table_name);
931       $values = (array) $record;
932       $query
933         ->fields(array_keys($values))
934         ->values($values);
935     }
936
937     $query->execute();
938   }
939
940   /**
941    * Maps from an entity object to the storage record.
942    *
943    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
944    *   The entity object.
945    * @param string $table_name
946    *   (optional) The table name to map records to. Defaults to the base table.
947    *
948    * @return \stdClass
949    *   The record to store.
950    */
951   protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
952     if (!isset($table_name)) {
953       $table_name = $this->baseTable;
954     }
955
956     $record = new \stdClass();
957     $table_mapping = $this->getTableMapping();
958     foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
959
960       if (empty($this->getFieldStorageDefinitions()[$field_name])) {
961         throw new EntityStorageException("Table mapping contains invalid field $field_name.");
962       }
963       $definition = $this->getFieldStorageDefinitions()[$field_name];
964       $columns = $table_mapping->getColumnNames($field_name);
965
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() : [];
974         }
975         else {
976           $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
977         }
978         if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
979           $value = serialize($value);
980         }
981
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;
988         }
989       }
990     }
991
992     return $record;
993   }
994
995   /**
996    * Checks whether a field column should be treated as serial.
997    *
998    * @param $table_name
999    *   The name of the table the field column belongs to.
1000    * @param $schema_name
1001    *   The schema name of the field column.
1002    *
1003    * @return bool
1004    *   TRUE if the column is serial, FALSE otherwise.
1005    *
1006    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1007    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1008    */
1009   protected function isColumnSerial($table_name, $schema_name) {
1010     $result = FALSE;
1011
1012     switch ($table_name) {
1013       case $this->baseTable:
1014         $result = $schema_name == $this->idKey;
1015         break;
1016
1017       case $this->revisionTable:
1018         $result = $schema_name == $this->revisionKey;
1019         break;
1020     }
1021
1022     return $result;
1023   }
1024
1025   /**
1026    * Maps from an entity object to the storage record of the field data.
1027    *
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.
1032    *
1033    * @return \stdClass
1034    *   The record to store.
1035    */
1036   protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1037     if (!isset($table_name)) {
1038       $table_name = $this->dataTable;
1039     }
1040     $record = $this->mapToStorageRecord($entity, $table_name);
1041     return $record;
1042   }
1043
1044   /**
1045    * Saves an entity revision.
1046    *
1047    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1048    *   The entity object.
1049    *
1050    * @return int
1051    *   The revision id.
1052    */
1053   protected function saveRevision(ContentEntityInterface $entity) {
1054     $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1055
1056     $entity->preSaveRevision($this, $record);
1057
1058     if ($entity->isNewRevision()) {
1059       $insert_id = $this->database
1060         ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1061         ->fields((array) $record)
1062         ->execute();
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;
1067       }
1068       if ($entity->isDefaultRevision()) {
1069         $this->database->update($this->baseTable)
1070           ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1071           ->condition($this->idKey, $record->{$this->idKey})
1072           ->execute();
1073       }
1074       // Make sure to update the new revision key for the entity.
1075       $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1076     }
1077     else {
1078       // Remove the revision ID from the record to enable updates on SQL
1079       // variants that prevent updating serial columns, for example,
1080       // mssql.
1081       unset($record->{$this->revisionKey});
1082       $this->database
1083         ->update($this->revisionTable)
1084         ->fields((array) $record)
1085         ->condition($this->revisionKey, $entity->getRevisionId())
1086         ->execute();
1087     }
1088     return $entity->getRevisionId();
1089   }
1090
1091   /**
1092    * {@inheritdoc}
1093    */
1094   protected function getQueryServiceName() {
1095     return 'entity.query.sql';
1096   }
1097
1098   /**
1099    * Loads values of fields stored in dedicated tables for a group of entities.
1100    *
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.
1105    */
1106   protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1107     if (empty($values)) {
1108       return;
1109     }
1110
1111     // Collect entities ids, bundles and languages.
1112     $bundles = [];
1113     $ids = [];
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];
1120       }
1121     }
1122
1123     // Collect impacted fields.
1124     $storage_definitions = [];
1125     $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;
1133         }
1134       }
1135     }
1136
1137     // Load field data.
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);
1141
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')
1146         ->fields('t')
1147         ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1148         ->condition('deleted', 0)
1149         ->condition('langcode', $langcodes, 'IN')
1150         ->orderBy('delta')
1151         ->execute();
1152
1153       foreach ($results as $row) {
1154         $bundle = $row->bundle;
1155
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;
1162         }
1163
1164         if (!isset($values[$value_key][$field_name][$langcode])) {
1165           $values[$value_key][$field_name][$langcode] = [];
1166         }
1167
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()) {
1172             $item = [];
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;
1179             }
1180
1181             // Add the item to the field values for the entity.
1182             $values[$value_key][$field_name][$langcode][] = $item;
1183           }
1184         }
1185       }
1186     }
1187   }
1188
1189   /**
1190    * Saves values of fields that use dedicated tables.
1191    *
1192    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1193    *   The 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
1198    *   available fields.
1199    */
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();
1208
1209     if (!isset($vid)) {
1210       $vid = $id;
1211     }
1212
1213     $original = !empty($entity->original) ? $entity->original : NULL;
1214
1215     // Determine which fields should be actually stored.
1216     $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1217     if ($names) {
1218       $definitions = array_intersect_key($definitions, array_flip($names));
1219     }
1220
1221     foreach ($definitions as $field_name => $field_definition) {
1222       $storage_definition = $field_definition->getFieldStorageDefinition();
1223       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1224         continue;
1225       }
1226
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)) {
1230         continue;
1231       }
1232
1233       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1234       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1235
1236       // Delete and insert, rather than update, in case a value was added.
1237       if ($update) {
1238         // Only overwrite the field's base table if saving the default revision
1239         // of an entity.
1240         if ($entity->isDefaultRevision()) {
1241           $this->database->delete($table_name)
1242             ->condition('entity_id', $id)
1243             ->execute();
1244         }
1245         if ($this->entityType->isRevisionable()) {
1246           $this->database->delete($revision_name)
1247             ->condition('entity_id', $id)
1248             ->condition('revision_id', $vid)
1249             ->execute();
1250         }
1251       }
1252
1253       // Prepare the multi-insert query.
1254       $do_insert = FALSE;
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);
1258       }
1259       $query = $this->database->insert($table_name)->fields($columns);
1260       if ($this->entityType->isRevisionable()) {
1261         $revision_query = $this->database->insert($revision_name)->fields($columns);
1262       }
1263
1264       $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1265       foreach ($langcodes as $langcode) {
1266         $delta_count = 0;
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.
1271           $do_insert = TRUE;
1272           $record = [
1273             'entity_id' => $id,
1274             'revision_id' => $vid,
1275             'bundle' => $bundle,
1276             'delta' => $delta,
1277             'langcode' => $langcode,
1278           ];
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);
1285             }
1286             $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1287           }
1288           $query->values($record);
1289           if ($this->entityType->isRevisionable()) {
1290             $revision_query->values($record);
1291           }
1292
1293           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1294             break;
1295           }
1296         }
1297       }
1298
1299       // Execute the query if we have values to insert.
1300       if ($do_insert) {
1301         // Only overwrite the field's base table if saving the default revision
1302         // of an entity.
1303         if ($entity->isDefaultRevision()) {
1304           $query->execute();
1305         }
1306         if ($this->entityType->isRevisionable()) {
1307           $revision_query->execute();
1308         }
1309       }
1310     }
1311   }
1312
1313   /**
1314    * Deletes values of fields in dedicated tables for all revisions.
1315    *
1316    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1317    *   The entity.
1318    */
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)) {
1324         continue;
1325       }
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())
1330         ->execute();
1331       if ($this->entityType->isRevisionable()) {
1332         $this->database->delete($revision_name)
1333           ->condition('entity_id', $entity->id())
1334           ->execute();
1335       }
1336     }
1337   }
1338
1339   /**
1340    * Deletes values of fields in dedicated tables for all revisions.
1341    *
1342    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1343    *   The entity. It must have a revision ID.
1344    */
1345   protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1346     $vid = $entity->getRevisionId();
1347     if (isset($vid)) {
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)) {
1352           continue;
1353         }
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)
1358           ->execute();
1359       }
1360     }
1361   }
1362
1363   /**
1364    * {@inheritdoc}
1365    */
1366   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1367     return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1368   }
1369
1370   /**
1371    * {@inheritdoc}
1372    */
1373   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1374     return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1375   }
1376
1377   /**
1378    * {@inheritdoc}
1379    */
1380   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1381     return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1382   }
1383
1384   /**
1385    * {@inheritdoc}
1386    */
1387   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1388     return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1389   }
1390
1391   /**
1392    * {@inheritdoc}
1393    */
1394   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1395     $this->wrapSchemaException(function () use ($entity_type) {
1396       $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1397     });
1398   }
1399
1400   /**
1401    * {@inheritdoc}
1402    */
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
1407     // definition.
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);
1412     });
1413   }
1414
1415   /**
1416    * {@inheritdoc}
1417    */
1418   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1419     $this->wrapSchemaException(function () use ($entity_type) {
1420       $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1421     });
1422   }
1423
1424   /**
1425    * {@inheritdoc}
1426    */
1427   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1428     $this->wrapSchemaException(function () use ($storage_definition) {
1429       $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1430     });
1431   }
1432
1433   /**
1434    * {@inheritdoc}
1435    */
1436   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1437     $this->wrapSchemaException(function () use ($storage_definition, $original) {
1438       $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1439     });
1440   }
1441
1442   /**
1443    * {@inheritdoc}
1444    */
1445   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1446     $table_mapping = $this->getTableMapping(
1447       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1448     );
1449
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])
1456         ->execute();
1457       if ($this->entityType->isRevisionable()) {
1458         $this->database->update($revision_table)
1459           ->fields(['deleted' => 1])
1460           ->execute();
1461       }
1462     }
1463
1464     // Update the field schema.
1465     $this->wrapSchemaException(function () use ($storage_definition) {
1466       $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1467     });
1468   }
1469
1470   /**
1471    * Wraps a database schema exception into an entity storage exception.
1472    *
1473    * @param callable $callback
1474    *   The callback to be executed.
1475    *
1476    * @throws \Drupal\Core\Entity\EntityStorageException
1477    *   When a database schema exception is thrown.
1478    */
1479   protected function wrapSchemaException(callable $callback) {
1480     $message = 'Exception thrown while performing a schema update.';
1481     try {
1482       $callback();
1483     }
1484     catch (SchemaException $e) {
1485       $message .= ' ' . $e->getMessage();
1486       throw new EntityStorageException($message, 0, $e);
1487     }
1488     catch (DatabaseExceptionWrapper $e) {
1489       $message .= ' ' . $e->getMessage();
1490       throw new EntityStorageException($message, 0, $e);
1491     }
1492   }
1493
1494   /**
1495    * {@inheritdoc}
1496    */
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())
1507         ->execute();
1508       if ($this->entityType->isRevisionable()) {
1509         $this->database->update($revision_name)
1510           ->fields(['deleted' => 1])
1511           ->condition('bundle', $field_definition->getTargetBundle())
1512           ->execute();
1513       }
1514     }
1515   }
1516
1517   /**
1518    * {@inheritdoc}
1519    */
1520   public function onBundleCreate($bundle, $entity_type_id) {}
1521
1522   /**
1523    * {@inheritdoc}
1524    */
1525   public function onBundleDelete($bundle, $entity_type_id) {}
1526
1527   /**
1528    * {@inheritdoc}
1529    */
1530   protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1531     // Check whether the whole field storage definition is gone, or just some
1532     // bundle fields.
1533     $storage_definition = $field_definition->getFieldStorageDefinition();
1534     $table_mapping = $this->getTableMapping();
1535     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1536
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));
1542     }
1543     $entity_query
1544       ->distinct(TRUE)
1545       ->fields('t', ['entity_id'])
1546       ->condition('bundle', $field_definition->getTargetBundle())
1547       ->range(0, $batch_size);
1548
1549     // Create a map of field data table column names to field column names.
1550     $column_map = [];
1551     foreach ($storage_definition->getColumns() as $column_name => $data) {
1552       $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1553     }
1554
1555     $entities = [];
1556     $items_by_entity = [];
1557     foreach ($entity_query->execute() as $row) {
1558       $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1559         ->fields('t')
1560         ->condition('entity_id', $row['entity_id'])
1561         ->condition('deleted', 1)
1562         ->orderBy('delta');
1563
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);
1571         }
1572         $item = [];
1573         foreach ($column_map as $db_column => $field_column) {
1574           $item[$field_column] = $item_row[$db_column];
1575         }
1576         $items_by_entity[$item_row['revision_id']][] = $item;
1577       }
1578     }
1579
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);
1584     }
1585     return $items_by_entity;
1586   }
1587
1588   /**
1589    * {@inheritdoc}
1590    */
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)
1601       ->execute();
1602     if ($this->entityType->isRevisionable()) {
1603       $this->database->delete($revision_name)
1604         ->condition('revision_id', $revision_id)
1605         ->condition('deleted', 1)
1606         ->execute();
1607     }
1608   }
1609
1610   /**
1611    * {@inheritdoc}
1612    */
1613   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1614     $this->getStorageSchema()->finalizePurge($storage_definition);
1615   }
1616
1617   /**
1618    * {@inheritdoc}
1619    */
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);
1628
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);
1633       }
1634       else {
1635         $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1636       }
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));
1641       }
1642       $query->condition($or);
1643       if (!$as_bool) {
1644         $query
1645           ->fields('t', ['entity_id'])
1646           ->distinct(TRUE);
1647       }
1648     }
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));
1657       }
1658       $query->condition($or);
1659       if (!$as_bool) {
1660         $query
1661           ->fields('t', [$this->idKey])
1662           ->distinct(TRUE);
1663       }
1664     }
1665
1666     // @todo Find a way to count field data also for fields having custom
1667     //   storage. See https://www.drupal.org/node/2337753.
1668     $count = 0;
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.
1672       if ($as_bool) {
1673         $query
1674           ->range(0, 1)
1675           ->addExpression('1');
1676       }
1677       else {
1678         // Otherwise count the number of rows.
1679         $query = $query->countQuery();
1680       }
1681       $count = $query->execute()->fetchField();
1682     }
1683     return $as_bool ? (bool) $count : (int) $count;
1684   }
1685
1686   /**
1687    * Determines whether the passed field has been already deleted.
1688    *
1689    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1690    *   The field storage definition.
1691    *
1692    * @return bool
1693    *   Whether the field has been already deleted.
1694    *
1695    * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
1696    *   \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
1697    *
1698    * @see https://www.drupal.org/node/2907785
1699    */
1700   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1701     return $storage_definition->isDeleted();
1702   }
1703
1704 }