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