SqlContentEntityStorage.php 61.8 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
16
17
18
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
19
use Drupal\Core\Entity\Query\QueryInterface;
20
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
21
use Drupal\Core\Field\FieldDefinitionInterface;
22
use Drupal\Core\Field\FieldStorageDefinitionInterface;
23
use Drupal\Core\Language\LanguageInterface;
24
use Drupal\Core\Language\LanguageManagerInterface;
25
26
27
use Symfony\Component\DependencyInjection\ContainerInterface;

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

  /**
   * The mapping of field columns to SQL tables.
   *
   * @var \Drupal\Core\Entity\Sql\TableMappingInterface
   */
  protected $tableMapping;
48
49
50
51
52
53
54
55
56
57

  /**
   * 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;

58
59
60
61
62
63
64
  /**
   * The entity langcode key.
   *
   * @var string|bool
   */
  protected $langcodeKey = FALSE;

65
66
67
68
69
70
71
  /**
   * The default language entity key.
   *
   * @var string
   */
  protected $defaultLangcodeKey = FALSE;

72
73
74
75
76
77
78
  /**
   * The base table of the entity.
   *
   * @var string
   */
  protected $baseTable;

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
  /**
   * 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;

107
  /**
108
   * The entity type's storage schema object.
109
   *
110
   * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
111
   */
112
  protected $storageSchema;
113

114
115
116
117
118
119
120
  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

121
122
123
124
125
126
127
  /**
   * Whether this storage should use the temporary table mapping.
   *
   * @var bool
   */
  protected $temporary = FALSE;

128
129
130
  /**
   * {@inheritdoc}
   */
131
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
132
    return new static(
133
      $entity_type,
134
      $container->get('database'),
135
      $container->get('entity.manager'),
136
      $container->get('cache.entity'),
137
138
      $container->get('language_manager'),
      $container->get('entity.memory_cache')
139
140
141
    );
  }

142
143
144
145
146
147
148
149
150
151
152
  /**
   * Gets the base field definitions for a content entity type.
   *
   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
   *   The array of base field definitions for the entity type, keyed by field
   *   name.
   */
  public function getFieldStorageDefinitions() {
    return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
  }

153
  /**
154
   * Constructs a SqlContentEntityStorage object.
155
   *
156
157
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
158
159
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
160
161
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager.
162
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
163
   *   The cache backend to be used.
164
165
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
166
167
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
   *   The memory cache backend to be used.
168
   */
169
170
  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
    parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
171
    $this->database = $database;
172
    $this->languageManager = $language_manager;
173
174
175
176
177
178
179
180
181
182
183
184
185
186
    $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;
187

188
189
    $table_mapping = $this->getTableMapping();
    $this->baseTable = $table_mapping->getBaseTable();
190
191
192
    $revisionable = $this->entityType->isRevisionable();
    if ($revisionable) {
      $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
193
      $this->revisionTable = $table_mapping->getRevisionTable();
194
    }
195
    $translatable = $this->entityType->isTranslatable();
196
    if ($translatable) {
197
      $this->dataTable = $table_mapping->getDataTable();
198
      $this->langcodeKey = $this->entityType->getKey('langcode');
199
      $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
200
201
    }
    if ($revisionable && $translatable) {
202
      $this->revisionDataTable = $table_mapping->getRevisionDataTable();
203
204
205
206
    }
  }

  /**
207
   * Gets the base table name.
208
209
210
211
212
213
214
215
216
   *
   * @return string
   *   The table name.
   */
  public function getBaseTable() {
    return $this->baseTable;
  }

  /**
217
   * Gets the revision table name.
218
219
220
221
222
223
224
225
226
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionTable() {
    return $this->revisionTable;
  }

  /**
227
   * Gets the data table name.
228
229
230
231
232
233
234
235
236
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getDataTable() {
    return $this->dataTable;
  }

  /**
237
   * Gets the revision data table name.
238
239
240
241
242
243
244
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionDataTable() {
    return $this->revisionDataTable;
  }
245

246
  /**
247
   * Gets the entity type's storage schema object.
248
   *
249
   * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
250
   *   The schema object.
251
   */
252
253
254
255
  protected function getStorageSchema() {
    if (!isset($this->storageSchema)) {
      $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
      $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
256
    }
257
    return $this->storageSchema;
258
  }
259

260
  /**
261
262
263
264
265
   * Updates the wrapped entity type definition.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The update entity type.
   *
266
267
   * @internal Only to be used internally by Entity API. Expected to be
   *   removed by https://www.drupal.org/node/2274017.
268
   */
