SqlContentEntityStorage.php 64.4 KB
Newer Older
1
2
<?php

3
namespace Drupal\Core\Entity\Sql;
4

5
use Drupal\Core\Cache\CacheBackendInterface;
6
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7
use Drupal\Core\Database\Connection;
8
use Drupal\Core\Database\Database;
9
10
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
11
12
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
13
use Drupal\Core\Entity\ContentEntityTypeInterface;
14
use Drupal\Core\Entity\EntityBundleListenerInterface;
15
use Drupal\Core\Entity\EntityFieldManagerInterface;
16
use Drupal\Core\Entity\EntityInterface;
17
18
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
19
20
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
21
use Drupal\Core\Entity\Query\QueryInterface;
22
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
23
use Drupal\Core\Field\FieldDefinitionInterface;
24
use Drupal\Core\Field\FieldStorageDefinitionInterface;
25
use Drupal\Core\Language\LanguageInterface;
26
use Drupal\Core\Language\LanguageManagerInterface;
27
28
29
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
30
 * A content entity database storage implementation.
31
 *
32
 * This class can be used as-is by most content entity types. Entity types
33
 * requiring special handling can extend the class.
34
 *
35
 * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
36
 * internally in order to automatically generate the database schema based on
37
38
 * the defined base fields. Entity types can override the schema handler to
 * customize the generated schema; e.g., to add additional indexes.
39
40
 *
 * @ingroup entity_api
41
 */
42
class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
43

44
45
46
47
48
49
50
  /**
   * The entity type's field storage definitions.
   *
   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
   */
  protected $fieldStorageDefinitions;

51
52
53
54
55
56
  /**
   * The mapping of field columns to SQL tables.
   *
   * @var \Drupal\Core\Entity\Sql\TableMappingInterface
   */
  protected $tableMapping;
57
58
59
60
61
62
63
64
65
66

  /**
   * Name of entity's revision database table field, if it supports revisions.
   *
   * Has the value FALSE if this entity does not use revisions.
   *
   * @var string
   */
  protected $revisionKey = FALSE;

67
68
69
70
71
72
73
  /**
   * The entity langcode key.
   *
   * @var string|bool
   */
  protected $langcodeKey = FALSE;

74
75
76
77
78
79
80
  /**
   * The default language entity key.
   *
   * @var string
   */
  protected $defaultLangcodeKey = FALSE;

81
82
83
84
85
86
87
  /**
   * The base table of the entity.
   *
   * @var string
   */
  protected $baseTable;

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
  /**
   * The table that stores revisions, if the entity supports revisions.
   *
   * @var string
   */
  protected $revisionTable;

  /**
   * The table that stores properties, if the entity has multilingual support.
   *
   * @var string
   */
  protected $dataTable;

  /**
   * The table that stores revision field data if the entity supports revisions.
   *
   * @var string
   */
  protected $revisionDataTable;

  /**
   * Active database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

116
  /**
117
   * The entity type's storage schema object.
118
   *
119
   * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
120
   */
121
  protected $storageSchema;
122

123
124
125
126
127
128
129
  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

130
131
132
133
134
135
136
  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

137
138
139
140
141
142
143
  /**
   * Whether this storage should use the temporary table mapping.
   *
   * @var bool
   */
  protected $temporary = FALSE;

144
145
146
  /**
   * {@inheritdoc}
   */
