SqlContentEntityStorage.php 61 KB

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