269
270
271
272
273
274
  public function setEntityType(EntityTypeInterface $entity_type) {
    if ($this->entityType->id() == $entity_type->id()) {
      $this->entityType = $entity_type;
      $this->initTableLayout();
    }
    else {
275
      throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
276
277
    }
  }
278

279
280
281
282
283
284
285
286
287
288
289
  /**
   * 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;
290
291
292
293
294

    $this->baseTable = $table_mapping->getBaseTable();
    $this->revisionTable = $table_mapping->getRevisionTable();
    $this->dataTable = $table_mapping->getDataTable();
    $this->revisionDataTable = $table_mapping->getRevisionDataTable();
295
296
297
298
299
300
301
302
303
304
305
306
307
308
  }

  /**
   * 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;
  }

309
310
311
312
  /**
   * {@inheritdoc}
   */
  public function getTableMapping(array $storage_definitions = NULL) {
313
314
315
316
317
318
    // 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);
    }
319
320

    // If we are using our internal storage definitions, which is our main use
321
322
323
324
325
    // case, we can statically cache the computed table mapping.
    if (!isset($this->tableMapping)) {
      $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);

      $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
326
    }
327

328
329
330
331
332
333
334
335
336
337
338
    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.
339
340
341
   * @param string $prefix
   *   (optional) A prefix to be used by all the tables of this mapping.
   *   Defaults to an empty string.
342
343
344
345
346
347
   *
   * @return \Drupal\Core\Entity\Sql\TableMappingInterface
   *   A table mapping object for the entity's tables.
   *
   * @internal
   */
348
349
350
  public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
    $prefix = $prefix ?: ($this->temporary ? 'tmp_' : '');
    return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix);
351
352
353
354
355
  }

  /**
   * {@inheritdoc}
   */
356
  protected function doLoadMultiple(array $ids = NULL) {
357
358
359
360
361
    // 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.
362
363
364
365
    if ($entities_from_storage = $this->getFromStorage($ids)) {
      $this->invokeStorageLoadHook($entities_from_storage);
      $this->setPersistentCache($entities_from_storage);
    }
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380

    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) {
381
    $entities = [];
382

383
384
385
386
387
388
    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);
    }

389
390
391
392
393
394
395
396
397
398
399
400
401
402
    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;
  }

403
  /**
404
   * Maps from storage records to entity objects, and attaches fields.
405
   *
406
   * @param array $records
407
408
   *   Associative array of query results, keyed on the entity ID or revision
   *   ID.
409
   * @param bool $load_from_revision
410
411
   *   (optional) Flag to indicate whether revisions should be loaded or not.
   *   Defaults to FALSE.
412
413
414
415
   *
   * @return array
   *   An array of entity objects implementing the EntityInterface.
   */
416
  protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
417
    if (!$records) {
418
      return [];
419
420
    }

421
422
423
424
425
426
427
428
    // 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)));
    }

429
    $values = [];
430
    foreach ($records as $id => $record) {
431
      $values[$id] = [];
432
433
434
      // 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.
435
436
437
438
439
440
441
442
443
444
      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) {
          foreach ($field_columns as $property_name => $column_name) {
            if (property_exists($record, $column_name)) {
              $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $record->{$column_name};
              unset($record->{$column_name});
            }
          }
445
        }
446
        // Handle field types that store only one property.
447
        else {
448
449
450
451
452
          $column_name = reset($field_columns);
          if (property_exists($record, $column_name)) {
            $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $record->{$column_name};
            unset($record->{$column_name});
          }
453
        }
454
      }
455
456
457
458
459
460

      // 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;
      }
461
    }
462

463
    // Initialize translations array.
464
    $translations = array_fill_keys(array_keys($values), []);
465
466

    // Load values from shared and dedicated tables.
467
    $this->loadFromSharedTables($values, $translations, $load_from_revision);
468
469
    $this->loadFromDedicatedTables($values, $load_from_revision);

470
    $entities = [];
471
472
473
474
475
    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]));
    }
476

477
478
479
480
    return $entities;
  }

  /**
481
   * Loads values for fields stored in the shared data tables.
482
   *
483
   * @param array &$values
484
485
   *   Associative array of entities values, keyed on the entity ID or the
   *   revision ID.
486
487
   * @param array &$translations
   *   List of translations, keyed on the entity ID.
488
489
   * @param bool $load_from_revision
   *   Flag to indicate whether revisions should be loaded or not.
490
   */
