123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758 |
- <?php
- namespace Drupal\Core\Entity\Sql;
- use Drupal\Core\Cache\CacheBackendInterface;
- use Drupal\Core\Database\Connection;
- use Drupal\Core\Database\Database;
- use Drupal\Core\Database\DatabaseExceptionWrapper;
- use Drupal\Core\Database\SchemaException;
- use Drupal\Core\Entity\ContentEntityInterface;
- use Drupal\Core\Entity\ContentEntityStorageBase;
- use Drupal\Core\Entity\EntityBundleListenerInterface;
- use Drupal\Core\Entity\EntityInterface;
- use Drupal\Core\Entity\EntityManagerInterface;
- use Drupal\Core\Entity\EntityStorageException;
- use Drupal\Core\Entity\EntityTypeInterface;
- use Drupal\Core\Entity\Query\QueryInterface;
- use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
- use Drupal\Core\Field\FieldDefinitionInterface;
- use Drupal\Core\Field\FieldStorageDefinitionInterface;
- use Drupal\Core\Language\LanguageInterface;
- use Drupal\Core\Language\LanguageManagerInterface;
- use Symfony\Component\DependencyInjection\ContainerInterface;
- /**
- * A content entity database storage implementation.
- *
- * This class can be used as-is by most content entity types. Entity types
- * requiring special handling can extend the class.
- *
- * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
- * internally in order to automatically generate the database schema based on
- * the defined base fields. Entity types can override the schema handler to
- * customize the generated schema; e.g., to add additional indexes.
- *
- * @ingroup entity_api
- */
- class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
- /**
- * The mapping of field columns to SQL tables.
- *
- * @var \Drupal\Core\Entity\Sql\TableMappingInterface
- */
- protected $tableMapping;
- /**
- * 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;
- /**
- * The entity langcode key.
- *
- * @var string|bool
- */
- protected $langcodeKey = FALSE;
- /**
- * The default language entity key.
- *
- * @var string
- */
- protected $defaultLangcodeKey = FALSE;
- /**
- * The base table of the entity.
- *
- * @var string
- */
- protected $baseTable;
- /**
- * 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;
- /**
- * The entity type's storage schema object.
- *
- * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
- */
- protected $storageSchema;
- /**
- * The language manager.
- *
- * @var \Drupal\Core\Language\LanguageManagerInterface
- */
- protected $languageManager;
- /**
- * Whether this storage should use the temporary table mapping.
- *
- * @var bool
- */
- protected $temporary = FALSE;
- /**
- * {@inheritdoc}
- */
- public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
- return new static(
- $entity_type,
- $container->get('database'),
- $container->get('entity.manager'),
- $container->get('cache.entity'),
- $container->get('language_manager')
- );
- }
- /**
- * 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);
- }
- /**
- * Constructs a SqlContentEntityStorage object.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type definition.
- * @param \Drupal\Core\Database\Connection $database
- * The database connection to be used.
- * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
- * The entity manager.
- * @param \Drupal\Core\Cache\CacheBackendInterface $cache
- * The cache backend to be used.
- * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
- * The language manager.
- */
- public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
- parent::__construct($entity_type, $entity_manager, $cache);
- $this->database = $database;
- $this->languageManager = $language_manager;
- $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;
- // @todo Remove table names from the entity type definition in
- // https://www.drupal.org/node/2232465.
- $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
- $revisionable = $this->entityType->isRevisionable();
- if ($revisionable) {
- $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
- $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
- }
- $translatable = $this->entityType->isTranslatable();
- if ($translatable) {
- $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
- $this->langcodeKey = $this->entityType->getKey('langcode');
- $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
- }
- if ($revisionable && $translatable) {
- $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
- }
- }
- /**
- * Gets the base table name.
- *
- * @return string
- * The table name.
- */
- public function getBaseTable() {
- return $this->baseTable;
- }
- /**
- * Gets the revision table name.
- *
- * @return string|false
- * The table name or FALSE if it is not available.
- */
- public function getRevisionTable() {
- return $this->revisionTable;
- }
- /**
- * Gets the data table name.
- *
- * @return string|false
- * The table name or FALSE if it is not available.
- */
- public function getDataTable() {
- return $this->dataTable;
- }
- /**
- * Gets the revision data table name.
- *
- * @return string|false
- * The table name or FALSE if it is not available.
- */
- public function getRevisionDataTable() {
- return $this->revisionDataTable;
- }
- /**
- * Gets the entity type's storage schema object.
- *
- * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
- * The schema object.
- */
- 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);
- }
- return $this->storageSchema;
- }
- /**
- * Updates the wrapped entity type definition.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The update entity type.
- *
- * @internal Only to be used internally by Entity API. Expected to be
- * removed by https://www.drupal.org/node/2274017.
- */
- public function setEntityType(EntityTypeInterface $entity_type) {
- if ($this->entityType->id() == $entity_type->id()) {
- $this->entityType = $entity_type;
- $this->initTableLayout();
- }
- else {
- throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
- }
- }
- /**
- * 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;
- }
- /**
- * 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;
- }
- /**
- * {@inheritdoc}
- */
- public function getTableMapping(array $storage_definitions = NULL) {
- $table_mapping = $this->tableMapping;
- // If we are using our internal storage definitions, which is our main use
- // case, we can statically cache the computed table mapping. 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.
- // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
- // easily instantiate a new table mapping whenever needed.
- if (!isset($this->tableMapping) || $storage_definitions) {
- $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
- $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
- $table_mapping = new $table_mapping_class($this->entityType, $definitions);
- $shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
- return $table_mapping->allowsSharedTableStorage($definition);
- });
- $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
- $all_fields = array_keys($shared_table_definitions);
- $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
- return $definition->isRevisionable();
- }));
- // Make sure the key fields come first in the list of fields.
- $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
- // If the entity is revisionable, gather the fields that need to be put
- // in the revision table.
- $revisionable = $this->entityType->isRevisionable();
- $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
- $translatable = $this->entityType->isTranslatable();
- if (!$revisionable && !$translatable) {
- // The base layout stores all the base field values in the base table.
- $table_mapping->setFieldNames($this->baseTable, $all_fields);
- }
- elseif ($revisionable && !$translatable) {
- // The revisionable layout stores all the base field values in the base
- // table, except for revision metadata fields. Revisionable fields
- // denormalized in the base table but also stored in the revision table
- // together with the entity ID and the revision ID as identifiers.
- $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
- $revision_key_fields = [$this->idKey, $this->revisionKey];
- $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
- }
- elseif (!$revisionable && $translatable) {
- // Multilingual layouts store key field values in the base table. The
- // other base field values are stored in the data table, no matter
- // whether they are translatable or not. The data table holds also a
- // denormalized copy of the bundle field value to allow for more
- // performant queries. This means that only the UUID is not stored on
- // the data table.
- $table_mapping
- ->setFieldNames($this->baseTable, $key_fields)
- ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
- }
- elseif ($revisionable && $translatable) {
- // The revisionable multilingual layout stores key field values in the
- // base table, except for language, which is stored in the revision
- // table along with revision metadata. The revision data table holds
- // data field values for all the revisionable fields and the data table
- // holds the data field values for all non-revisionable fields. The data
- // field values of revisionable fields are denormalized in the data
- // table, as well.
- $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
- // Like in the multilingual, non-revisionable case the UUID is not
- // in the data table. Additionally, do not store revision metadata
- // fields in the data table.
- $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
- $table_mapping->setFieldNames($this->dataTable, $data_fields);
- $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
- $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
- $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
- $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
- $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
- }
- // Add dedicated tables.
- $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
- return $table_mapping->requiresDedicatedTableStorage($definition);
- });
- $extra_columns = [
- 'bundle',
- 'deleted',
- 'entity_id',
- 'revision_id',
- 'langcode',
- 'delta',
- ];
- foreach ($dedicated_table_definitions as $field_name => $definition) {
- $tables = [$table_mapping->getDedicatedDataTableName($definition)];
- if ($revisionable && $definition->isRevisionable()) {
- $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
- }
- foreach ($tables as $table_name) {
- $table_mapping->setFieldNames($table_name, [$field_name]);
- $table_mapping->setExtraColumns($table_name, $extra_columns);
- }
- }
- // Cache the computed table mapping only if we are using our internal
- // storage definitions.
- if (!$storage_definitions) {
- $this->tableMapping = $table_mapping;
- }
- }
- return $table_mapping;
- }
- /**
- * {@inheritdoc}
- */
- protected function doLoadMultiple(array $ids = NULL) {
- // 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.
- if ($entities_from_storage = $this->getFromStorage($ids)) {
- $this->invokeStorageLoadHook($entities_from_storage);
- $this->setPersistentCache($entities_from_storage);
- }
- 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) {
- $entities = [];
- 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);
- }
- 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;
- }
- /**
- * Maps from storage records to entity objects, and attaches fields.
- *
- * @param array $records
- * Associative array of query results, keyed on the entity ID or revision
- * ID.
- * @param bool $load_from_revision
- * (optional) Flag to indicate whether revisions should be loaded or not.
- * Defaults to FALSE.
- *
- * @return array
- * An array of entity objects implementing the EntityInterface.
- */
- protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
- if (!$records) {
- return [];
- }
- $values = [];
- foreach ($records as $id => $record) {
- $values[$id] = [];
- // 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.
- foreach ($record as $name => $value) {
- // Handle columns named [field_name]__[column_name] (e.g for field types
- // that store several properties).
- if ($field_name = strstr($name, '__', TRUE)) {
- $property_name = substr($name, strpos($name, '__') + 2);
- $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
- }
- else {
- // Handle columns named directly after the field (e.g if the field
- // type only stores one property).
- $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
- }
- }
- }
- // Initialize translations array.
- $translations = array_fill_keys(array_keys($values), []);
- // Load values from shared and dedicated tables.
- $this->loadFromSharedTables($values, $translations, $load_from_revision);
- $this->loadFromDedicatedTables($values, $load_from_revision);
- $entities = [];
- 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]));
- }
- return $entities;
- }
- /**
- * Loads values for fields stored in the shared data tables.
- *
- * @param array &$values
- * Associative array of entities values, keyed on the entity ID or the
- * revision ID.
- * @param array &$translations
- * List of translations, keyed on the entity ID.
- * @param bool $load_from_revision
- * Flag to indicate whether revisions should be loaded or not.
- */
- protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
- $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
- 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;
- $alias = $this->revisionDataTable ? 'revision' : 'data';
- $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
- ->fields($alias)
- ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
- ->orderBy($alias . '.' . $record_key);
- $table_mapping = $this->getTableMapping();
- if ($this->revisionDataTable) {
- // Find revisioned fields that are not entity keys. Exclude the langcode
- // key as the base table holds only the default language.
- $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
- $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
- // Find fields that are not revisioned or entity keys. Data fields have
- // the same value regardless of entity revision.
- $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;
- if ($data_fields) {
- $all_fields = array_merge($revisioned_fields, $data_fields);
- $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
- $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) {
- // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
- // 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);
- }
- // Get the revision IDs.
- $revision_ids = [];
- foreach ($values as $entity_values) {
- $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
- }
- $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
- }
- else {
- $all_fields = $table_mapping->getFieldNames($this->dataTable);
- }
- $result = $query->execute();
- foreach ($result as $row) {
- $id = $row[$record_key];
- // Field values in default language are stored with
- // LanguageInterface::LANGCODE_DEFAULT as key.
- $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
- $translations[$id][$langcode] = TRUE;
- foreach ($all_fields as $field_name) {
- $columns = $table_mapping->getColumnNames($field_name);
- // Do not key single-column fields by property name.
- if (count($columns) == 1) {
- $values[$id][$field_name][$langcode] = $row[reset($columns)];
- }
- else {
- foreach ($columns as $property_name => $column_name) {
- $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
- }
- }
- }
- }
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function doLoadRevisionFieldItems($revision_id) {
- @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);
- $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
- return !empty($revisions) ? reset($revisions) : NULL;
- }
- /**
- * {@inheritdoc}
- */
- protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
- $revisions = [];
- // 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);
- }
- }
- return $revisions;
- }
- /**
- * {@inheritdoc}
- */
- protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
- $this->database->delete($this->revisionTable)
- ->condition($this->revisionKey, $revision->getRevisionId())
- ->execute();
- if ($this->revisionDataTable) {
- $this->database->delete($this->revisionDataTable)
- ->condition($this->revisionKey, $revision->getRevisionId())
- ->execute();
- }
- $this->deleteRevisionFromDedicatedTables($revision);
- }
- /**
- * {@inheritdoc}
- */
- protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
- if ($this->dataTable) {
- // @todo We should not be using a condition to specify whether conditions
- // apply to the default language. See
- // https://www.drupal.org/node/1866330.
- // Default to the original entity language if not explicitly specified
- // otherwise.
- if (!array_key_exists($this->defaultLangcodeKey, $values)) {
- $values[$this->defaultLangcodeKey] = 1;
- }
- // 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.
- elseif ($values[$this->defaultLangcodeKey] === NULL) {
- unset($values[$this->defaultLangcodeKey]);
- }
- }
- parent::buildPropertyQuery($entity_query, $values);
- }
- /**
- * 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.
- * See Drupal\comment\CommentStorage::buildQuery() for an example.
- *
- * @param array|null $ids
- * An array of entity IDs, or NULL to load all entities.
- * @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.
- *
- * @return \Drupal\Core\Database\Query\Select
- * A SelectQuery object for loading the entity.
- */
- protected function buildQuery($ids, $revision_ids = FALSE) {
- $query = $this->database->select($this->entityType->getBaseTable(), 'base');
- $query->addTag($this->entityTypeId . '_load_multiple');
- 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]);
- }
- elseif ($this->revisionTable) {
- $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
- }
- // Add fields from the {entity} table.
- $table_mapping = $this->getTableMapping();
- $entity_fields = $table_mapping->getAllColumns($this->baseTable);
- if ($this->revisionTable) {
- // Add all fields from the {entity_revision} table.
- $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
- $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
- // 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.
- $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
- }
- $query->fields('base', $entity_fields);
- if ($ids) {
- $query->condition("base.{$this->idKey}", $ids, 'IN');
- }
- return $query;
- }
- /**
- * {@inheritdoc}
- */
- public function delete(array $entities) {
- if (!$entities) {
- // If no IDs or invalid IDs were passed, do nothing.
- return;
- }
- $transaction = $this->database->startTransaction();
- try {
- parent::delete($entities);
- // Ignore replica server temporarily.
- db_ignore_replica();
- }
- catch (\Exception $e) {
- $transaction->rollBack();
- watchdog_exception($this->entityTypeId, $e);
- throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function doDeleteFieldItems($entities) {
- $ids = array_keys($entities);
- $this->database->delete($this->entityType->getBaseTable())
- ->condition($this->idKey, $ids, 'IN')
- ->execute();
- if ($this->revisionTable) {
- $this->database->delete($this->revisionTable)
- ->condition($this->idKey, $ids, 'IN')
- ->execute();
- }
- if ($this->dataTable) {
- $this->database->delete($this->dataTable)
- ->condition($this->idKey, $ids, 'IN')
- ->execute();
- }
- if ($this->revisionDataTable) {
- $this->database->delete($this->revisionDataTable)
- ->condition($this->idKey, $ids, 'IN')
- ->execute();
- }
- foreach ($entities as $entity) {
- $this->deleteFromDedicatedTables($entity);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function save(EntityInterface $entity) {
- $transaction = $this->database->startTransaction();
- try {
- $return = parent::save($entity);
- // Ignore replica server temporarily.
- db_ignore_replica();
- return $return;
- }
- catch (\Exception $e) {
- $transaction->rollBack();
- watchdog_exception($this->entityTypeId, $e);
- throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
- $full_save = empty($names);
- $update = !$full_save || !$entity->isNew();
- if ($full_save) {
- $shared_table_fields = TRUE;
- $dedicated_table_fields = TRUE;
- }
- else {
- $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;
- }
- }
- }
- // 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) {
- $this->database
- ->update($this->baseTable)
- ->fields((array) $record)
- ->condition($this->idKey, $record->{$this->idKey})
- ->execute();
- }
- if ($this->revisionTable) {
- if ($full_save) {
- $entity->{$this->revisionKey} = $this->saveRevision($entity);
- }
- else {
- $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
- $entity->preSaveRevision($this, $record);
- $this->database
- ->update($this->revisionTable)
- ->fields((array) $record)
- ->condition($this->revisionKey, $record->{$this->revisionKey})
- ->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);
- }
- }
- else {
- $insert_id = $this->database
- ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
- ->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);
- }
- }
- }
- // Update dedicated table records if necessary.
- if ($dedicated_table_fields) {
- $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
- $this->saveToDedicatedTables($entity, $update, $names);
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function has($id, EntityInterface $entity) {
- return !$entity->isNew();
- }
- /**
- * Saves fields that use the shared tables.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- * @param string $table_name
- * (optional) The table name to save to. Defaults to the data table.
- * @param bool $new_revision
- * (optional) Whether we are dealing with a new revision. By default fetches
- * the information from the entity object.
- */
- protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
- if (!isset($table_name)) {
- $table_name = $this->dataTable;
- }
- if (!isset($new_revision)) {
- $new_revision = $entity->isNewRevision();
- }
- $revision = $table_name != $this->dataTable;
- if (!$revision || !$new_revision) {
- $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);
- $record = $this->mapToDataStorageRecord($translation, $table_name);
- $values = (array) $record;
- $query
- ->fields(array_keys($values))
- ->values($values);
- }
- $query->execute();
- }
- /**
- * Maps from an entity object to the storage record.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- * @param string $table_name
- * (optional) The table name to map records to. Defaults to the base table.
- *
- * @return \stdClass
- * The record to store.
- */
- protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
- if (!isset($table_name)) {
- $table_name = $this->baseTable;
- }
- $record = new \stdClass();
- $table_mapping = $this->getTableMapping();
- foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
- if (empty($this->getFieldStorageDefinitions()[$field_name])) {
- throw new EntityStorageException("Table mapping contains invalid field $field_name.");
- }
- $definition = $this->getFieldStorageDefinitions()[$field_name];
- $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
- // https://www.drupal.org/node/2232427.
- if (!$definition->getMainPropertyName() && count($columns) == 1) {
- $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
- }
- else {
- $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
- }
- if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
- $value = serialize($value);
- }
- // 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);
- if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
- $record->$schema_name = $value;
- }
- }
- }
- return $record;
- }
- /**
- * 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
- * TRUE if the column is serial, FALSE otherwise.
- *
- * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
- * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
- */
- 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;
- }
- /**
- * Maps from an entity object to the storage record of the field data.
- *
- * @param \Drupal\Core\Entity\EntityInterface $entity
- * The entity object.
- * @param string $table_name
- * (optional) The table name to map records to. Defaults to the data table.
- *
- * @return \stdClass
- * The record to store.
- */
- protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
- if (!isset($table_name)) {
- $table_name = $this->dataTable;
- }
- $record = $this->mapToStorageRecord($entity, $table_name);
- return $record;
- }
- /**
- * Saves an entity revision.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- *
- * @return int
- * The revision id.
- */
- protected function saveRevision(ContentEntityInterface $entity) {
- $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
- $entity->preSaveRevision($this, $record);
- if ($entity->isNewRevision()) {
- $insert_id = $this->database
- ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
- ->fields((array) $record)
- ->execute();
- // Even if this is a new revision, the revision ID key might have been
- // set in which case we should not override the provided revision ID.
- if (!isset($record->{$this->revisionKey})) {
- $record->{$this->revisionKey} = $insert_id;
- }
- if ($entity->isDefaultRevision()) {
- $this->database->update($this->entityType->getBaseTable())
- ->fields([$this->revisionKey => $record->{$this->revisionKey}])
- ->condition($this->idKey, $record->{$this->idKey})
- ->execute();
- }
- }
- else {
- $this->database
- ->update($this->revisionTable)
- ->fields((array) $record)
- ->condition($this->revisionKey, $record->{$this->revisionKey})
- ->execute();
- }
- // Make sure to update the new revision key for the entity.
- $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
- return $record->{$this->revisionKey};
- }
- /**
- * {@inheritdoc}
- */
- protected function getQueryServiceName() {
- return 'entity.query.sql';
- }
- /**
- * Loads values of fields stored in dedicated tables for a group of entities.
- *
- * @param array &$values
- * An array of values keyed by entity ID.
- * @param bool $load_from_revision
- * Flag to indicate whether revisions should be loaded or not.
- */
- protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
- if (empty($values)) {
- return;
- }
- // Collect entities ids, bundles and languages.
- $bundles = [];
- $ids = [];
- $default_langcodes = [];
- foreach ($values as $key => $entity_values) {
- $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
- $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
- if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
- $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
- }
- }
- // Collect impacted fields.
- $storage_definitions = [];
- $definitions = [];
- $table_mapping = $this->getTableMapping();
- foreach ($bundles as $bundle => $v) {
- $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
- foreach ($definitions[$bundle] as $field_name => $field_definition) {
- $storage_definition = $field_definition->getFieldStorageDefinition();
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $storage_definitions[$field_name] = $storage_definition;
- }
- }
- }
- // Load field data.
- $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
- foreach ($storage_definitions as $field_name => $storage_definition) {
- $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
- // Ensure that only values having valid languages are retrieved. Since we
- // are loading values for multiple entities, we cannot limit the query to
- // the available translations.
- $results = $this->database->select($table, 't')
- ->fields('t')
- ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
- ->condition('deleted', 0)
- ->condition('langcode', $langcodes, 'IN')
- ->orderBy('delta')
- ->execute();
- foreach ($results as $row) {
- $bundle = $row->bundle;
- $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
- // Field values in default language are stored with
- // LanguageInterface::LANGCODE_DEFAULT as key.
- $langcode = LanguageInterface::LANGCODE_DEFAULT;
- if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
- $langcode = $row->langcode;
- }
- if (!isset($values[$value_key][$field_name][$langcode])) {
- $values[$value_key][$field_name][$langcode] = [];
- }
- // Ensure that records for non-translatable fields having invalid
- // languages are skipped.
- if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
- if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
- $item = [];
- // For each column declared by the field, populate the item from the
- // prefixed database column.
- foreach ($storage_definition->getColumns() as $column => $attributes) {
- $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
- // Unserialize the value if specified in the column schema.
- $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
- }
- // Add the item to the field values for the entity.
- $values[$value_key][$field_name][$langcode][] = $item;
- }
- }
- }
- }
- }
- /**
- * Saves values of fields that use dedicated tables.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity.
- * @param bool $update
- * TRUE if the entity is being updated, FALSE if it is being inserted.
- * @param string[] $names
- * (optional) The names of the fields to be stored. Defaults to all the
- * available fields.
- */
- protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
- $vid = $entity->getRevisionId();
- $id = $entity->id();
- $bundle = $entity->bundle();
- $entity_type = $entity->getEntityTypeId();
- $default_langcode = $entity->getUntranslated()->language()->getId();
- $translation_langcodes = array_keys($entity->getTranslationLanguages());
- $table_mapping = $this->getTableMapping();
- if (!isset($vid)) {
- $vid = $id;
- }
- $original = !empty($entity->original) ? $entity->original : NULL;
- // Determine which fields should be actually stored.
- $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
- if ($names) {
- $definitions = array_intersect_key($definitions, array_flip($names));
- }
- foreach ($definitions as $field_name => $field_definition) {
- $storage_definition = $field_definition->getFieldStorageDefinition();
- if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- continue;
- }
- // When updating an existing revision, keep the existing records if the
- // field values did not change.
- if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
- continue;
- }
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- // Delete and insert, rather than update, in case a value was added.
- if ($update) {
- // Only overwrite the field's base table if saving the default revision
- // of an entity.
- if ($entity->isDefaultRevision()) {
- $this->database->delete($table_name)
- ->condition('entity_id', $id)
- ->execute();
- }
- if ($this->entityType->isRevisionable()) {
- $this->database->delete($revision_name)
- ->condition('entity_id', $id)
- ->condition('revision_id', $vid)
- ->execute();
- }
- }
- // Prepare the multi-insert query.
- $do_insert = FALSE;
- $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
- foreach ($storage_definition->getColumns() as $column => $attributes) {
- $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
- }
- $query = $this->database->insert($table_name)->fields($columns);
- if ($this->entityType->isRevisionable()) {
- $revision_query = $this->database->insert($revision_name)->fields($columns);
- }
- $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
- foreach ($langcodes as $langcode) {
- $delta_count = 0;
- $items = $entity->getTranslation($langcode)->get($field_name);
- $items->filterEmptyItems();
- foreach ($items as $delta => $item) {
- // We now know we have something to insert.
- $do_insert = TRUE;
- $record = [
- 'entity_id' => $id,
- 'revision_id' => $vid,
- 'bundle' => $bundle,
- 'delta' => $delta,
- 'langcode' => $langcode,
- ];
- foreach ($storage_definition->getColumns() as $column => $attributes) {
- $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
- // Serialize the value if specified in the column schema.
- $value = $item->$column;
- if (!empty($attributes['serialize'])) {
- $value = serialize($value);
- }
- $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
- }
- $query->values($record);
- if ($this->entityType->isRevisionable()) {
- $revision_query->values($record);
- }
- if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
- break;
- }
- }
- }
- // Execute the query if we have values to insert.
- if ($do_insert) {
- // Only overwrite the field's base table if saving the default revision
- // of an entity.
- if ($entity->isDefaultRevision()) {
- $query->execute();
- }
- if ($this->entityType->isRevisionable()) {
- $revision_query->execute();
- }
- }
- }
- }
- /**
- * Deletes values of fields in dedicated tables for all revisions.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity.
- */
- protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
- $table_mapping = $this->getTableMapping();
- foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
- $storage_definition = $field_definition->getFieldStorageDefinition();
- if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- continue;
- }
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- $this->database->delete($table_name)
- ->condition('entity_id', $entity->id())
- ->execute();
- if ($this->entityType->isRevisionable()) {
- $this->database->delete($revision_name)
- ->condition('entity_id', $entity->id())
- ->execute();
- }
- }
- }
- /**
- * Deletes values of fields in dedicated tables for all revisions.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity. It must have a revision ID.
- */
- protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
- $vid = $entity->getRevisionId();
- if (isset($vid)) {
- $table_mapping = $this->getTableMapping();
- foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
- $storage_definition = $field_definition->getFieldStorageDefinition();
- if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- continue;
- }
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- $this->database->delete($revision_name)
- ->condition('entity_id', $entity->id())
- ->condition('revision_id', $vid)
- ->execute();
- }
- }
- }
- /**
- * {@inheritdoc}
- */
- public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
- }
- /**
- * {@inheritdoc}
- */
- public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
- }
- /**
- * {@inheritdoc}
- */
- public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
- }
- /**
- * {@inheritdoc}
- */
- public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
- }
- /**
- * {@inheritdoc}
- */
- public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
- $this->wrapSchemaException(function () use ($entity_type) {
- $this->getStorageSchema()->onEntityTypeCreate($entity_type);
- });
- }
- /**
- * {@inheritdoc}
- */
- public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- // Ensure we have an updated entity type definition.
- $this->entityType = $entity_type;
- // The table layout may have changed depending on the new entity type
- // definition.
- $this->initTableLayout();
- // Let the schema handler adapt to possible table layout changes.
- $this->wrapSchemaException(function () use ($entity_type, $original) {
- $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
- });
- }
- /**
- * {@inheritdoc}
- */
- public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
- $this->wrapSchemaException(function () use ($entity_type) {
- $this->getStorageSchema()->onEntityTypeDelete($entity_type);
- });
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
- // If we are adding a field stored in a shared table we need to recompute
- // the table mapping.
- // @todo This does not belong here. Remove it once we are able to generate a
- // fresh table mapping in the schema handler. See
- // https://www.drupal.org/node/2274017.
- if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
- $this->tableMapping = NULL;
- }
- $this->wrapSchemaException(function () use ($storage_definition) {
- $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
- });
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- $this->wrapSchemaException(function () use ($storage_definition, $original) {
- $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
- });
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
- $table_mapping = $this->getTableMapping(
- $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
- );
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- // Mark all data associated with the field for deletion.
- $table = $table_mapping->getDedicatedDataTableName($storage_definition);
- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- $this->database->update($table)
- ->fields(['deleted' => 1])
- ->execute();
- if ($this->entityType->isRevisionable()) {
- $this->database->update($revision_table)
- ->fields(['deleted' => 1])
- ->execute();
- }
- }
- // Update the field schema.
- $this->wrapSchemaException(function () use ($storage_definition) {
- $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
- });
- }
- /**
- * Wraps a database schema exception into an entity storage exception.
- *
- * @param callable $callback
- * The callback to be executed.
- *
- * @throws \Drupal\Core\Entity\EntityStorageException
- * When a database schema exception is thrown.
- */
- protected function wrapSchemaException(callable $callback) {
- $message = 'Exception thrown while performing a schema update.';
- try {
- $callback();
- }
- catch (SchemaException $e) {
- $message .= ' ' . $e->getMessage();
- throw new EntityStorageException($message, 0, $e);
- }
- catch (DatabaseExceptionWrapper $e) {
- $message .= ' ' . $e->getMessage();
- throw new EntityStorageException($message, 0, $e);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
- $table_mapping = $this->getTableMapping();
- $storage_definition = $field_definition->getFieldStorageDefinition();
- // Mark field data as deleted.
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- $this->database->update($table_name)
- ->fields(['deleted' => 1])
- ->condition('bundle', $field_definition->getTargetBundle())
- ->execute();
- if ($this->entityType->isRevisionable()) {
- $this->database->update($revision_name)
- ->fields(['deleted' => 1])
- ->condition('bundle', $field_definition->getTargetBundle())
- ->execute();
- }
- }
- }
- /**
- * {@inheritdoc}
- */
- public function onBundleCreate($bundle, $entity_type_id) {}
- /**
- * {@inheritdoc}
- */
- public function onBundleDelete($bundle, $entity_type_id) {}
- /**
- * {@inheritdoc}
- */
- protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
- // Check whether the whole field storage definition is gone, or just some
- // bundle fields.
- $storage_definition = $field_definition->getFieldStorageDefinition();
- $table_mapping = $this->getTableMapping();
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
- // Get the entities which we want to purge first.
- $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
- $or = $entity_query->orConditionGroup();
- foreach ($storage_definition->getColumns() as $column_name => $data) {
- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
- }
- $entity_query
- ->distinct(TRUE)
- ->fields('t', ['entity_id'])
- ->condition('bundle', $field_definition->getTargetBundle())
- ->range(0, $batch_size);
- // Create a map of field data table column names to field column names.
- $column_map = [];
- foreach ($storage_definition->getColumns() as $column_name => $data) {
- $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
- }
- $entities = [];
- $items_by_entity = [];
- foreach ($entity_query->execute() as $row) {
- $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
- ->fields('t')
- ->condition('entity_id', $row['entity_id'])
- ->condition('deleted', 1)
- ->orderBy('delta');
- foreach ($item_query->execute() as $item_row) {
- if (!isset($entities[$item_row['revision_id']])) {
- // Create entity with the right revision id and entity id combination.
- $item_row['entity_type'] = $this->entityTypeId;
- // @todo: Replace this by an entity object created via an entity
- // factory, see https://www.drupal.org/node/1867228.
- $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
- }
- $item = [];
- foreach ($column_map as $db_column => $field_column) {
- $item[$field_column] = $item_row[$db_column];
- }
- $items_by_entity[$item_row['revision_id']][] = $item;
- }
- }
- // Create field item objects and return.
- foreach ($items_by_entity as $revision_id => $values) {
- $entity_adapter = $entities[$revision_id]->getTypedData();
- $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
- }
- return $items_by_entity;
- }
- /**
- * {@inheritdoc}
- */
- protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
- $storage_definition = $field_definition->getFieldStorageDefinition();
- $is_deleted = $storage_definition->isDeleted();
- $table_mapping = $this->getTableMapping();
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
- $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
- $this->database->delete($table_name)
- ->condition('revision_id', $revision_id)
- ->condition('deleted', 1)
- ->execute();
- if ($this->entityType->isRevisionable()) {
- $this->database->delete($revision_name)
- ->condition('revision_id', $revision_id)
- ->condition('deleted', 1)
- ->execute();
- }
- }
- /**
- * {@inheritdoc}
- */
- public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
- $this->getStorageSchema()->finalizePurge($storage_definition);
- }
- /**
- * {@inheritdoc}
- */
- public function countFieldData($storage_definition, $as_bool = FALSE) {
- // The table mapping contains stale data during a request when a field
- // storage definition is added, so bypass the internal storage definitions
- // and fetch the table mapping using the passed in storage definition.
- // @todo Fix this in https://www.drupal.org/node/2705205.
- $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- $storage_definitions[$storage_definition->getName()] = $storage_definition;
- $table_mapping = $this->getTableMapping($storage_definitions);
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $is_deleted = $storage_definition->isDeleted();
- if ($this->entityType->isRevisionable()) {
- $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
- }
- else {
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
- }
- $query = $this->database->select($table_name, 't');
- $or = $query->orConditionGroup();
- foreach ($storage_definition->getColumns() as $column_name => $data) {
- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
- }
- $query->condition($or);
- if (!$as_bool) {
- $query
- ->fields('t', ['entity_id'])
- ->distinct(TRUE);
- }
- }
- elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
- // Ascertain the table this field is mapped too.
- $field_name = $storage_definition->getName();
- try {
- $table_name = $table_mapping->getFieldTableName($field_name);
- }
- catch (SqlContentEntityStorageException $e) {
- // This may happen when changing field storage schema, since we are not
- // able to use a table mapping matching the passed storage definition.
- // @todo Revisit this once we are able to instantiate the table mapping
- // properly. See https://www.drupal.org/node/2274017.
- $table_name = $this->dataTable ?: $this->baseTable;
- }
- $query = $this->database->select($table_name, 't');
- $or = $query->orConditionGroup();
- foreach (array_keys($storage_definition->getColumns()) as $property_name) {
- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
- }
- $query->condition($or);
- if (!$as_bool) {
- $query
- ->fields('t', [$this->idKey])
- ->distinct(TRUE);
- }
- }
- // @todo Find a way to count field data also for fields having custom
- // storage. See https://www.drupal.org/node/2337753.
- $count = 0;
- if (isset($query)) {
- // If we are performing the query just to check if the field has data
- // limit the number of rows.
- if ($as_bool) {
- $query
- ->range(0, 1)
- ->addExpression('1');
- }
- else {
- // Otherwise count the number of rows.
- $query = $query->countQuery();
- }
- $count = $query->execute()->fetchField();
- }
- return $as_bool ? (bool) $count : (int) $count;
- }
- /**
- * Determines whether the passed field has been already deleted.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- *
- * @return bool
- * Whether the field has been already deleted.
- *
- * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
- * \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
- *
- * @see https://www.drupal.org/node/2907785
- */
- protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
- return $storage_definition->isDeleted();
- }
- }
|