3 namespace Drupal\Core\Entity\Sql;
5 use Drupal\Core\Entity\ContentEntityTypeInterface;
6 use Drupal\Core\Field\FieldStorageDefinitionInterface;
9 * Defines a default table mapping class.
11 class DefaultTableMapping implements TableMappingInterface {
14 * The entity type definition.
16 * @var \Drupal\Core\Entity\ContentEntityTypeInterface
18 protected $entityType;
21 * The field storage definitions of this mapping.
23 * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
25 protected $fieldStorageDefinitions = [];
28 * The base table of the entity.
35 * The table that stores revisions, if the entity supports revisions.
39 protected $revisionTable;
42 * The table that stores field data, if the entity has multilingual support.
49 * The table that stores revision field data if the entity supports revisions
50 * and has multilingual support.
54 protected $revisionDataTable;
57 * A list of field names per table.
59 * This corresponds to the return value of
60 * TableMappingInterface::getFieldNames() except that this variable is
61 * additionally keyed by table name.
65 protected $fieldNames = [];
68 * A list of database columns which store denormalized data per table.
70 * This corresponds to the return value of
71 * TableMappingInterface::getExtraColumns() except that this variable is
72 * additionally keyed by table name.
76 protected $extraColumns = [];
79 * A mapping of column names per field name.
81 * This corresponds to the return value of
82 * TableMappingInterface::getColumnNames() except that this variable is
83 * additionally keyed by field name.
85 * This data is derived from static::$storageDefinitions, but is stored
86 * separately to avoid repeated processing.
90 protected $columnMapping = [];
93 * A list of all database columns per table.
95 * This corresponds to the return value of
96 * TableMappingInterface::getAllColumns() except that this variable is
97 * additionally keyed by table name.
99 * This data is derived from static::$storageDefinitions, static::$fieldNames,
100 * and static::$extraColumns, but is stored separately to avoid repeated
105 protected $allColumns = [];
108 * Constructs a DefaultTableMapping.
110 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
111 * The entity type definition.
112 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
113 * A list of field storage definitions that should be available for the
114 * field columns of this table mapping.
116 public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
117 $this->entityType = $entity_type;
118 $this->fieldStorageDefinitions = $storage_definitions;
120 // @todo Remove table names from the entity type definition in
121 // https://www.drupal.org/node/2232465.
122 $this->baseTable = $entity_type->getBaseTable() ?: $entity_type->id();
123 if ($entity_type->isRevisionable()) {
124 $this->revisionTable = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
126 if ($entity_type->isTranslatable()) {
127 $this->dataTable = $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
129 if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
130 $this->revisionDataTable = $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
135 * Initializes the table mapping.
137 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
138 * The entity type definition.
139 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
140 * A list of field storage definitions that should be available for the
141 * field columns of this table mapping.
147 public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
148 $table_mapping = new static($entity_type, $storage_definitions);
150 $revisionable = $entity_type->isRevisionable();
151 $translatable = $entity_type->isTranslatable();
153 $id_key = $entity_type->getKey('id');
154 $revision_key = $entity_type->getKey('revision');
155 $bundle_key = $entity_type->getKey('bundle');
156 $uuid_key = $entity_type->getKey('uuid');
157 $langcode_key = $entity_type->getKey('langcode');
159 $shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
160 return $table_mapping->allowsSharedTableStorage($definition);
163 $key_fields = array_values(array_filter([$id_key, $revision_key, $bundle_key, $uuid_key, $langcode_key]));
164 $all_fields = array_keys($shared_table_definitions);
165 $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
166 return $definition->isRevisionable();
168 // Make sure the key fields come first in the list of fields.
169 $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
171 $revision_metadata_fields = $revisionable ? array_values($entity_type->getRevisionMetadataKeys()) : [];
173 if (!$revisionable && !$translatable) {
174 // The base layout stores all the base field values in the base table.
175 $table_mapping->setFieldNames($table_mapping->baseTable, $all_fields);
177 elseif ($revisionable && !$translatable) {
178 // The revisionable layout stores all the base field values in the base
179 // table, except for revision metadata fields. Revisionable fields
180 // denormalized in the base table but also stored in the revision table
181 // together with the entity ID and the revision ID as identifiers.
182 $table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields));
183 $revision_key_fields = [$id_key, $revision_key];
184 $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
186 elseif (!$revisionable && $translatable) {
187 // Multilingual layouts store key field values in the base table. The
188 // other base field values are stored in the data table, no matter
189 // whether they are translatable or not. The data table holds also a
190 // denormalized copy of the bundle field value to allow for more
191 // performant queries. This means that only the UUID is not stored on
194 ->setFieldNames($table_mapping->baseTable, $key_fields)
195 ->setFieldNames($table_mapping->dataTable, array_values(array_diff($all_fields, [$uuid_key])));
197 elseif ($revisionable && $translatable) {
198 // The revisionable multilingual layout stores key field values in the
199 // base table and the revision table holds the entity ID, revision ID and
200 // langcode ID along with revision metadata. The revision data table holds
201 // data field values for all the revisionable fields and the data table
202 // holds the data field values for all non-revisionable fields. The data
203 // field values of revisionable fields are denormalized in the data
205 $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields);
207 // Like in the multilingual, non-revisionable case the UUID is not
208 // in the data table. Additionally, do not store revision metadata
209 // fields in the data table.
210 $data_fields = array_values(array_diff($all_fields, [$uuid_key], $revision_metadata_fields));
211 $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields);
213 $revision_base_fields = array_merge([$id_key, $revision_key, $langcode_key], $revision_metadata_fields);
214 $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields);
216 $revision_data_key_fields = [$id_key, $revision_key, $langcode_key];
217 $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]);
218 $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
221 // Add dedicated tables.
222 $dedicated_table_definitions = array_filter($table_mapping->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
223 return $table_mapping->requiresDedicatedTableStorage($definition);
233 foreach ($dedicated_table_definitions as $field_name => $definition) {
234 $tables = [$table_mapping->getDedicatedDataTableName($definition)];
235 if ($revisionable && $definition->isRevisionable()) {
236 $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
238 foreach ($tables as $table_name) {
239 $table_mapping->setFieldNames($table_name, [$field_name]);
240 $table_mapping->setExtraColumns($table_name, $extra_columns);
244 return $table_mapping;
248 * Gets the base table name.
251 * The base table name.
255 public function getBaseTable() {
256 return $this->baseTable;
260 * Gets the revision table name.
262 * @return string|null
263 * The revision table name.
267 public function getRevisionTable() {
268 return $this->revisionTable;
272 * Gets the data table name.
274 * @return string|null
275 * The data table name.
279 public function getDataTable() {
280 return $this->dataTable;
284 * Gets the revision data table name.
286 * @return string|null
287 * The revision data table name.
291 public function getRevisionDataTable() {
292 return $this->revisionDataTable;
298 public function getTableNames() {
299 return array_unique(array_merge(array_keys($this->fieldNames), array_keys($this->extraColumns)));
305 public function getAllColumns($table_name) {
306 if (!isset($this->allColumns[$table_name])) {
307 $this->allColumns[$table_name] = [];
309 foreach ($this->getFieldNames($table_name) as $field_name) {
310 $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
313 // There is just one field for each dedicated storage table, thus
314 // $field_name can only refer to it.
315 if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) {
316 // Unlike in shared storage tables, in dedicated ones field columns are
318 $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]);
321 $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
324 return $this->allColumns[$table_name];
330 public function getFieldNames($table_name) {
331 if (isset($this->fieldNames[$table_name])) {
332 return $this->fieldNames[$table_name];
340 public function getFieldTableName($field_name) {
343 if (isset($this->fieldStorageDefinitions[$field_name])) {
344 // Since a field may be stored in more than one table, we inspect tables
345 // in order of relevance: the data table if present is the main place
346 // where field data is stored, otherwise the base table is responsible for
347 // storing field data. Revision metadata is an exception as it's stored
348 // only in the revision table.
349 $storage_definition = $this->fieldStorageDefinitions[$field_name];
350 $table_names = array_filter([
353 $this->revisionTable,
354 $this->getDedicatedDataTableName($storage_definition),
357 // Collect field columns.
359 foreach (array_keys($storage_definition->getColumns()) as $property_name) {
360 $field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
363 foreach ($table_names as $table_name) {
364 $columns = $this->getAllColumns($table_name);
365 // We assume finding one field column belonging to the mapping is enough
366 // to identify the field table.
367 if (array_intersect($columns, $field_columns)) {
368 $result = $table_name;
374 if (!isset($result)) {
375 throw new SqlContentEntityStorageException("Table information not available for the '$field_name' field.");
384 public function getColumnNames($field_name) {
385 if (!isset($this->columnMapping[$field_name])) {
386 $this->columnMapping[$field_name] = [];
387 if (isset($this->fieldStorageDefinitions[$field_name]) && !$this->fieldStorageDefinitions[$field_name]->hasCustomStorage()) {
388 foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
389 $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
393 return $this->columnMapping[$field_name];
399 public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $property_name) {
400 $field_name = $storage_definition->getName();
402 if ($this->allowsSharedTableStorage($storage_definition)) {
403 $column_name = count($storage_definition->getColumns()) == 1 ? $field_name : $field_name . '__' . $property_name;
405 elseif ($this->requiresDedicatedTableStorage($storage_definition)) {
406 if ($property_name == TableMappingInterface::DELTA) {
407 $column_name = 'delta';
410 $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
414 throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field.");
421 * Adds field columns for a table to the table mapping.
423 * @param string $table_name
424 * The name of the table to add the field column for.
425 * @param string[] $field_names
426 * A list of field names to add the columns for.
430 * @deprecated in Drupal 8.6.0 and will be changed to a protected method
431 * before Drupal 9.0.0. There will be no replacement for it because the
432 * default table mapping is now able to be initialized on its own.
434 public function setFieldNames($table_name, array $field_names) {
435 $this->fieldNames[$table_name] = $field_names;
436 // Force the re-computation of the column list.
437 unset($this->allColumns[$table_name]);
444 public function getExtraColumns($table_name) {
445 if (isset($this->extraColumns[$table_name])) {
446 return $this->extraColumns[$table_name];
452 * Adds a extra columns for a table to the table mapping.
454 * @param string $table_name
455 * The name of table to add the extra columns for.
456 * @param string[] $column_names
457 * The list of column names.
461 * @deprecated in Drupal 8.6.0 and will be changed to a protected method
462 * before Drupal 9.0.0. There will be no replacement for it because the
463 * default table mapping is now able to be initialized on its own.
465 public function setExtraColumns($table_name, array $column_names) {
466 $this->extraColumns[$table_name] = $column_names;
467 // Force the re-computation of the column list.
468 unset($this->allColumns[$table_name]);
473 * Checks whether the given field can be stored in a shared table.
475 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
476 * The field storage definition.
479 * TRUE if the field can be stored in a shared table, FALSE otherwise.
481 public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
482 return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple() && !$storage_definition->isDeleted();
486 * Checks whether the given field has to be stored in a dedicated table.
488 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
489 * The field storage definition.
492 * TRUE if the field has to be stored in a dedicated table, FALSE otherwise.
494 public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
495 return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
499 * Gets a list of dedicated table names for this mapping.
502 * An array of table names.
504 public function getDedicatedTableNames() {
505 $table_mapping = $this;
506 $definitions = array_filter($this->fieldStorageDefinitions, function ($definition) use ($table_mapping) {
507 return $table_mapping->requiresDedicatedTableStorage($definition);
509 $data_tables = array_map(function ($definition) use ($table_mapping) {
510 return $table_mapping->getDedicatedDataTableName($definition);
512 $revision_tables = array_map(function ($definition) use ($table_mapping) {
513 return $table_mapping->getDedicatedRevisionTableName($definition);
515 $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables));
516 return $dedicated_tables;
522 public function getReservedColumns() {
527 * Generates a table name for a field data table.
529 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
530 * The field storage definition.
531 * @param bool $is_deleted
532 * (optional) Whether the table name holding the values of a deleted field
533 * should be returned.
536 * A string containing the generated name for the database table.
538 public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
540 // When a field is a deleted, the table is renamed to
541 // {field_deleted_data_UNIQUE_STORAGE_ID}. To make sure we don't end up
542 // with table names longer than 64 characters, we hash the unique storage
543 // identifier and return the first 10 characters so we end up with a short
545 return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
548 return $this->generateFieldTableName($storage_definition, FALSE);
553 * Generates a table name for a field revision archive table.
555 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
556 * The field storage definition.
557 * @param bool $is_deleted
558 * (optional) Whether the table name holding the values of a deleted field
559 * should be returned.
562 * A string containing the generated name for the database table.
564 public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
566 // When a field is a deleted, the table is renamed to
567 // {field_deleted_revision_UNIQUE_STORAGE_ID}. To make sure we don't end
568 // up with table names longer than 64 characters, we hash the unique
569 // storage identifier and return the first 10 characters so we end up with
570 // a short unique ID.
571 return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
574 return $this->generateFieldTableName($storage_definition, TRUE);
579 * Generates a safe and unambiguous field table name.
581 * The method accounts for a maximum table name length of 64 characters, and
582 * takes care of disambiguation.
584 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
585 * The field storage definition.
586 * @param bool $revision
587 * TRUE for revision table, FALSE otherwise.
590 * The final table name.
592 protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
593 $separator = $revision ? '_revision__' : '__';
594 $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName();
595 // Limit the string to 48 characters, keeping a 16 characters margin for db
597 if (strlen($table_name) > 48) {
598 // Use a shorter separator, a truncated entity_type, and a hash of the
599 // field storage unique identifier.
600 $separator = $revision ? '_r__' : '__';
601 // Truncate to the same length for the current and revision tables.
602 $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
603 $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
604 $table_name = $entity_type . $separator . $field_hash;