491
492
  protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
    $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
493
494
495
496
    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;
497
      $alias = $this->revisionDataTable ? 'revision' : 'data';
498
      $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
499
        ->fields($alias)
500
501
        ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
        ->orderBy($alias . '.' . $record_key);
502

503
      $table_mapping = $this->getTableMapping();
504
      if ($this->revisionDataTable) {
505
506
        // Find revisioned fields that are not entity keys. Exclude the langcode
        // key as the base table holds only the default language.
507
        $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
508
        $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
509
510
511

        // Find fields that are not revisioned or entity keys. Data fields have
        // the same value regardless of entity revision.
512
513
514
515
516
        $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;
517
        if ($data_fields) {
518
          $all_fields = array_merge($revisioned_fields, $data_fields);
519
          $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
520
521
522
523
          $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) {
524
            // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
525
526
527
528
529
530
            // 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);
531
532
        }

533
        // Get the revision IDs.
534
        $revision_ids = [];
535
536
        foreach ($values as $entity_values) {
          $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
537
        }
538
        $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
539
540
      }
      else {
541
        $all_fields = $table_mapping->getFieldNames($this->dataTable);
542
543
      }

544
545
      $result = $query->execute();
      foreach ($result as $row) {
546
        $id = $row[$record_key];
547
548

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

552
553
        $translations[$id][$langcode] = TRUE;

554
        foreach ($all_fields as $field_name) {
555
556
557
          $columns = $table_mapping->getColumnNames($field_name);
          // Do not key single-column fields by property name.
          if (count($columns) == 1) {
558
            $values[$id][$field_name][$langcode] = $row[reset($columns)];
559
560
          }
          else {
561
            foreach ($columns as $property_name => $column_name) {
562
              $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
563
            }
564
565
566
567
568
569
570
          }
        }
      }
    }
  }

  /**
571
   * {@inheritdoc}
572
   */
573
  protected function doLoadRevisionFieldItems($revision_id) {
574
    @trigger_error('"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
575

576
577
578
579
580
581
582
583
584
585
    $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);

    return !empty($revisions) ? reset($revisions) : NULL;
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
    $revisions = [];
586

587
588
589
590
591
592
593
594
595
596
597
598
599
    // 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);
      }
600
    }
601

602
    return $revisions;
603
604
605
  }

  /**
606
   * {@inheritdoc}
607
   */
608
609
610
611
  protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
    $this->database->delete($this->revisionTable)
      ->condition($this->revisionKey, $revision->getRevisionId())
      ->execute();
612
613
614
615
616
617
618

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

619
    $this->deleteRevisionFromDedicatedTables($revision);
620
621
622
  }

  /**
623
   * {@inheritdoc}
624
625
626
627
   */
  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
    if ($this->dataTable) {
      // @todo We should not be using a condition to specify whether conditions
628
629
      //   apply to the default language. See
      //   https://www.drupal.org/node/1866330.
630
631
      // Default to the original entity language if not explicitly specified
      // otherwise.
632
633
      if (!array_key_exists($this->defaultLangcodeKey, $values)) {
        $values[$this->defaultLangcodeKey] = 1;
634
635
636
      }
      // 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.
637
638
      elseif ($values[$this->defaultLangcodeKey] === NULL) {
        unset($values[$this->defaultLangcodeKey]);
639
640
641
      }
    }

642
    parent::buildPropertyQuery($entity_query, $values);
643
644
645
646
647
648
649
650
651
652
653
  }

  /**
   * 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
   * table, such as loading node type into comments or vocabulary machine name
   * into terms, however it can also support $conditions on different tables.
654
   * See Drupal\comment\CommentStorage::buildQuery() for an example.
655
656
657
   *
   * @param array|null $ids
   *   An array of entity IDs, or NULL to load all entities.
658
659
660
   * @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.
661
   *
662
   * @return \Drupal\Core\Database\Query\Select
663
664
   *   A SelectQuery object for loading the entity.
   */