147
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
148
    return new static(
149
      $entity_type,
150
      $container->get('database'),
151
      $container->get('entity_field.manager'),
152
      $container->get('cache.entity'),
153
      $container->get('language_manager'),
154
155
      $container->get('entity.memory_cache'),
      $container->get('entity_type.bundle.info'),
156
      $container->get('entity_type.manager')
157
158
159
160
    );
  }

  /**
161
   * Constructs a SqlContentEntityStorage object.
162
   *
163
164
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
165
166
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
167
168
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
169
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
170
   *   The cache backend to be used.
171
172
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
173
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
174
   *   The memory cache backend to be used.
175
176
177
178
179
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
180
  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager) {
181
    parent::__construct($entity_type, $entity_field_manager, $cache, $memory_cache, $entity_type_bundle_info);
182
    $this->database = $database;
183
    $this->languageManager = $language_manager;
184
    $this->entityTypeManager = $entity_type_manager;
185
186
    $this->entityType = $this->entityTypeManager->getActiveDefinition($entity_type->id());
    $this->fieldStorageDefinitions = $this->entityFieldManager->getActiveFieldStorageDefinitions($entity_type->id());
187

188
189
190
191
192
193
194
195
196
197
198
199
200
201
    $this->initTableLayout();
  }

  /**
   * Initializes table name variables.
   */
  protected function initTableLayout() {
    // Reset table field values to ensure changes in the entity type definition
    // are correctly reflected in the table layout.
    $this->tableMapping = NULL;
    $this->revisionKey = NULL;
    $this->revisionTable = NULL;
    $this->dataTable = NULL;
    $this->revisionDataTable = NULL;
202

203
204
    $table_mapping = $this->getTableMapping();
    $this->baseTable = $table_mapping->getBaseTable();
205
206
207
    $revisionable = $this->entityType->isRevisionable();
    if ($revisionable) {
      $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
208
      $this->revisionTable = $table_mapping->getRevisionTable();
209
    }
210
    $translatable = $this->entityType->isTranslatable();
211
    if ($translatable) {
212
      $this->dataTable = $table_mapping->getDataTable();
213
      $this->langcodeKey = $this->entityType->getKey('langcode');
214
      $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
215
216
    }
    if ($revisionable && $translatable) {
217
      $this->revisionDataTable = $table_mapping->getRevisionDataTable();
218
219
220
221
    }
  }

  /**
222
   * Gets the base table name.
223
224
225
226
227
228
229
230
231
   *
   * @return string
   *   The table name.
   */
  public function getBaseTable() {
    return $this->baseTable;
  }

  /**
232
   * Gets the revision table name.
233
234
235
236
237
238
239
240
241
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionTable() {
    return $this->revisionTable;
  }

  /**
242
   * Gets the data table name.
243
244
245
246
247
248
249
250
251
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getDataTable() {
    return $this->dataTable;
  }

  /**
252
   * Gets the revision data table name.
253
254
255
256
257
258
259
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionDataTable() {
    return $this->revisionDataTable;
  }
260

261
  /**
262
   * Gets the entity type's storage schema object.
263
   *
264
   * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
265
   *   The schema object.
266
   */
267
268
269
  protected function getStorageSchema() {
    if (!isset($this->storageSchema)) {
      $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
270
      $this->storageSchema = new $class($this->entityTypeManager, $this->entityType, $this, $this->database, $this->entityFieldManager);
271
    }
272
    return $this->storageSchema;
273
  }
274

275
  /**
276
277
278
279
280
   * Updates the wrapped entity type definition.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The update entity type.
   *
281
282
   * @internal Only to be used internally by Entity API. Expected to be
   *   removed by https://www.drupal.org/node/2274017.
283
   */
284
285
286
287
288
289
  public function setEntityType(EntityTypeInterface $entity_type) {
    if ($this->entityType->id() == $entity_type->id()) {
      $this->entityType = $entity_type;
      $this->initTableLayout();
    }
    else {
290
      throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
291
292
    }
  }
293

294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
  /**
   * Updates the internal list of field storage definitions.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
   *   An array of field storage definitions.
   *
   * @internal Only to be used internally by Entity API.
   */
  public function setFieldStorageDefinitions(array $field_storage_definitions) {
    foreach ($field_storage_definitions as $field_storage_definition) {
      if ($field_storage_definition->getTargetEntityTypeId() !== $this->entityType->id()) {
        throw new EntityStorageException("Unsupported entity type {$field_storage_definition->getTargetEntityTypeId()}");
      }
    }

    $this->fieldStorageDefinitions = $field_storage_definitions;
  }

