Updated to Drupal 8.5. Core Media not yet in use.
[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\Database\Connection;
7 use Drupal\Core\Database\Database;
8 use Drupal\Core\Database\DatabaseExceptionWrapper;
9 use Drupal\Core\Database\SchemaException;
10 use Drupal\Core\Entity\ContentEntityInterface;
11 use Drupal\Core\Entity\ContentEntityStorageBase;
12 use Drupal\Core\Entity\EntityBundleListenerInterface;
13 use Drupal\Core\Entity\EntityInterface;
14 use Drupal\Core\Entity\EntityManagerInterface;
15 use Drupal\Core\Entity\EntityStorageException;
16 use Drupal\Core\Entity\EntityTypeInterface;
17 use Drupal\Core\Entity\Query\QueryInterface;
18 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
19 use Drupal\Core\Field\FieldDefinitionInterface;
20 use Drupal\Core\Field\FieldStorageDefinitionInterface;
21 use Drupal\Core\Language\LanguageInterface;
22 use Drupal\Core\Language\LanguageManagerInterface;
23 use Symfony\Component\DependencyInjection\ContainerInterface;
24
25 /**
26  * A content entity database storage implementation.
27  *
28  * This class can be used as-is by most content entity types. Entity types
29  * requiring special handling can extend the class.
30  *
31  * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
32  * internally in order to automatically generate the database schema based on
33  * the defined base fields. Entity types can override the schema handler to
34  * customize the generated schema; e.g., to add additional indexes.
35  *
36  * @ingroup entity_api
37  */
38 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
39
40   /**
41    * The mapping of field columns to SQL tables.
42    *
43    * @var \Drupal\Core\Entity\Sql\TableMappingInterface
44    */
45   protected $tableMapping;
46
47   /**
48    * Name of entity's revision database table field, if it supports revisions.
49    *
50    * Has the value FALSE if this entity does not use revisions.
51    *
52    * @var string
53    */
54   protected $revisionKey = FALSE;
55
56   /**
57    * The entity langcode key.
58    *
59    * @var string|bool
60    */
61   protected $langcodeKey = FALSE;
62
63   /**
64    * The default language entity key.
65    *
66    * @var string
67    */
68   protected $defaultLangcodeKey = FALSE;
69
70   /**
71    * The base table of the entity.
72    *
73    * @var string
74    */
75   protected $baseTable;
76
77   /**
78    * The table that stores revisions, if the entity supports revisions.
79    *
80    * @var string
81    */
82   protected $revisionTable;
83
84   /**
85    * The table that stores properties, if the entity has multilingual support.
86    *
87    * @var string
88    */
89   protected $dataTable;
90
91   /**
92    * The table that stores revision field data if the entity supports revisions.
93    *
94    * @var string
95    */
96   protected $revisionDataTable;
97
98   /**
99    * Active database connection.
100    *
101    * @var \Drupal\Core\Database\Connection
102    */
103   protected $database;
104
105   /**
106    * The entity type's storage schema object.
107    *
108    * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
109    */
110   protected $storageSchema;
111
112   /**
113    * The language manager.
114    *
115    * @var \Drupal\Core\Language\LanguageManagerInterface
116    */
117   protected $languageManager;
118
119   /**
120    * Whether this storage should use the temporary table mapping.
121    *
122    * @var bool
123    */
124   protected $temporary = FALSE;
125
126   /**
127    * {@inheritdoc}
128    */
129   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
130     return new static(
131       $entity_type,
132       $container->get('database'),
133       $container->get('entity.manager'),
134       $container->get('cache.entity'),
135       $container->get('language_manager')
136     );
137   }
138
139   /**
140    * Gets the base field definitions for a content entity type.
141    *
142    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
143    *   The array of base field definitions for the entity type, keyed by field
144    *   name.
145    */
146   public function getFieldStorageDefinitions() {
147     return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
148   }
149
150   /**
151    * Constructs a SqlContentEntityStorage object.
152    *
153    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
154    *   The entity type definition.
155    * @param \Drupal\Core\Database\Connection $database
156    *   The database connection to be used.
157    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
158    *   The entity manager.
159    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
160    *   The cache backend to be used.
161    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
162    *   The language manager.
163    */
164   public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
165     parent::__construct($entity_type, $entity_manager, $cache);
166     $this->database = $database;
167     $this->languageManager = $language_manager;
168     $this->initTableLayout();
169   }
170
171   /**
172    * Initializes table name variables.
173    */
174   protected function initTableLayout() {
175     // Reset table field values to ensure changes in the entity type definition
176     // are correctly reflected in the table layout.
177     $this->tableMapping = NULL;
178     $this->revisionKey = NULL;
179     $this->revisionTable = NULL;
180     $this->dataTable = NULL;
181     $this->revisionDataTable = NULL;
182
183     // @todo Remove table names from the entity type definition in
184     //   https://www.drupal.org/node/2232465.
185     $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
186     $revisionable = $this->entityType->isRevisionable();
187     if ($revisionable) {
188       $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
189       $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
190     }
191     $translatable = $this->entityType->isTranslatable();
192     if ($translatable) {
193       $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
194       $this->langcodeKey = $this->entityType->getKey('langcode');
195       $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
196     }
197     if ($revisionable && $translatable) {
198       $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
199     }
200   }
201
202   /**
203    * Gets the base table name.
204    *
205    * @return string
206    *   The table name.
207    */
208   public function getBaseTable() {
209     return $this->baseTable;
210   }
211
212   /**
213    * Gets the revision table name.
214    *
215    * @return string|false
216    *   The table name or FALSE if it is not available.
217    */
218   public function getRevisionTable() {
219     return $this->revisionTable;
220   }
221
222   /**
223    * Gets the data table name.
224    *
225    * @return string|false
226    *   The table name or FALSE if it is not available.
227    */
228   public function getDataTable() {
229     return $this->dataTable;
230   }
231
232   /**
233    * Gets the revision data table name.
234    *
235    * @return string|false
236    *   The table name or FALSE if it is not available.
237    */
238   public function getRevisionDataTable() {
239     return $this->revisionDataTable;
240   }
241
242   /**
243    * Gets the entity type's storage schema object.
244    *
245    * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
246    *   The schema object.
247    */
248   protected function getStorageSchema() {
249     if (!isset($this->storageSchema)) {
250       $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
251       $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
252     }
253     return $this->storageSchema;
254   }
255
256   /**
257    * Updates the wrapped entity type definition.
258    *
259    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
260    *   The update entity type.
261    *
262    * @internal Only to be used internally by Entity API. Expected to be
263    *   removed by https://www.drupal.org/node/2274017.
264    */
265   public function setEntityType(EntityTypeInterface $entity_type) {
266     if ($this->entityType->id() == $entity_type->id()) {
267       $this->entityType = $entity_type;
268       $this->initTableLayout();
269     }
270     else {
271       throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
272     }
273   }
274
275   /**
276    * Sets the wrapped table mapping definition.
277    *
278    * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
279    *   The table mapping.
280    *
281    * @internal Only to be used internally by Entity API. Expected to be removed
282    *   by https://www.drupal.org/node/2554235.
283    */
284   public function setTableMapping(TableMappingInterface $table_mapping) {
285     $this->tableMapping = $table_mapping;
286   }
287
288   /**
289    * Changes the temporary state of the storage.
290    *
291    * @param bool $temporary
292    *   Whether to use a temporary table mapping or not.
293    *
294    * @internal Only to be used internally by Entity API.
295    */
296   public function setTemporary($temporary) {
297     $this->temporary = $temporary;
298   }
299
300   /**
301    * {@inheritdoc}
302    */
303   public function getTableMapping(array $storage_definitions = NULL) {
304     $table_mapping = $this->tableMapping;
305
306     // If we are using our internal storage definitions, which is our main use
307     // case, we can statically cache the computed table mapping. If a new set
308     // of field storage definitions is passed, for instance when comparing old
309     // and new storage schema, we compute the table mapping without caching.
310     // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
311     //   easily instantiate a new table mapping whenever needed.
312     if (!isset($this->tableMapping) || $storage_definitions) {
313       $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
314       $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
315       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
316       $table_mapping = new $table_mapping_class($this->entityType, $definitions);
317
318       $shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
319         return $table_mapping->allowsSharedTableStorage($definition);
320       });
321
322       $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
323       $all_fields = array_keys($shared_table_definitions);
324       $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
325         return $definition->isRevisionable();
326       }));
327       // Make sure the key fields come first in the list of fields.
328       $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
329
330       // If the entity is revisionable, gather the fields that need to be put
331       // in the revision table.
332       $revisionable = $this->entityType->isRevisionable();
333       $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
334
335       $translatable = $this->entityType->isTranslatable();
336       if (!$revisionable && !$translatable) {
337         // The base layout stores all the base field values in the base table.
338         $table_mapping->setFieldNames($this->baseTable, $all_fields);
339       }
340       elseif ($revisionable && !$translatable) {
341         // The revisionable layout stores all the base field values in the base
342         // table, except for revision metadata fields. Revisionable fields
343         // denormalized in the base table but also stored in the revision table
344         // together with the entity ID and the revision ID as identifiers.
345         $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
346         $revision_key_fields = [$this->idKey, $this->revisionKey];
347         $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
348       }
349       elseif (!$revisionable && $translatable) {
350         // Multilingual layouts store key field values in the base table. The
351         // other base field values are stored in the data table, no matter
352         // whether they are translatable or not. The data table holds also a
353         // denormalized copy of the bundle field value to allow for more
354         // performant queries. This means that only the UUID is not stored on
355         // the data table.
356         $table_mapping
357           ->setFieldNames($this->baseTable, $key_fields)
358           ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
359       }
360       elseif ($revisionable && $translatable) {
361         // The revisionable multilingual layout stores key field values in the
362         // base table, except for language, which is stored in the revision
363         // table along with revision metadata. The revision data table holds
364         // data field values for all the revisionable fields and the data table
365         // holds the data field values for all non-revisionable fields. The data
366         // field values of revisionable fields are denormalized in the data
367         // table, as well.
368         $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
369
370         // Like in the multilingual, non-revisionable case the UUID is not
371         // in the data table. Additionally, do not store revision metadata
372         // fields in the data table.
373         $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
374         $table_mapping->setFieldNames($this->dataTable, $data_fields);
375
376         $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
377         $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
378
379         $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
380         $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
381         $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
382       }
383
384       // Add dedicated tables.
385       $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
386         return $table_mapping->requiresDedicatedTableStorage($definition);
387       });
388       $extra_columns = [
389         'bundle',
390         'deleted',
391         'entity_id',
392         'revision_id',
393         'langcode',
394         'delta',
395       ];
396       foreach ($dedicated_table_definitions as $field_name => $definition) {
397         $tables = [$table_mapping->getDedicatedDataTableName($definition)];
398         if ($revisionable && $definition->isRevisionable()) {
399           $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
400         }
401         foreach ($tables as $table_name) {
402           $table_mapping->setFieldNames($table_name, [$field_name]);
403           $table_mapping->setExtraColumns($table_name, $extra_columns);
404         }
405       }
406
407       // Cache the computed table mapping only if we are using our internal
408       // storage definitions.
409       if (!$storage_definitions) {
410         $this->tableMapping = $table_mapping;
411       }
412     }
413
414     return $table_mapping;
415   }
416
417   /**
418    * {@inheritdoc}
419    */
420   protected function doLoadMultiple(array $ids = NULL) {
421     // Attempt to load entities from the persistent cache. This will remove IDs
422     // that were loaded from $ids.
423     $entities_from_cache = $this->getFromPersistentCache($ids);
424
425     // Load any remaining entities from the database.
426     if ($entities_from_storage = $this->getFromStorage($ids)) {
427       $this->invokeStorageLoadHook($entities_from_storage);
428       $this->setPersistentCache($entities_from_storage);
429     }
430
431     return $entities_from_cache + $entities_from_storage;
432   }
433
434   /**
435    * Gets entities from the storage.
436    *
437    * @param array|null $ids
438    *   If not empty, return entities that match these IDs. Return all entities
439    *   when NULL.
440    *
441    * @return \Drupal\Core\Entity\ContentEntityInterface[]
442    *   Array of entities from the storage.
443    */
444   protected function getFromStorage(array $ids = NULL) {
445     $entities = [];
446
447     if (!empty($ids)) {
448       // Sanitize IDs. Before feeding ID array into buildQuery, check whether
449       // it is empty as this would load all entities.
450       $ids = $this->cleanIds($ids);
451     }
452
453     if ($ids === NULL || $ids) {
454       // Build and execute the query.
455       $query_result = $this->buildQuery($ids)->execute();
456       $records = $query_result->fetchAllAssoc($this->idKey);
457
458       // Map the loaded records into entity objects and according fields.
459       if ($records) {
460         $entities = $this->mapFromStorageRecords($records);
461       }
462     }
463
464     return $entities;
465   }
466
467   /**
468    * Maps from storage records to entity objects, and attaches fields.
469    *
470    * @param array $records
471    *   Associative array of query results, keyed on the entity ID or revision
472    *   ID.
473    * @param bool $load_from_revision
474    *   (optional) Flag to indicate whether revisions should be loaded or not.
475    *   Defaults to FALSE.
476    *
477    * @return array
478    *   An array of entity objects implementing the EntityInterface.
479    */
480   protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
481     if (!$records) {
482       return [];
483     }
484
485     $values = [];
486     foreach ($records as $id => $record) {
487       $values[$id] = [];
488       // Skip the item delta and item value levels (if possible) but let the
489       // field assign the value as suiting. This avoids unnecessary array
490       // hierarchies and saves memory here.
491       foreach ($record as $name => $value) {
492         // Handle columns named [field_name]__[column_name] (e.g for field types
493         // that store several properties).
494         if ($field_name = strstr($name, '__', TRUE)) {
495           $property_name = substr($name, strpos($name, '__') + 2);
496           $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
497         }
498         else {
499           // Handle columns named directly after the field (e.g if the field
500           // type only stores one property).
501           $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
502         }
503       }
504     }
505
506     // Initialize translations array.
507     $translations = array_fill_keys(array_keys($values), []);
508
509     // Load values from shared and dedicated tables.
510     $this->loadFromSharedTables($values, $translations, $load_from_revision);
511     $this->loadFromDedicatedTables($values, $load_from_revision);
512
513     $entities = [];
514     foreach ($values as $id => $entity_values) {
515       $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
516       // Turn the record into an entity class.
517       $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
518     }
519
520     return $entities;
521   }
522
523   /**
524    * Loads values for fields stored in the shared data tables.
525    *
526    * @param array &$values
527    *   Associative array of entities values, keyed on the entity ID or the
528    *   revision ID.
529    * @param array &$translations
530    *   List of translations, keyed on the entity ID.
531    * @param bool $load_from_revision
532    *   Flag to indicate whether revisions should be loaded or not.
533    */
534   protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
535     $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
536     if ($this->dataTable) {
537       // If a revision table is available, we need all the properties of the
538       // latest revision. Otherwise we fall back to the data table.
539       $table = $this->revisionDataTable ?: $this->dataTable;
540       $alias = $this->revisionDataTable ? 'revision' : 'data';
541       $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
542         ->fields($alias)
543         ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
544         ->orderBy($alias . '.' . $record_key);
545
546       $table_mapping = $this->getTableMapping();
547       if ($this->revisionDataTable) {
548         // Find revisioned fields that are not entity keys. Exclude the langcode
549         // key as the base table holds only the default language.
550         $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
551         $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
552
553         // Find fields that are not revisioned or entity keys. Data fields have
554         // the same value regardless of entity revision.
555         $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
556         // If there are no data fields then only revisioned fields are needed
557         // else both data fields and revisioned fields are needed to map the
558         // entity values.
559         $all_fields = $revisioned_fields;
560         if ($data_fields) {
561           $all_fields = array_merge($revisioned_fields, $data_fields);
562           $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
563           $column_names = [];
564           // Some fields can have more then one columns in the data table so
565           // column names are needed.
566           foreach ($data_fields as $data_field) {
567             // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
568             // returns an array keyed by property names so remove the keys
569             // before array_merge() to avoid losing data with fields having the
570             // same columns i.e. value.
571             $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
572           }
573           $query->fields('data', $column_names);
574         }
575
576         // Get the revision IDs.
577         $revision_ids = [];
578         foreach ($values as $entity_values) {
579           $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
580         }
581         $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
582       }
583       else {
584         $all_fields = $table_mapping->getFieldNames($this->dataTable);
585       }
586
587       $result = $query->execute();
588       foreach ($result as $row) {
589         $id = $row[$record_key];
590
591         // Field values in default language are stored with
592         // LanguageInterface::LANGCODE_DEFAULT as key.
593         $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
594
595         $translations[$id][$langcode] = TRUE;
596
597         foreach ($all_fields as $field_name) {
598           $columns = $table_mapping->getColumnNames($field_name);
599           // Do not key single-column fields by property name.
600           if (count($columns) == 1) {
601             $values[$id][$field_name][$langcode] = $row[reset($columns)];
602           }
603           else {
604             foreach ($columns as $property_name => $column_name) {
605               $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
606             }
607           }
608         }
609       }
610     }
611   }
612
613   /**
614    * {@inheritdoc}
615    */
616   protected function doLoadRevisionFieldItems($revision_id) {
617     @trigger_error('"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
618
619     $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
620
621     return !empty($revisions) ? reset($revisions) : NULL;
622   }
623
624   /**
625    * {@inheritdoc}
626    */
627   protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
628     $revisions = [];
629
630     // Sanitize IDs. Before feeding ID array into buildQuery, check whether
631     // it is empty as this would load all entity revisions.
632     $revision_ids = $this->cleanIds($revision_ids, 'revision');
633
634     if (!empty($revision_ids)) {
635       // Build and execute the query.
636       $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
637       $records = $query_result->fetchAllAssoc($this->revisionKey);
638
639       // Map the loaded records into entity objects and according fields.
640       if ($records) {
641         $revisions = $this->mapFromStorageRecords($records, TRUE);
642       }
643     }
644
645     return $revisions;
646   }
647
648   /**
649    * {@inheritdoc}
650    */
651   protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
652     $this->database->delete($this->revisionTable)
653       ->condition($this->revisionKey, $revision->getRevisionId())
654       ->execute();
655
656     if ($this->revisionDataTable) {
657       $this->database->delete($this->revisionDataTable)
658         ->condition($this->revisionKey, $revision->getRevisionId())
659         ->execute();
660     }
661
662     $this->deleteRevisionFromDedicatedTables($revision);
663   }
664
665   /**
666    * {@inheritdoc}
667    */
668   protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
669     if ($this->dataTable) {
670       // @todo We should not be using a condition to specify whether conditions
671       //   apply to the default language. See
672       //   https://www.drupal.org/node/1866330.
673       // Default to the original entity language if not explicitly specified
674       // otherwise.
675       if (!array_key_exists($this->defaultLangcodeKey, $values)) {
676         $values[$this->defaultLangcodeKey] = 1;
677       }
678       // If the 'default_langcode' flag is explicitly not set, we do not care
679       // whether the queried values are in the original entity language or not.
680       elseif ($values[$this->defaultLangcodeKey] === NULL) {
681         unset($values[$this->defaultLangcodeKey]);
682       }
683     }
684
685     parent::buildPropertyQuery($entity_query, $values);
686   }
687
688   /**
689    * Builds the query to load the entity.
690    *
691    * This has full revision support. For entities requiring special queries,
692    * the class can be extended, and the default query can be constructed by
693    * calling parent::buildQuery(). This is usually necessary when the object
694    * being loaded needs to be augmented with additional data from another
695    * table, such as loading node type into comments or vocabulary machine name
696    * into terms, however it can also support $conditions on different tables.
697    * See Drupal\comment\CommentStorage::buildQuery() for an example.
698    *
699    * @param array|null $ids
700    *   An array of entity IDs, or NULL to load all entities.
701    * @param array|bool $revision_ids
702    *   The IDs of the revisions to load, or FALSE if this query is asking for
703    *   the default revisions. Defaults to FALSE.
704    *
705    * @return \Drupal\Core\Database\Query\Select
706    *   A SelectQuery object for loading the entity.
707    */
708   protected function buildQuery($ids, $revision_ids = FALSE) {
709     $query = $this->database->select($this->entityType->getBaseTable(), 'base');
710
711     $query->addTag($this->entityTypeId . '_load_multiple');
712
713     if ($revision_ids) {
714       if (!is_array($revision_ids)) {
715         @trigger_error('Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
716       }
717       $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
718     }
719     elseif ($this->revisionTable) {
720       $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
721     }
722
723     // Add fields from the {entity} table.
724     $table_mapping = $this->getTableMapping();
725     $entity_fields = $table_mapping->getAllColumns($this->baseTable);
726
727     if ($this->revisionTable) {
728       // Add all fields from the {entity_revision} table.
729       $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
730       $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
731       // The ID field is provided by entity, so remove it.
732       unset($entity_revision_fields[$this->idKey]);
733
734       // Remove all fields from the base table that are also fields by the same
735       // name in the revision table.
736       $entity_field_keys = array_flip($entity_fields);
737       foreach ($entity_revision_fields as $name) {
738         if (isset($entity_field_keys[$name])) {
739           unset($entity_fields[$entity_field_keys[$name]]);
740         }
741       }
742       $query->fields('revision', $entity_revision_fields);
743
744       // Compare revision ID of the base and revision table, if equal then this
745       // is the default revision.
746       $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
747     }
748
749     $query->fields('base', $entity_fields);
750
751     if ($ids) {
752       $query->condition("base.{$this->idKey}", $ids, 'IN');
753     }
754
755     return $query;
756   }
757
758   /**
759    * {@inheritdoc}
760    */
761   public function delete(array $entities) {
762     if (!$entities) {
763       // If no IDs or invalid IDs were passed, do nothing.
764       return;
765     }
766
767     $transaction = $this->database->startTransaction();
768     try {
769       parent::delete($entities);
770
771       // Ignore replica server temporarily.
772       db_ignore_replica();
773     }
774     catch (\Exception $e) {
775       $transaction->rollBack();
776       watchdog_exception($this->entityTypeId, $e);
777       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
778     }
779   }
780
781   /**
782    * {@inheritdoc}
783    */
784   protected function doDeleteFieldItems($entities) {
785     $ids = array_keys($entities);
786
787     $this->database->delete($this->entityType->getBaseTable())
788       ->condition($this->idKey, $ids, 'IN')
789       ->execute();
790
791     if ($this->revisionTable) {
792       $this->database->delete($this->revisionTable)
793         ->condition($this->idKey, $ids, 'IN')
794         ->execute();
795     }
796
797     if ($this->dataTable) {
798       $this->database->delete($this->dataTable)
799         ->condition($this->idKey, $ids, 'IN')
800         ->execute();
801     }
802
803     if ($this->revisionDataTable) {
804       $this->database->delete($this->revisionDataTable)
805         ->condition($this->idKey, $ids, 'IN')
806         ->execute();
807     }
808
809     foreach ($entities as $entity) {
810       $this->deleteFromDedicatedTables($entity);
811     }
812   }
813
814   /**
815    * {@inheritdoc}
816    */
817   public function save(EntityInterface $entity) {
818     $transaction = $this->database->startTransaction();
819     try {
820       $return = parent::save($entity);
821
822       // Ignore replica server temporarily.
823       db_ignore_replica();
824       return $return;
825     }
826     catch (\Exception $e) {
827       $transaction->rollBack();
828       watchdog_exception($this->entityTypeId, $e);
829       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
830     }
831   }
832
833   /**
834    * {@inheritdoc}
835    */
836   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
837     $full_save = empty($names);
838     $update = !$full_save || !$entity->isNew();
839
840     if ($full_save) {
841       $shared_table_fields = TRUE;
842       $dedicated_table_fields = TRUE;
843     }
844     else {
845       $table_mapping = $this->getTableMapping();
846       $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
847       $shared_table_fields = FALSE;
848       $dedicated_table_fields = [];
849
850       // Collect the name of fields to be written in dedicated tables and check
851       // whether shared table records need to be updated.
852       foreach ($names as $name) {
853         $storage_definition = $storage_definitions[$name];
854         if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
855           $shared_table_fields = TRUE;
856         }
857         elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
858           $dedicated_table_fields[] = $name;
859         }
860       }
861     }
862
863     // Update shared table records if necessary.
864     if ($shared_table_fields) {
865       $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
866       // Create the storage record to be saved.
867       if ($update) {
868         $default_revision = $entity->isDefaultRevision();
869         if ($default_revision) {
870           $this->database
871             ->update($this->baseTable)
872             ->fields((array) $record)
873             ->condition($this->idKey, $record->{$this->idKey})
874             ->execute();
875         }
876         if ($this->revisionTable) {
877           if ($full_save) {
878             $entity->{$this->revisionKey} = $this->saveRevision($entity);
879           }
880           else {
881             $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
882             $entity->preSaveRevision($this, $record);
883             $this->database
884               ->update($this->revisionTable)
885               ->fields((array) $record)
886               ->condition($this->revisionKey, $record->{$this->revisionKey})
887               ->execute();
888           }
889         }
890         if ($default_revision && $this->dataTable) {
891           $this->saveToSharedTables($entity);
892         }
893         if ($this->revisionDataTable) {
894           $new_revision = $full_save && $entity->isNewRevision();
895           $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
896         }
897       }
898       else {
899         $insert_id = $this->database
900           ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
901           ->fields((array) $record)
902           ->execute();
903         // Even if this is a new entity the ID key might have been set, in which
904         // case we should not override the provided ID. An ID key that is not set
905         // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
906         if (!isset($record->{$this->idKey})) {
907           $record->{$this->idKey} = $insert_id;
908         }
909         $entity->{$this->idKey} = (string) $record->{$this->idKey};
910         if ($this->revisionTable) {
911           $record->{$this->revisionKey} = $this->saveRevision($entity);
912         }
913         if ($this->dataTable) {
914           $this->saveToSharedTables($entity);
915         }
916         if ($this->revisionDataTable) {
917           $this->saveToSharedTables($entity, $this->revisionDataTable);
918         }
919       }
920     }
921
922     // Update dedicated table records if necessary.
923     if ($dedicated_table_fields) {
924       $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
925       $this->saveToDedicatedTables($entity, $update, $names);
926     }
927   }
928
929   /**
930    * {@inheritdoc}
931    */
932   protected function has($id, EntityInterface $entity) {
933     return !$entity->isNew();
934   }
935
936   /**
937    * Saves fields that use the shared tables.
938    *
939    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
940    *   The entity object.
941    * @param string $table_name
942    *   (optional) The table name to save to. Defaults to the data table.
943    * @param bool $new_revision
944    *   (optional) Whether we are dealing with a new revision. By default fetches
945    *   the information from the entity object.
946    */
947   protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
948     if (!isset($table_name)) {
949       $table_name = $this->dataTable;
950     }
951     if (!isset($new_revision)) {
952       $new_revision = $entity->isNewRevision();
953     }
954     $revision = $table_name != $this->dataTable;
955
956     if (!$revision || !$new_revision) {
957       $key = $revision ? $this->revisionKey : $this->idKey;
958       $value = $revision ? $entity->getRevisionId() : $entity->id();
959       // Delete and insert to handle removed values.
960       $this->database->delete($table_name)
961         ->condition($key, $value)
962         ->execute();
963     }
964
965     $query = $this->database->insert($table_name);
966
967     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
968       $translation = $entity->getTranslation($langcode);
969       $record = $this->mapToDataStorageRecord($translation, $table_name);
970       $values = (array) $record;
971       $query
972         ->fields(array_keys($values))
973         ->values($values);
974     }
975
976     $query->execute();
977   }
978
979   /**
980    * Maps from an entity object to the storage record.
981    *
982    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
983    *   The entity object.
984    * @param string $table_name
985    *   (optional) The table name to map records to. Defaults to the base table.
986    *
987    * @return \stdClass
988    *   The record to store.
989    */
990   protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
991     if (!isset($table_name)) {
992       $table_name = $this->baseTable;
993     }
994
995     $record = new \stdClass();
996     $table_mapping = $this->getTableMapping();
997     foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
998
999       if (empty($this->getFieldStorageDefinitions()[$field_name])) {
1000         throw new EntityStorageException("Table mapping contains invalid field $field_name.");
1001       }
1002       $definition = $this->getFieldStorageDefinitions()[$field_name];
1003       $columns = $table_mapping->getColumnNames($field_name);
1004
1005       foreach ($columns as $column_name => $schema_name) {
1006         // If there is no main property and only a single column, get all
1007         // properties from the first field item and assume that they will be
1008         // stored serialized.
1009         // @todo Give field types more control over this behavior in
1010         //   https://www.drupal.org/node/2232427.
1011         if (!$definition->getMainPropertyName() && count($columns) == 1) {
1012           $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
1013         }
1014         else {
1015           $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
1016         }
1017         if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
1018           $value = serialize($value);
1019         }
1020
1021         // Do not set serial fields if we do not have a value. This supports all
1022         // SQL database drivers.
1023         // @see https://www.drupal.org/node/2279395
1024         $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
1025         if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
1026           $record->$schema_name = $value;
1027         }
1028       }
1029     }
1030
1031     return $record;
1032   }
1033
1034   /**
1035    * Checks whether a field column should be treated as serial.
1036    *
1037    * @param $table_name
1038    *   The name of the table the field column belongs to.
1039    * @param $schema_name
1040    *   The schema name of the field column.
1041    *
1042    * @return bool
1043    *   TRUE if the column is serial, FALSE otherwise.
1044    *
1045    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1046    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1047    */
1048   protected function isColumnSerial($table_name, $schema_name) {
1049     $result = FALSE;
1050
1051     switch ($table_name) {
1052       case $this->baseTable:
1053         $result = $schema_name == $this->idKey;
1054         break;
1055
1056       case $this->revisionTable:
1057         $result = $schema_name == $this->revisionKey;
1058         break;
1059     }
1060
1061     return $result;
1062   }
1063
1064   /**
1065    * Maps from an entity object to the storage record of the field data.
1066    *
1067    * @param \Drupal\Core\Entity\EntityInterface $entity
1068    *   The entity object.
1069    * @param string $table_name
1070    *   (optional) The table name to map records to. Defaults to the data table.
1071    *
1072    * @return \stdClass
1073    *   The record to store.
1074    */
1075   protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1076     if (!isset($table_name)) {
1077       $table_name = $this->dataTable;
1078     }
1079     $record = $this->mapToStorageRecord($entity, $table_name);
1080     return $record;
1081   }
1082
1083   /**
1084    * Saves an entity revision.
1085    *
1086    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1087    *   The entity object.
1088    *
1089    * @return int
1090    *   The revision id.
1091    */
1092   protected function saveRevision(ContentEntityInterface $entity) {
1093     $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1094
1095     $entity->preSaveRevision($this, $record);
1096
1097     if ($entity->isNewRevision()) {
1098       $insert_id = $this->database
1099         ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1100         ->fields((array) $record)
1101         ->execute();
1102       // Even if this is a new revision, the revision ID key might have been
1103       // set in which case we should not override the provided revision ID.
1104       if (!isset($record->{$this->revisionKey})) {
1105         $record->{$this->revisionKey} = $insert_id;
1106       }
1107       if ($entity->isDefaultRevision()) {
1108         $this->database->update($this->entityType->getBaseTable())
1109           ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1110           ->condition($this->idKey, $record->{$this->idKey})
1111           ->execute();
1112       }
1113     }
1114     else {
1115       $this->database
1116         ->update($this->revisionTable)
1117         ->fields((array) $record)
1118         ->condition($this->revisionKey, $record->{$this->revisionKey})
1119         ->execute();
1120     }
1121
1122     // Make sure to update the new revision key for the entity.
1123     $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1124
1125     return $record->{$this->revisionKey};
1126   }
1127
1128   /**
1129    * {@inheritdoc}
1130    */
1131   protected function getQueryServiceName() {
1132     return 'entity.query.sql';
1133   }
1134
1135   /**
1136    * Loads values of fields stored in dedicated tables for a group of entities.
1137    *
1138    * @param array &$values
1139    *   An array of values keyed by entity ID.
1140    * @param bool $load_from_revision
1141    *   Flag to indicate whether revisions should be loaded or not.
1142    */
1143   protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1144     if (empty($values)) {
1145       return;
1146     }
1147
1148     // Collect entities ids, bundles and languages.
1149     $bundles = [];
1150     $ids = [];
1151     $default_langcodes = [];
1152     foreach ($values as $key => $entity_values) {
1153       $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
1154       $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
1155       if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
1156         $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
1157       }
1158     }
1159
1160     // Collect impacted fields.
1161     $storage_definitions = [];
1162     $definitions = [];
1163     $table_mapping = $this->getTableMapping();
1164     foreach ($bundles as $bundle => $v) {
1165       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
1166       foreach ($definitions[$bundle] as $field_name => $field_definition) {
1167         $storage_definition = $field_definition->getFieldStorageDefinition();
1168         if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1169           $storage_definitions[$field_name] = $storage_definition;
1170         }
1171       }
1172     }
1173
1174     // Load field data.
1175     $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
1176     foreach ($storage_definitions as $field_name => $storage_definition) {
1177       $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
1178
1179       // Ensure that only values having valid languages are retrieved. Since we
1180       // are loading values for multiple entities, we cannot limit the query to
1181       // the available translations.
1182       $results = $this->database->select($table, 't')
1183         ->fields('t')
1184         ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1185         ->condition('deleted', 0)
1186         ->condition('langcode', $langcodes, 'IN')
1187         ->orderBy('delta')
1188         ->execute();
1189
1190       foreach ($results as $row) {
1191         $bundle = $row->bundle;
1192
1193         $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
1194         // Field values in default language are stored with
1195         // LanguageInterface::LANGCODE_DEFAULT as key.
1196         $langcode = LanguageInterface::LANGCODE_DEFAULT;
1197         if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
1198           $langcode = $row->langcode;
1199         }
1200
1201         if (!isset($values[$value_key][$field_name][$langcode])) {
1202           $values[$value_key][$field_name][$langcode] = [];
1203         }
1204
1205         // Ensure that records for non-translatable fields having invalid
1206         // languages are skipped.
1207         if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
1208           if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
1209             $item = [];
1210             // For each column declared by the field, populate the item from the
1211             // prefixed database column.
1212             foreach ($storage_definition->getColumns() as $column => $attributes) {
1213               $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1214               // Unserialize the value if specified in the column schema.
1215               $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
1216             }
1217
1218             // Add the item to the field values for the entity.
1219             $values[$value_key][$field_name][$langcode][] = $item;
1220           }
1221         }
1222       }
1223     }
1224   }
1225
1226   /**
1227    * Saves values of fields that use dedicated tables.
1228    *
1229    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1230    *   The entity.
1231    * @param bool $update
1232    *   TRUE if the entity is being updated, FALSE if it is being inserted.
1233    * @param string[] $names
1234    *   (optional) The names of the fields to be stored. Defaults to all the
1235    *   available fields.
1236    */
1237   protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
1238     $vid = $entity->getRevisionId();
1239     $id = $entity->id();
1240     $bundle = $entity->bundle();
1241     $entity_type = $entity->getEntityTypeId();
1242     $default_langcode = $entity->getUntranslated()->language()->getId();
1243     $translation_langcodes = array_keys($entity->getTranslationLanguages());
1244     $table_mapping = $this->getTableMapping();
1245
1246     if (!isset($vid)) {
1247       $vid = $id;
1248     }
1249
1250     $original = !empty($entity->original) ? $entity->original : NULL;
1251
1252     // Determine which fields should be actually stored.
1253     $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1254     if ($names) {
1255       $definitions = array_intersect_key($definitions, array_flip($names));
1256     }
1257
1258     foreach ($definitions as $field_name => $field_definition) {
1259       $storage_definition = $field_definition->getFieldStorageDefinition();
1260       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1261         continue;
1262       }
1263
1264       // When updating an existing revision, keep the existing records if the
1265       // field values did not change.
1266       if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
1267         continue;
1268       }
1269
1270       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1271       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1272
1273       // Delete and insert, rather than update, in case a value was added.
1274       if ($update) {
1275         // Only overwrite the field's base table if saving the default revision
1276         // of an entity.
1277         if ($entity->isDefaultRevision()) {
1278           $this->database->delete($table_name)
1279             ->condition('entity_id', $id)
1280             ->execute();
1281         }
1282         if ($this->entityType->isRevisionable()) {
1283           $this->database->delete($revision_name)
1284             ->condition('entity_id', $id)
1285             ->condition('revision_id', $vid)
1286             ->execute();
1287         }
1288       }
1289
1290       // Prepare the multi-insert query.
1291       $do_insert = FALSE;
1292       $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
1293       foreach ($storage_definition->getColumns() as $column => $attributes) {
1294         $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
1295       }
1296       $query = $this->database->insert($table_name)->fields($columns);
1297       if ($this->entityType->isRevisionable()) {
1298         $revision_query = $this->database->insert($revision_name)->fields($columns);
1299       }
1300
1301       $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1302       foreach ($langcodes as $langcode) {
1303         $delta_count = 0;
1304         $items = $entity->getTranslation($langcode)->get($field_name);
1305         $items->filterEmptyItems();
1306         foreach ($items as $delta => $item) {
1307           // We now know we have something to insert.
1308           $do_insert = TRUE;
1309           $record = [
1310             'entity_id' => $id,
1311             'revision_id' => $vid,
1312             'bundle' => $bundle,
1313             'delta' => $delta,
1314             'langcode' => $langcode,
1315           ];
1316           foreach ($storage_definition->getColumns() as $column => $attributes) {
1317             $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1318             // Serialize the value if specified in the column schema.
1319             $value = $item->$column;
1320             if (!empty($attributes['serialize'])) {
1321               $value = serialize($value);
1322             }
1323             $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1324           }
1325           $query->values($record);
1326           if ($this->entityType->isRevisionable()) {
1327             $revision_query->values($record);
1328           }
1329
1330           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1331             break;
1332           }
1333         }
1334       }
1335
1336       // Execute the query if we have values to insert.
1337       if ($do_insert) {
1338         // Only overwrite the field's base table if saving the default revision
1339         // of an entity.
1340         if ($entity->isDefaultRevision()) {
1341           $query->execute();
1342         }
1343         if ($this->entityType->isRevisionable()) {
1344           $revision_query->execute();
1345         }
1346       }
1347     }
1348   }
1349
1350   /**
1351    * Deletes values of fields in dedicated tables for all revisions.
1352    *
1353    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1354    *   The entity.
1355    */
1356   protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
1357     $table_mapping = $this->getTableMapping();
1358     foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1359       $storage_definition = $field_definition->getFieldStorageDefinition();
1360       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1361         continue;
1362       }
1363       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1364       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1365       $this->database->delete($table_name)
1366         ->condition('entity_id', $entity->id())
1367         ->execute();
1368       if ($this->entityType->isRevisionable()) {
1369         $this->database->delete($revision_name)
1370           ->condition('entity_id', $entity->id())
1371           ->execute();
1372       }
1373     }
1374   }
1375
1376   /**
1377    * Deletes values of fields in dedicated tables for all revisions.
1378    *
1379    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1380    *   The entity. It must have a revision ID.
1381    */
1382   protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1383     $vid = $entity->getRevisionId();
1384     if (isset($vid)) {
1385       $table_mapping = $this->getTableMapping();
1386       foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1387         $storage_definition = $field_definition->getFieldStorageDefinition();
1388         if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1389           continue;
1390         }
1391         $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1392         $this->database->delete($revision_name)
1393           ->condition('entity_id', $entity->id())
1394           ->condition('revision_id', $vid)
1395           ->execute();
1396       }
1397     }
1398   }
1399
1400   /**
1401    * {@inheritdoc}
1402    */
1403   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1404     return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1405   }
1406
1407   /**
1408    * {@inheritdoc}
1409    */
1410   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1411     return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1412   }
1413
1414   /**
1415    * {@inheritdoc}
1416    */
1417   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1418     return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1419   }
1420
1421   /**
1422    * {@inheritdoc}
1423    */
1424   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1425     return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1426   }
1427
1428   /**
1429    * {@inheritdoc}
1430    */
1431   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1432     $this->wrapSchemaException(function () use ($entity_type) {
1433       $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1434     });
1435   }
1436
1437   /**
1438    * {@inheritdoc}
1439    */
1440   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1441     // Ensure we have an updated entity type definition.
1442     $this->entityType = $entity_type;
1443     // The table layout may have changed depending on the new entity type
1444     // definition.
1445     $this->initTableLayout();
1446     // Let the schema handler adapt to possible table layout changes.
1447     $this->wrapSchemaException(function () use ($entity_type, $original) {
1448       $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
1449     });
1450   }
1451
1452   /**
1453    * {@inheritdoc}
1454    */
1455   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1456     $this->wrapSchemaException(function () use ($entity_type) {
1457       $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1458     });
1459   }
1460
1461   /**
1462    * {@inheritdoc}
1463    */
1464   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1465     // If we are adding a field stored in a shared table we need to recompute
1466     // the table mapping.
1467     // @todo This does not belong here. Remove it once we are able to generate a
1468     //   fresh table mapping in the schema handler. See
1469     //   https://www.drupal.org/node/2274017.
1470     if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
1471       $this->tableMapping = NULL;
1472     }
1473     $this->wrapSchemaException(function () use ($storage_definition) {
1474       $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1475     });
1476   }
1477
1478   /**
1479    * {@inheritdoc}
1480    */
1481   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1482     $this->wrapSchemaException(function () use ($storage_definition, $original) {
1483       $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1484     });
1485   }
1486
1487   /**
1488    * {@inheritdoc}
1489    */
1490   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1491     $table_mapping = $this->getTableMapping(
1492       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1493     );
1494
1495     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1496       // Mark all data associated with the field for deletion.
1497       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1498       $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1499       $this->database->update($table)
1500         ->fields(['deleted' => 1])
1501         ->execute();
1502       if ($this->entityType->isRevisionable()) {
1503         $this->database->update($revision_table)
1504           ->fields(['deleted' => 1])
1505           ->execute();
1506       }
1507     }
1508
1509     // Update the field schema.
1510     $this->wrapSchemaException(function () use ($storage_definition) {
1511       $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1512     });
1513   }
1514
1515   /**
1516    * Wraps a database schema exception into an entity storage exception.
1517    *
1518    * @param callable $callback
1519    *   The callback to be executed.
1520    *
1521    * @throws \Drupal\Core\Entity\EntityStorageException
1522    *   When a database schema exception is thrown.
1523    */
1524   protected function wrapSchemaException(callable $callback) {
1525     $message = 'Exception thrown while performing a schema update.';
1526     try {
1527       $callback();
1528     }
1529     catch (SchemaException $e) {
1530       $message .= ' ' . $e->getMessage();
1531       throw new EntityStorageException($message, 0, $e);
1532     }
1533     catch (DatabaseExceptionWrapper $e) {
1534       $message .= ' ' . $e->getMessage();
1535       throw new EntityStorageException($message, 0, $e);
1536     }
1537   }
1538
1539   /**
1540    * {@inheritdoc}
1541    */
1542   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
1543     $table_mapping = $this->getTableMapping();
1544     $storage_definition = $field_definition->getFieldStorageDefinition();
1545     // Mark field data as deleted.
1546     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1547       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1548       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1549       $this->database->update($table_name)
1550         ->fields(['deleted' => 1])
1551         ->condition('bundle', $field_definition->getTargetBundle())
1552         ->execute();
1553       if ($this->entityType->isRevisionable()) {
1554         $this->database->update($revision_name)
1555           ->fields(['deleted' => 1])
1556           ->condition('bundle', $field_definition->getTargetBundle())
1557           ->execute();
1558       }
1559     }
1560   }
1561
1562   /**
1563    * {@inheritdoc}
1564    */
1565   public function onBundleCreate($bundle, $entity_type_id) {}
1566
1567   /**
1568    * {@inheritdoc}
1569    */
1570   public function onBundleDelete($bundle, $entity_type_id) {}
1571
1572   /**
1573    * {@inheritdoc}
1574    */
1575   protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1576     // Check whether the whole field storage definition is gone, or just some
1577     // bundle fields.
1578     $storage_definition = $field_definition->getFieldStorageDefinition();
1579     $table_mapping = $this->getTableMapping();
1580     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1581
1582     // Get the entities which we want to purge first.
1583     $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
1584     $or = $entity_query->orConditionGroup();
1585     foreach ($storage_definition->getColumns() as $column_name => $data) {
1586       $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1587     }
1588     $entity_query
1589       ->distinct(TRUE)
1590       ->fields('t', ['entity_id'])
1591       ->condition('bundle', $field_definition->getTargetBundle())
1592       ->range(0, $batch_size);
1593
1594     // Create a map of field data table column names to field column names.
1595     $column_map = [];
1596     foreach ($storage_definition->getColumns() as $column_name => $data) {
1597       $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1598     }
1599
1600     $entities = [];
1601     $items_by_entity = [];
1602     foreach ($entity_query->execute() as $row) {
1603       $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1604         ->fields('t')
1605         ->condition('entity_id', $row['entity_id'])
1606         ->condition('deleted', 1)
1607         ->orderBy('delta');
1608
1609       foreach ($item_query->execute() as $item_row) {
1610         if (!isset($entities[$item_row['revision_id']])) {
1611           // Create entity with the right revision id and entity id combination.
1612           $item_row['entity_type'] = $this->entityTypeId;
1613           // @todo: Replace this by an entity object created via an entity
1614           // factory, see https://www.drupal.org/node/1867228.
1615           $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
1616         }
1617         $item = [];
1618         foreach ($column_map as $db_column => $field_column) {
1619           $item[$field_column] = $item_row[$db_column];
1620         }
1621         $items_by_entity[$item_row['revision_id']][] = $item;
1622       }
1623     }
1624
1625     // Create field item objects and return.
1626     foreach ($items_by_entity as $revision_id => $values) {
1627       $entity_adapter = $entities[$revision_id]->getTypedData();
1628       $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
1629     }
1630     return $items_by_entity;
1631   }
1632
1633   /**
1634    * {@inheritdoc}
1635    */
1636   protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
1637     $storage_definition = $field_definition->getFieldStorageDefinition();
1638     $is_deleted = $storage_definition->isDeleted();
1639     $table_mapping = $this->getTableMapping();
1640     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1641     $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1642     $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
1643     $this->database->delete($table_name)
1644       ->condition('revision_id', $revision_id)
1645       ->condition('deleted', 1)
1646       ->execute();
1647     if ($this->entityType->isRevisionable()) {
1648       $this->database->delete($revision_name)
1649         ->condition('revision_id', $revision_id)
1650         ->condition('deleted', 1)
1651         ->execute();
1652     }
1653   }
1654
1655   /**
1656    * {@inheritdoc}
1657    */
1658   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1659     $this->getStorageSchema()->finalizePurge($storage_definition);
1660   }
1661
1662   /**
1663    * {@inheritdoc}
1664    */
1665   public function countFieldData($storage_definition, $as_bool = FALSE) {
1666     // The table mapping contains stale data during a request when a field
1667     // storage definition is added, so bypass the internal storage definitions
1668     // and fetch the table mapping using the passed in storage definition.
1669     // @todo Fix this in https://www.drupal.org/node/2705205.
1670     $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
1671     $storage_definitions[$storage_definition->getName()] = $storage_definition;
1672     $table_mapping = $this->getTableMapping($storage_definitions);
1673
1674     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1675       $is_deleted = $storage_definition->isDeleted();
1676       if ($this->entityType->isRevisionable()) {
1677         $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1678       }
1679       else {
1680         $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1681       }
1682       $query = $this->database->select($table_name, 't');
1683       $or = $query->orConditionGroup();
1684       foreach ($storage_definition->getColumns() as $column_name => $data) {
1685         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1686       }
1687       $query->condition($or);
1688       if (!$as_bool) {
1689         $query
1690           ->fields('t', ['entity_id'])
1691           ->distinct(TRUE);
1692       }
1693     }
1694     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1695       // Ascertain the table this field is mapped too.
1696       $field_name = $storage_definition->getName();
1697       try {
1698         $table_name = $table_mapping->getFieldTableName($field_name);
1699       }
1700       catch (SqlContentEntityStorageException $e) {
1701         // This may happen when changing field storage schema, since we are not
1702         // able to use a table mapping matching the passed storage definition.
1703         // @todo Revisit this once we are able to instantiate the table mapping
1704         //   properly. See https://www.drupal.org/node/2274017.
1705         $table_name = $this->dataTable ?: $this->baseTable;
1706       }
1707       $query = $this->database->select($table_name, 't');
1708       $or = $query->orConditionGroup();
1709       foreach (array_keys($storage_definition->getColumns()) as $property_name) {
1710         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
1711       }
1712       $query->condition($or);
1713       if (!$as_bool) {
1714         $query
1715           ->fields('t', [$this->idKey])
1716           ->distinct(TRUE);
1717       }
1718     }
1719
1720     // @todo Find a way to count field data also for fields having custom
1721     //   storage. See https://www.drupal.org/node/2337753.
1722     $count = 0;
1723     if (isset($query)) {
1724       // If we are performing the query just to check if the field has data
1725       // limit the number of rows.
1726       if ($as_bool) {
1727         $query
1728           ->range(0, 1)
1729           ->addExpression('1');
1730       }
1731       else {
1732         // Otherwise count the number of rows.
1733         $query = $query->countQuery();
1734       }
1735       $count = $query->execute()->fetchField();
1736     }
1737     return $as_bool ? (bool) $count : (int) $count;
1738   }
1739
1740   /**
1741    * Determines whether the passed field has been already deleted.
1742    *
1743    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1744    *   The field storage definition.
1745    *
1746    * @return bool
1747    *   Whether the field has been already deleted.
1748    *
1749    * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
1750    *   \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
1751    *
1752    * @see https://www.drupal.org/node/2907785
1753    */
1754   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1755     return $storage_definition->isDeleted();
1756   }
1757
1758 }