665
  protected function buildQuery($ids, $revision_ids = FALSE) {
666
    $query = $this->database->select($this->baseTable, 'base');
667

668
    $query->addTag($this->entityTypeId . '_load_multiple');
669

670
671
672
673
674
    if ($revision_ids) {
      if (!is_array($revision_ids)) {
        @trigger_error('Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
      }
      $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
675
676
677
678
679
680
    }
    elseif ($this->revisionTable) {
      $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
    }

    // Add fields from the {entity} table.
681
682
    $table_mapping = $this->getTableMapping();
    $entity_fields = $table_mapping->getAllColumns($this->baseTable);
683
684
685

    if ($this->revisionTable) {
      // Add all fields from the {entity_revision} table.
686
      $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
687
      $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
      // 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.
703
      $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
704
705
706
707
708
709
710
711
712
713
714
715
    }

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

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

    return $query;
  }

  /**
716
   * {@inheritdoc}
717
718
719
720
721
722
723
724
725
   */
  public function delete(array $entities) {
    if (!$entities) {
      // If no IDs or invalid IDs were passed, do nothing.
      return;
    }

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

728
      // Ignore replica server temporarily.
729
      \Drupal::service('database.replica_kill_switch')->trigger();
730
731
    }
    catch (\Exception $e) {
732
      $transaction->rollBack();
733
      watchdog_exception($this->entityTypeId, $e);
734
735
736
737
738
739
740
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

  /**
   * {@inheritdoc}
   */
741
  protected function doDeleteFieldItems($entities) {
742
    $ids = array_keys($entities);
743

744
    $this->database->delete($this->baseTable)
745
      ->condition($this->idKey, $ids, 'IN')
746
      ->execute();
747

748
749
    if ($this->revisionTable) {
      $this->database->delete($this->revisionTable)
750
        ->condition($this->idKey, $ids, 'IN')
751
752
        ->execute();
    }
753

754
755
    if ($this->dataTable) {
      $this->database->delete($this->dataTable)
756
        ->condition($this->idKey, $ids, 'IN')
757
758
        ->execute();
    }
759

760
761
    if ($this->revisionDataTable) {
      $this->database->delete($this->revisionDataTable)
762
        ->condition($this->idKey, $ids, 'IN')
763
764
        ->execute();
    }
765

766
    foreach ($entities as $entity) {
767
      $this->deleteFromDedicatedTables($entity);
768
769
770
771
772
773
774
775
776
777
    }
  }

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

779
      // Ignore replica server temporarily.
780
      \Drupal::service('database.replica_kill_switch')->trigger();
781
782
783
      return $return;
    }
    catch (\Exception $e) {
784
      $transaction->rollBack();
785
      watchdog_exception($this->entityTypeId, $e);
786
787
788
789
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

790
791
792
  /**
   * {@inheritdoc}
   */
793
794
795
  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
    $full_save = empty($names);
    $update = !$full_save || !$entity->isNew();
796

797
798
799
    if ($full_save) {
      $shared_table_fields = TRUE;
      $dedicated_table_fields = TRUE;
800
801
    }
    else {
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
      $table_mapping = $this->getTableMapping();
      $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
      $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) {
        $storage_definition = $storage_definitions[$name];
        if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
          $shared_table_fields = TRUE;
        }
        elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
          $dedicated_table_fields[] = $name;
        }
817
      }
818
819
820
821
822
823
824
825
826
    }

    // 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) {
827
          $id = $record->{$this->idKey};
828
829
830
          // Remove the ID from the record to enable updates on SQL variants
          // that prevent updating serial columns, for example, mssql.
          unset($record->{$this->idKey});
831
832
833
          $this->database
            ->update($this->baseTable)
            ->fields((array) $record)
834
            ->condition($this->idKey, $id)
835
836
837
838
839
840
841
842
            ->execute();
        }
        if ($this->revisionTable) {
          if ($full_save) {
            $entity->{$this->revisionKey} = $this->saveRevision($entity);
          }
          else {
            $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
843
844
845
846
            // 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});
847
848
849
850
            $entity->preSaveRevision($this, $record);
            $this->database
              ->update($this->revisionTable)
              ->fields((array) $record)
851
              ->condition($this->revisionKey, $entity->getRevisionId())
852
853
854
855
856
857
858
859
860
861
              ->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);
        }
862
      }
863
864
      else {
        $insert_id = $this->database
865
          ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
          ->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);
        }
884
885
886
      }
    }

887
888
889
890
    // Update dedicated table records if necessary.
    if ($dedicated_table_fields) {
      $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
      $this->saveToDedicatedTables($entity, $update, $names);
891
    }
892
893
894
895
896
897
898
899
900
  }

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

901
  /**
902
   * Saves fields that use the shared tables.
903
   *
904
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
905
   *   The entity object.
906
907
   * @param string $table_name
   *   (optional) The table name to save to. Defaults to the data table.
908
909
910
   * @param bool $new_revision
   *   (optional) Whether we are dealing with a new revision. By default fetches
   *   the information from the entity object.
911
   */