312
313
314
315
316
317
318
319
320
321
322
  /**
   * Sets the wrapped table mapping definition.
   *
   * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
   *   The table mapping.
   *
   * @internal Only to be used internally by Entity API. Expected to be removed
   *   by https://www.drupal.org/node/2554235.
   */
  public function setTableMapping(TableMappingInterface $table_mapping) {
    $this->tableMapping = $table_mapping;
323
324
325
326
327

    $this->baseTable = $table_mapping->getBaseTable();
    $this->revisionTable = $table_mapping->getRevisionTable();
    $this->dataTable = $table_mapping->getDataTable();
    $this->revisionDataTable = $table_mapping->getRevisionDataTable();
328
329
330
331
332
333
334
335
336
337
338
339
340
341
  }

  /**
   * Changes the temporary state of the storage.
   *
   * @param bool $temporary
   *   Whether to use a temporary table mapping or not.
   *
   * @internal Only to be used internally by Entity API.
   */
  public function setTemporary($temporary) {
    $this->temporary = $temporary;
  }

342
343
344
345
  /**
   * {@inheritdoc}
   */
  public function getTableMapping(array $storage_definitions = NULL) {
346
347
348
349
350
351
    // If a new set of field storage definitions is passed, for instance when
    // comparing old and new storage schema, we compute the table mapping
    // without caching.
    if ($storage_definitions) {
      return $this->getCustomTableMapping($this->entityType, $storage_definitions);
    }
352
353

    // If we are using our internal storage definitions, which is our main use
354
355
    // case, we can statically cache the computed table mapping.
    if (!isset($this->tableMapping)) {
356
      $this->tableMapping = $this->getCustomTableMapping($this->entityType, $this->fieldStorageDefinitions);
357
    }
358

359
360
361
362
363
364
365
366
367
368
369
    return $this->tableMapping;
  }

  /**
   * Gets a table mapping for the specified entity type and storage definitions.
   *
   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
   *   An entity type definition.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
   *   An array of field storage definitions to be used to compute the table
   *   mapping.
370
371
372
   * @param string $prefix
   *   (optional) A prefix to be used by all the tables of this mapping.
   *   Defaults to an empty string.
373
374
375
376
377
378
   *
   * @return \Drupal\Core\Entity\Sql\TableMappingInterface
   *   A table mapping object for the entity's tables.
   *
   * @internal
   */
379
380
381
  public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
    $prefix = $prefix ?: ($this->temporary ? 'tmp_' : '');
    return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix);
382
383
384
385
386
  }

  /**
   * {@inheritdoc}
   */
387
  protected function doLoadMultiple(array $ids = NULL) {
388
389
390
391
392
    // Attempt to load entities from the persistent cache. This will remove IDs
    // that were loaded from $ids.
    $entities_from_cache = $this->getFromPersistentCache($ids);

    // Load any remaining entities from the database.
393
394
395
396
    if ($entities_from_storage = $this->getFromStorage($ids)) {
      $this->invokeStorageLoadHook($entities_from_storage);
      $this->setPersistentCache($entities_from_storage);
    }
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411

    return $entities_from_cache + $entities_from_storage;
  }

  /**
   * Gets entities from the storage.
   *
   * @param array|null $ids
   *   If not empty, return entities that match these IDs. Return all entities
   *   when NULL.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   Array of entities from the storage.
   */
  protected function getFromStorage(array $ids = NULL) {
412
    $entities = [];
413

414
415
416
417
418
419
    if (!empty($ids)) {
      // Sanitize IDs. Before feeding ID array into buildQuery, check whether
      // it is empty as this would load all entities.
      $ids = $this->cleanIds($ids);
    }

420
421
422
423
424
425
426
427
428
429
430
431
432
433
    if ($ids === NULL || $ids) {
      // Build and execute the query.
      $query_result = $this->buildQuery($ids)->execute();
      $records = $query_result->fetchAllAssoc($this->idKey);

      // Map the loaded records into entity objects and according fields.
      if ($records) {
        $entities = $this->mapFromStorageRecords($records);
      }
    }

    return $entities;
  }

434
  /**
435
   * Maps from storage records to entity objects, and attaches fields.
436
   *
437
   * @param array $records
438
439
   *   Associative array of query results, keyed on the entity ID or revision
   *   ID.
440
   * @param bool $load_from_revision
441
442
   *   (optional) Flag to indicate whether revisions should be loaded or not.
   *   Defaults to FALSE.
443
444
445
446
   *
   * @return array
   *   An array of entity objects implementing the EntityInterface.
   */
447
  protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
448
    if (!$records) {
449
      return [];
450
451
    }