912
  protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
913
914
915
    if (!isset($table_name)) {
      $table_name = $this->dataTable;
    }
916
917
918
    if (!isset($new_revision)) {
      $new_revision = $entity->isNewRevision();
    }
919
    $revision = $table_name != $this->dataTable;
920

921
    if (!$revision || !$new_revision) {
922
923
924
925
926
927
928
929
930
931
932
933
      $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);
934
      $record = $this->mapToDataStorageRecord($translation, $table_name);
935
936
937
938
939
940
941
942
943
944
945
946
      $values = (array) $record;
      $query
        ->fields(array_keys($values))
        ->values($values);
    }

    $query->execute();
  }

  /**
   * Maps from an entity object to the storage record.
   *
947
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
948
   *   The entity object.
949
950
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the base table.
951
952
953
954
   *
   * @return \stdClass
   *   The record to store.
   */
955
956
957
958
959
  protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->baseTable;
    }

960
    $record = new \stdClass();
961
962
    $table_mapping = $this->getTableMapping();
    foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
963

964
      if (empty($this->getFieldStorageDefinitions()[$field_name])) {
965
        throw new EntityStorageException("Table mapping contains invalid field $field_name.");
966
      }
967
      $definition = $this->getFieldStorageDefinitions()[$field_name];
968
969
970
971
972
973
974
      $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
975
        //   https://www.drupal.org/node/2232427.
976
        if (!$definition->getMainPropertyName() && count($columns) == 1) {
977
          $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
978
        }
979
980
        else {
          $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
981
        }
982
983
        if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
          $value = serialize($value);
984
        }
985
986
987
988
989

        // 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
        $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
990
        if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
991
992
          $record->$schema_name = $value;
        }
993
994
995
996
997
998
      }
    }

    return $record;
  }

999
1000
1001
1002
1003
1004
1005
1006
1007
  /**
   * 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
1008
   *   TRUE if the column is serial, FALSE otherwise.
1009
   *
1010
1011
   * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
   * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
   */
  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;
  }

1029
1030
1031
1032
1033
  /**
   * Maps from an entity object to the storage record of the field data.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
1034
1035
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the data table.
1036
1037
1038
1039
   *
   * @return \stdClass
   *   The record to store.
   */
1040
1041
1042
1043
1044
  protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->dataTable;
    }
    $record = $this->mapToStorageRecord($entity, $table_name);
1045
1046
1047
1048
1049
1050
    return $record;
  }

  /**
   * Saves an entity revision.
   *
1051
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1052
1053
1054
1055
1056
   *   The entity object.
   *
   * @return int
   *   The revision id.
   */
1057
  protected function saveRevision(ContentEntityInterface $entity) {
1058
    $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1059
1060
1061
1062

    $entity->preSaveRevision($this, $record);

    if ($entity->isNewRevision()) {
1063
      $insert_id = $this->database
1064
        ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1065
1066
        ->fields((array) $record)
        ->execute();
1067
      // Even if this is a new revision, the revision ID key might have been
1068
1069
1070
1071
      // set in which case we should not override the provided revision ID.
      if (!isset($record->{$this->revisionKey})) {
        $record->{$this->revisionKey} = $insert_id;
      }
1072
      if ($entity->isDefaultRevision()) {
1073
        $this->database->update($this->baseTable)
1074
          ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1075
1076
1077
          ->condition($this->idKey, $record->{$this->idKey})
          ->execute();
      }
1078
1079
      // Make sure to update the new revision key for the entity.
      $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1080
1081
    }
    else {
1082
1083
1084
1085
      // 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});
1086
1087
1088
      $this->database
        ->update($this->revisionTable)
        ->fields((array) $record)
1089
        ->condition($this->revisionKey, $entity->getRevisionId())
1090
        ->execute();
1091
    }
1092
    return $entity->getRevisionId();
1093
1094
1095
1096
1097
  }

  /**
   * {@inheritdoc}
   */
1098
  protected function getQueryServiceName() {
1099
1100
1101
1102
    return 'entity.query.sql';
  }

  /**
1103
   * Loads values of fields stored in dedicated tables for a group of entities.
1104
   *
1105
1106
1107
   * @param array &$values
   *   An array of values keyed by entity ID.
   * @param bool $load_from_revision
1108
   *   Flag to indicate whether revisions should be loaded or not.