452
453
454
455
456
457
458
459
    // Get the names of the fields that are stored in the base table and, if
    // applicable, the revision table. Other entity data will be loaded in
    // loadFromSharedTables() and loadFromDedicatedTables().
    $field_names = $this->tableMapping->getFieldNames($this->baseTable);
    if ($this->revisionTable) {
      $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable)));
    }

460
    $values = [];
461
    foreach ($records as $id => $record) {
462
      $values[$id] = [];
463
464
465
      // Skip the item delta and item value levels (if possible) but let the
      // field assign the value as suiting. This avoids unnecessary array
      // hierarchies and saves memory here.
466
467
468
469
      foreach ($field_names as $field_name) {
        $field_columns = $this->tableMapping->getColumnNames($field_name);
        // Handle field types that store several properties.
        if (count($field_columns) > 1) {
470
          $definition_columns = $this->fieldStorageDefinitions[$field_name]->getColumns();
471
472
          foreach ($field_columns as $property_name => $column_name) {
            if (property_exists($record, $column_name)) {
473
              $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = !empty($definition_columns[$property_name]['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name};
474
475
476
              unset($record->{$column_name});
            }
          }
477
        }
478
        // Handle field types that store only one property.
479
        else {
480
481
          $column_name = reset($field_columns);
          if (property_exists($record, $column_name)) {
482
            $columns = $this->fieldStorageDefinitions[$field_name]->getColumns();
483
484
            $column = reset($columns);
            $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = !empty($column['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name};
485
486
            unset($record->{$column_name});
          }
487
        }
488
      }
489
490
491
492
493
494

      // Handle additional record entries that are not provided by an entity
      // field, such as 'isDefaultRevision'.
      foreach ($record as $name => $value) {
        $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
      }
495
    }
496

497
    // Initialize translations array.
498
    $translations = array_fill_keys(array_keys($values), []);
499
500

    // Load values from shared and dedicated tables.
501
    $this->loadFromSharedTables($values, $translations, $load_from_revision);
502
503
    $this->loadFromDedicatedTables($values, $load_from_revision);

504
    $entities = [];
505
506
507
508
509
    foreach ($values as $id => $entity_values) {
      $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
      // Turn the record into an entity class.
      $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
    }
510

511
512
513
514
    return $entities;
  }

  /**
515
   * Loads values for fields stored in the shared data tables.
516
   *
517
   * @param array &$values
518
519
   *   Associative array of entities values, keyed on the entity ID or the
   *   revision ID.
520
521
   * @param array &$translations
   *   List of translations, keyed on the entity ID.
522
523
   * @param bool $load_from_revision
   *   Flag to indicate whether revisions should be loaded or not.
524
   */
525
526
  protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
    $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
527
528
529
530
    if ($this->dataTable) {
      // If a revision table is available, we need all the properties of the
      // latest revision. Otherwise we fall back to the data table.
      $table = $this->revisionDataTable ?: $this->dataTable;
531
      $alias = $this->revisionDataTable ? 'revision' : 'data';
532
      $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
533
        ->fields($alias)
534
535
        ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
        ->orderBy($alias . '.' . $record_key);
536

537
      $table_mapping = $this->getTableMapping();
538
      if ($this->revisionDataTable) {
539
540
        // Find revisioned fields that are not entity keys. Exclude the langcode
        // key as the base table holds only the default language.
541
        $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
542
        $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
543
544
545

        // Find fields that are not revisioned or entity keys. Data fields have
        // the same value regardless of entity revision.
546
547
548
549
550
        $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
        // If there are no data fields then only revisioned fields are needed
        // else both data fields and revisioned fields are needed to map the
        // entity values.
        $all_fields = $revisioned_fields;
551
        if ($data_fields) {
552
          $all_fields = array_merge($revisioned_fields, $data_fields);
553
          $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
554
555
556
557
          $column_names = [];
          // Some fields can have more then one columns in the data table so
          // column names are needed.
          foreach ($data_fields as $data_field) {
558
            // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
559
560
561
562
563
564
            // returns an array keyed by property names so remove the keys
            // before array_merge() to avoid losing data with fields having the
            // same columns i.e. value.
            $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
          }
          $query->fields('data', $column_names);
565
566
        }

567
        // Get the revision IDs.
568
        $revision_ids = [];
569
570
        foreach ($values as $entity_values) {
          $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
571
        }
572
        $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
573
574
      }
      else {
575
        $all_fields = $table_mapping->getFieldNames($this->dataTable);
576
577
      }

578
579
      $result = $query->execute();
      foreach ($result as $row) {
580
        $id = $row[$record_key];
581
582

        // Field values in default language are stored with
583
        // LanguageInterface::LANGCODE_DEFAULT as key.
584
585
        $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;

586
587
        $translations[$id][$langcode] = TRUE;

588
        foreach ($all_fields as $field_name) {
589
          $storage_definition = $this->fieldStorageDefinitions[$field_name];
590
          $definition_columns = $storage_definition->getColumns();
591
592
593
          $columns = $table_mapping->getColumnNames($field_name);
          // Do not key single-column fields by property name.
          if (count($columns) == 1) {
594
595
596
            $column_name = reset($columns);
            $column_attributes = $definition_columns[key($columns)];
            $values[$id][$field_name][$langcode] = (!empty($column_attributes['serialize'])) ? unserialize($row[$column_name]) : $row[$column_name];
597
598
          }
          else {
599
            foreach ($columns as $property_name => $column_name) {
600
601
              $column_attributes = $definition_columns[$property_name];
              $values[$id][$field_name][$langcode][$property_name] = (!empty($column_attributes['serialize'])) ? unserialize($row[$column_name]) : $row[$column_name];
602
            }
603
604
605
606
607
608
          }
        }
      }
    }
  }

609
610
611
612
613
  /**
   * {@inheritdoc}
   */
  protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
    $revisions = [];
614

615
616
617
618
619
620
621
622
623
624
625
626
627
    // Sanitize IDs. Before feeding ID array into buildQuery, check whether
    // it is empty as this would load all entity revisions.
    $revision_ids = $this->cleanIds($revision_ids, 'revision');

    if (!empty($revision_ids)) {
      // Build and execute the query.
      $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
      $records = $query_result->fetchAllAssoc($this->revisionKey);

      // Map the loaded records into entity objects and according fields.
      if ($records) {
        $revisions = $this->mapFromStorageRecords($records, TRUE);
      }
628
    }
629

630
    return $revisions;
631
632
633
  }

  /**
634
   * {@inheritdoc}
635
   */
636
637
638
639
  protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
    $this->database->delete($this->revisionTable)
      ->condition($this->revisionKey, $revision->getRevisionId())
      ->execute();
640
641
642
643
644
645
646

    if ($this->revisionDataTable) {
      $this->database->delete($this->revisionDataTable)
        ->condition($this->revisionKey, $revision->getRevisionId())
        ->execute();
    }

647
    $this->deleteRevisionFromDedicatedTables($revision);
648
649
650
  }

  /**
651
   * {@inheritdoc}
652
653
654
655
   */
  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
    if ($this->dataTable) {
      // @todo We should not be using a condition to specify whether conditions
656
657
      //   apply to the default language. See
      //   https://www.drupal.org/node/1866330.
658
659
      // Default to the original entity language if not explicitly specified
      // otherwise.
660
661
      if (!array_key_exists($this->defaultLangcodeKey, $values)) {
        $values[$this->defaultLangcodeKey] = 1;
662
663
664
      }
      // If the 'default_langcode' flag is explicitly not set, we do not care
      // whether the queried values are in the original entity language or not.
665
666
      elseif ($values[$this->defaultLangcodeKey] === NULL) {
        unset($values[$this->defaultLangcodeKey]);
667
668
669
      }
    }

670
    parent::buildPropertyQuery($entity_query, $values);
671
672
673
674
675
676
677
678
679
  }

  /**
   * Builds the query to load the entity.
   *
   * This has full revision support. For entities requiring special queries,
   * the class can be extended, and the default query can be constructed by
   * calling parent::buildQuery(). This is usually necessary when the object
   * being loaded needs to be augmented with additional data from another
680
681
   * table, such as loading vocabulary machine name into terms, however it
   * can also support $conditions on different tables.
682
683
684
   *
   * @param array|null $ids
   *   An array of entity IDs, or NULL to load all entities.
685
686
687
   * @param array|bool $revision_ids
   *   The IDs of the revisions to load, or FALSE if this query is asking for
   *   the default revisions. Defaults to FALSE.
688
   *
689
   * @return \Drupal\Core\Database\Query\SelectInterface
690
691
   *   A SelectQuery object for loading the entity.
   */
692
  protected function buildQuery($ids, $revision_ids = FALSE) {
693
    $query = $this->database->select($this->baseTable, 'base');
694

695
    $query->addTag($this->entityTypeId . '_load_multiple');
696

697
    if ($revision_ids) {
698
      $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => $revision_ids]);
699
700
701
702
703
704
    }
    elseif ($this->revisionTable) {
      $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
    }

    // Add fields from the {entity} table.
705
706
    $table_mapping = $this->getTableMapping();
    $entity_fields = $table_mapping->getAllColumns($this->baseTable);
707
708
709

    if ($this->revisionTable) {
      // Add all fields from the {entity_revision} table.
710
      $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
711
      $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
      // The ID field is provided by entity, so remove it.
      unset($entity_revision_fields[$this->idKey]);

      // Remove all fields from the base table that are also fields by the same
      // name in the revision table.
      $entity_field_keys = array_flip($entity_fields);
      foreach ($entity_revision_fields as $name) {
        if (isset($entity_field_keys[$name])) {
          unset($entity_fields[$entity_field_keys[$name]]);
        }
      }
      $query->fields('revision', $entity_revision_fields);

      // Compare revision ID of the base and revision table, if equal then this
      // is the default revision.
727
      $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
728
729
730
731
732
733
734
735
736
737
738
739
    }

    $query->fields('base', $entity_fields);

    if ($ids) {
      $query->condition("base.{$this->idKey}", $ids, 'IN');
    }

    return $query;
  }

  /**
740
   * {@inheritdoc}
741
742
743
744
745
746
747
748
749
   */
  public function delete(array $entities) {
    if (!$entities) {
      // If no IDs or invalid IDs were passed, do nothing.
      return;
    }

    $transaction = $this->database->startTransaction();
    try {
750
      parent::delete($entities);
751

752
      // Ignore replica server temporarily.
753
      \Drupal::service('database.replica_kill_switch')->trigger();
754
755
    }
    catch (\Exception $e) {
756
      $transaction->rollBack();
757
      watchdog_exception($this->entityTypeId, $e);
758
759
760
761
762
763
764
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

  /**
   * {@inheritdoc}
   */
765
  protected function doDeleteFieldItems($entities) {
766
    $ids = array_keys($entities);
767

768
    $this->database->delete($this->baseTable)
769
      ->condition($this->idKey, $ids, 'IN')
770
      ->execute();
771

772
773
    if ($this->revisionTable) {
      $this->database->delete($this->revisionTable)
774
        ->condition($this->idKey, $ids, 'IN')
775
776
        ->execute();
    }
777

778
779
    if ($this->dataTable) {
      $this->database->delete($this->dataTable)
780
        ->condition($this->idKey, $ids, 'IN')
781
782
        ->execute();
    }
783

784
785
    if ($this->revisionDataTable) {
      $this->database->delete($this->revisionDataTable)
786
        ->condition($this->idKey, $ids, 'IN')
787
788
        ->execute();
    }
789

790
    foreach ($entities as $entity) {
791
      $this->deleteFromDedicatedTables($entity);
792
793
794
795
796
797
798
799
800
801
    }
  }

  /**
   * {@inheritdoc}
   */
  public function save(EntityInterface $entity) {
    $transaction = $this->database->startTransaction();
    try {
      $return = parent::save($entity);
802

803
      // Ignore replica server temporarily.
804
      \Drupal::service('database.replica_kill_switch')->trigger();
805
806
807
      return $return;
    }
    catch (\Exception $e) {
808
      $transaction->rollBack();
809
      watchdog_exception($this->entityTypeId, $e);
810
811
812
813
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

814
815
816
817
818
819
820
821
  /**
   * {@inheritdoc}
   */
  public function restore(EntityInterface $entity) {
    $transaction = $this->database->startTransaction();
    try {
      // Insert the entity data in the base and data tables only for default
      // revisions.
822
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
      if ($entity->isDefaultRevision()) {
        $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
        $this->database
          ->insert($this->baseTable)
          ->fields((array) $record)
          ->execute();

        if ($this->dataTable) {
          $this->saveToSharedTables($entity);
        }
      }

      // Insert the entity data in the revision and revision data tables.
      if ($this->revisionTable) {
        $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
        $this->database
          ->insert($this->revisionTable)
          ->fields((array) $record)
          ->execute();

        if ($this->revisionDataTable) {
          $this->saveToSharedTables($entity, $this->revisionDataTable);
        }
      }

      // Insert the entity data in the dedicated tables.
      $this->saveToDedicatedTables($entity, FALSE, []);

      // Ignore replica server temporarily.
      \Drupal::service('database.replica_kill_switch')->trigger();
    }
    catch (\Exception $e) {
      $transaction->rollBack();
      watchdog_exception($this->entityTypeId, $e);
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

861
862
863
  /**
   * {@inheritdoc}
   */
864
865
866
  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
    $full_save = empty($names);
    $update = !$full_save || !$entity->isNew();
867

868
869
870
    if ($full_save) {
      $shared_table_fields = TRUE;
      $dedicated_table_fields = TRUE;
871
872
    }
    else {
873
874
875
876
877
878
879
      $table_mapping = $this->getTableMapping();
      $shared_table_fields = FALSE;
      $dedicated_table_fields = [];

      // Collect the name of fields to be written in dedicated tables and check
      // whether shared table records need to be updated.
      foreach ($names as $name) {
880
        $storage_definition = $this->fieldStorageDefinitions[$name];
881
882
883
884
885
886
        if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
          $shared_table_fields = TRUE;
        }
        elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
          $dedicated_table_fields[] = $name;
        }
887
      }
888
889
890
891
892
893
894
895
896
    }

    // Update shared table records if necessary.
    if ($shared_table_fields) {
      $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
      // Create the storage record to be saved.
      if ($update) {
        $default_revision = $entity->isDefaultRevision();
        if ($default_revision) {
897
          $id = $record->{$this->idKey};
898
899
900
          // Remove the ID from the record to enable updates on SQL variants
          // that prevent updating serial columns, for example, mssql.
          unset($record->{$this->idKey});
901
902
903
          $this->database
            ->update($this->baseTable)
            ->fields((array) $record)
904
            ->condition($this->idKey, $id)
905
906
907
908
909
910
911
912
            ->execute();
        }
        if ($this->revisionTable) {
          if ($full_save) {
            $entity->{$this->revisionKey} = $this->saveRevision($entity);
          }
          else {
            $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
913
914
915
916
            // Remove the revision ID from the record to enable updates on SQL
            // variants that prevent updating serial columns, for example,
            // mssql.
            unset($record->{$this->revisionKey});
917
918
919
920
            $entity->preSaveRevision($this, $record);
            $this->database
              ->update($this->revisionTable)
              ->fields((array) $record)
921
              ->condition($this->revisionKey, $entity->getRevisionId())
922
923
924
925
926
927
928
929
930
931
              ->execute();
          }
        }
        if ($default_revision && $this->dataTable) {
          $this->saveToSharedTables($entity);
        }
        if ($this->revisionDataTable) {
          $new_revision = $full_save && $entity->isNewRevision();
          $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
        }
932
      }
933
934
      else {
        $insert_id = $this->database
935
          ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
          ->fields((array) $record)
          ->execute();
        // Even if this is a new entity the ID key might have been set, in which
        // case we should not override the provided ID. An ID key that is not set
        // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
        if (!isset($record->{$this->idKey})) {
          $record->{$this->idKey} = $insert_id;
        }
        $entity->{$this->idKey} = (string) $record->{$this->idKey};
        if ($this->revisionTable) {
          $record->{$this->revisionKey} = $this->saveRevision($entity);
        }
        if ($this->dataTable) {
          $this->saveToSharedTables($entity);
        }
        if ($this->revisionDataTable) {
          $this->saveToSharedTables($entity, $this->revisionDataTable);
        }
954
955
956
      }
    }

957
958
959
960
    // Update dedicated table records if necessary.
    if ($dedicated_table_fields) {
      $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
      $this->saveToDedicatedTables($entity, $update, $names);
961
    }
962
963
964
965
966
967
968
969
970
  }

  /**
   * {@inheritdoc}
   */
  protected function has($id, EntityInterface $entity) {
    return !$entity->isNew();
  }

971
  /**
972
   * Saves fields that use the shared tables.
973
   *
974
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
975
   *   The entity object.
976
977
   * @param string $table_name
   *   (optional) The table name to save to. Defaults to the data table.
978
979
980
   * @param bool $new_revision
   *   (optional) Whether we are dealing with a new revision. By default fetches
   *   the information from the entity object.
981
   */
982
  protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
983
984
985
    if (!isset($table_name)) {
      $table_name = $this->dataTable;
    }
986
987
988
    if (!isset($new_revision)) {
      $new_revision = $entity->isNewRevision();
    }
989
    $revision = $table_name != $this->dataTable;
990

991
    if (!$revision || !$new_revision) {
992
993
994
995
996
997
998
999
1000
1001
1002
1003
      $key = $revision ? $this->revisionKey : $this->idKey;
      $value = $revision ? $entity->getRevisionId() : $entity->id();
      // Delete and insert to handle removed values.
      $this->database->delete($table_name)
        ->condition($key, $value)
        ->execute();
    }

    $query = $this->database->insert($table_name);

    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
      $translation = $entity->getTranslation($langcode);
1004
      $record = $this->mapToDataStorageRecord($translation, $table_name);
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
      $values = (array) $record;
      $query
        ->fields(array_keys($values))
        ->values($values);
    }

    $query->execute();
  }

  /**
   * Maps from an entity object to the storage record.
   *
1017
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1018
   *   The entity object.
1019
1020
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the base table.
1021
   *
1022
   * @return object
1023
1024
   *   The record to store.
   */
1025
1026
1027
1028
1029
  protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->baseTable;
    }

1030
    $record = new \stdClass();
1031
1032
    $table_mapping = $this->getTableMapping();
    foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1033

1034
      if (empty($this->fieldStorageDefinitions[$field_name])) {
1035
        throw new EntityStorageException("Table mapping contains invalid field $field_name.");
1036
      }
1037
      $definition = $this->fieldStorageDefinitions[$field_name];
1038
1039
1040
1041
1042
1043
1044
      $columns = $table_mapping->getColumnNames($field_name);

      foreach ($columns as $column_name => $schema_name) {
        // If there is no main property and only a single column, get all
        // properties from the first field item and assume that they will be
        // stored serialized.
        // @todo Give field types more control over this behavior in
1045
        //   https://www.drupal.org/node/2232427.
1046
        if (!$definition->getMainPropertyName() && count($columns) == 1) {
1047
          $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
1048
        }
1049
1050
        else {
          $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
1051
        }
1052
1053
        if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
          $value = serialize($value);
1054
        }
1055
1056
1057
1058

        // Do not set serial fields if we do not have a value. This supports all
        // SQL database drivers.
        // @see https://www.drupal.org/node/2279395
1059
        $value = SqlContentEntityStorageSchema::castValue($definition->getSchema()['columns'][$column_name], $value);
1060
        if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
1061
1062
          $record->$schema_name = $value;
        }
1063
1064
1065
1066
1067
1068
      }
    }

    return $record;
  }

1069
1070
1071
1072
1073
1074
1075
1076
1077
  /**
   * Checks whether a field column should be treated as serial.
   *
   * @param $table_name
   *   The name of the table the field column belongs to.
   * @param $schema_name
   *   The schema name of the field column.
   *
   * @return bool
1078
   *   TRUE if the column is serial, FALSE otherwise.
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
   */
  protected function isColumnSerial($table_name, $schema_name) {
    $result = FALSE;

    switch ($table_name) {
      case $this->baseTable:
        $result = $schema_name == $this->idKey;
        break;

      case $this->revisionTable:
        $result = $schema_name == $this->revisionKey;
        break;
    }

    return $result;
  }

1096
1097
1098
1099
1100
  /**
   * Maps from an entity object to the storage record of the field data.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
1101
1102
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the data table.
1103
   *
1104
   * @return object
1105
1106
   *   The record to store.
   */
1107
1108