src/Core/Framework/DataAbstractionLayer/Dbal/EntityWriteGateway.php line 111

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
  3. use Doctrine\DBAL\Connection;
  4. use Doctrine\DBAL\Exception;
  5. use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilderAlias;
  6. use Doctrine\DBAL\Types\Types;
  7. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableTransaction;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityTranslationDefinition;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Exception\CanNotFindParentStorageFieldException;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InvalidParentAssociationException;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldForeignKeyConstraintMissingException;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldNotFoundException;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Exception\PrimaryKeyNotProvidedException;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Exception\UnsupportedCommandTypeException;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
  25. use Shopware\Core\Framework\DataAbstractionLayer\MappingEntityDefinition;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSet;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\JsonUpdateCommand;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;
  34. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
  35. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriteGatewayInterface;
  36. use Shopware\Core\Framework\DataAbstractionLayer\Write\PrimaryKeyBag;
  37. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;
  38. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  39. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\WriteCommandExceptionEvent;
  40. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
  41. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
  42. use Shopware\Core\Framework\Log\Package;
  43. use Shopware\Core\Framework\Uuid\Uuid;
  44. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  45. /**
  46.  * @internal
  47.  */
  48. #[Package('core')]
  49. class EntityWriteGateway implements EntityWriteGatewayInterface
  50. {
  51.     private ?PrimaryKeyBag $primaryKeyBag null;
  52.     public function __construct(private readonly int $batchSize, private readonly Connection $connection, private readonly EventDispatcherInterface $eventDispatcher, private readonly ExceptionHandlerRegistry $exceptionHandlerRegistry, private readonly DefinitionInstanceRegistry $definitionInstanceRegistry)
  53.     {
  54.     }
  55.     public function prefetchExistences(WriteParameterBag $parameters): void
  56.     {
  57.         $primaryKeyBag $this->primaryKeyBag $parameters->getPrimaryKeyBag();
  58.         if ($primaryKeyBag->isPrefetchingCompleted()) {
  59.             return;
  60.         }
  61.         foreach ($primaryKeyBag->getPrimaryKeys() as $entity => $pks) {
  62.             $this->prefetch($this->definitionInstanceRegistry->getByEntityName($entity), $pks$parameters);
  63.         }
  64.         $primaryKeyBag->setPrefetchingCompleted(true);
  65.     }
  66.     /**
  67.      * {@inheritdoc}
  68.      */
  69.     public function getExistence(EntityDefinition $definition, array $primaryKey, array $dataWriteCommandQueue $commandQueue): EntityExistence
  70.     {
  71.         $state $this->getCurrentState($definition$primaryKey$commandQueue);
  72.         $exists = !empty($state);
  73.         $isChild $this->isChild($definition$data$state$primaryKey$commandQueue);
  74.         $wasChild $this->wasChild($definition$state);
  75.         return new EntityExistence($definition->getEntityName(), Uuid::fromBytesToHexList($primaryKey), $exists$isChild$wasChild$state);
  76.     }
  77.     /**
  78.      * {@inheritdoc}
  79.      */
  80.     public function execute(array $commandsWriteContext $context): void
  81.     {
  82.         try {
  83.             RetryableTransaction::retryable($this->connection, function () use ($commands$context): void {
  84.                 $this->executeCommands($commands$context);
  85.             });
  86.         } catch (\Throwable $e) {
  87.             $event = new WriteCommandExceptionEvent($e$commands$context->getContext());
  88.             $this->eventDispatcher->dispatch($event);
  89.             throw $e;
  90.         }
  91.     }
  92.     /**
  93.      * @param list<WriteCommand> $commands
  94.      */
  95.     private function executeCommands(array $commandsWriteContext $context): void
  96.     {
  97.         $beforeDeleteEvent BeforeDeleteEvent::create($context$commands);
  98.         if ($beforeDeleteEvent->filled()) {
  99.             $this->eventDispatcher->dispatch($beforeDeleteEvent);
  100.         }
  101.         // throws exception on violation and then aborts/rollbacks this transaction
  102.         $event = new PreWriteValidationEvent($context$commands);
  103.         $this->eventDispatcher->dispatch($event);
  104.         $this->generateChangeSets($commands);
  105.         $context->getExceptions()->tryToThrow();
  106.         $previous null;
  107.         $mappings = new MultiInsertQueryQueue($this->connection$this->batchSizefalsetrue);
  108.         $inserts = new MultiInsertQueryQueue($this->connection$this->batchSize);
  109.         $executeInserts = function () use ($mappings$inserts): void {
  110.             $mappings->execute();
  111.             $inserts->execute();
  112.         };
  113.         try {
  114.             foreach ($commands as $command) {
  115.                 if (!$command->isValid()) {
  116.                     continue;
  117.                 }
  118.                 $command->setFailed(false);
  119.                 $current $command->getDefinition()->getEntityName();
  120.                 if ($current !== $previous) {
  121.                     $executeInserts();
  122.                 }
  123.                 $previous $current;
  124.                 try {
  125.                     $definition $command->getDefinition();
  126.                     $table $definition->getEntityName();
  127.                     if ($command instanceof DeleteCommand) {
  128.                         $executeInserts();
  129.                         RetryableQuery::retryable($this->connection, function () use ($command$table): void {
  130.                             $this->connection->delete(EntityDefinitionQueryHelper::escape($table), $command->getPrimaryKey());
  131.                         });
  132.                         continue;
  133.                     }
  134.                     if ($command instanceof JsonUpdateCommand) {
  135.                         $executeInserts();
  136.                         $this->executeJsonUpdate($command);
  137.                         continue;
  138.                     }
  139.                     if ($definition instanceof MappingEntityDefinition && $command instanceof InsertCommand) {
  140.                         $mappings->addInsert($definition->getEntityName(), $command->getPayload());
  141.                         continue;
  142.                     }
  143.                     if ($command instanceof UpdateCommand) {
  144.                         $executeInserts();
  145.                         RetryableQuery::retryable($this->connection, function () use ($command$table): void {
  146.                             $this->connection->update(
  147.                                 EntityDefinitionQueryHelper::escape($table),
  148.                                 $this->escapeColumnKeys($command->getPayload()),
  149.                                 $command->getPrimaryKey()
  150.                             );
  151.                         });
  152.                         continue;
  153.                     }
  154.                     if ($command instanceof InsertCommand) {
  155.                         $inserts->addInsert($definition->getEntityName(), $command->getPayload());
  156.                         continue;
  157.                     }
  158.                     throw new UnsupportedCommandTypeException($command);
  159.                 } catch (\Exception $e) {
  160.                     $command->setFailed(true);
  161.                     $innerException $this->exceptionHandlerRegistry->matchException($e);
  162.                     if ($innerException instanceof \Exception) {
  163.                         $e $innerException;
  164.                     }
  165.                     $context->getExceptions()->add($e);
  166.                     throw $e;
  167.                 }
  168.             }
  169.             $mappings->execute();
  170.             $inserts->execute();
  171.             $beforeDeleteEvent->success();
  172.         } catch (Exception $e) {
  173.             // Match exception without passing a specific command when feature-flag 16640 is active
  174.             $innerException $this->exceptionHandlerRegistry->matchException($e);
  175.             if ($innerException instanceof \Exception) {
  176.                 $e $innerException;
  177.             }
  178.             $context->getExceptions()->add($e);
  179.             $beforeDeleteEvent->error();
  180.             throw $e;
  181.         }
  182.         // throws exception on violation and then aborts/rollbacks this transaction
  183.         $event = new PostWriteValidationEvent($context$commands);
  184.         $this->eventDispatcher->dispatch($event);
  185.         $context->getExceptions()->tryToThrow();
  186.     }
  187.     /**
  188.      * @param list<array<string, string>> $pks
  189.      */
  190.     private function prefetch(EntityDefinition $definition, array $pksWriteParameterBag $parameters): void
  191.     {
  192.         $pkFields = [];
  193.         $versionField null;
  194.         /** @var StorageAware&Field $field */
  195.         foreach ($definition->getPrimaryKeys() as $field) {
  196.             if ($field instanceof VersionField) {
  197.                 $versionField $field;
  198.                 continue;
  199.             }
  200.             if ($field instanceof StorageAware) {
  201.                 $pkFields[$field->getStorageName()] = $field;
  202.             }
  203.         }
  204.         $query $this->connection->createQueryBuilder();
  205.         $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
  206.         $query->addSelect('1 as `exists`');
  207.         if ($definition->isChildrenAware()) {
  208.             $query->addSelect('parent_id');
  209.         } elseif ($definition->isInheritanceAware()) {
  210.             $parent $this->getParentField($definition);
  211.             if ($parent !== null) {
  212.                 $query->addSelect(
  213.                     EntityDefinitionQueryHelper::escape($parent->getStorageName())
  214.                     . ' as `parent`'
  215.                 );
  216.             }
  217.         }
  218.         foreach ($pkFields as $storageName => $_) {
  219.             $query->addSelect(EntityDefinitionQueryHelper::escape($storageName));
  220.         }
  221.         if ($versionField) {
  222.             $query->addSelect(EntityDefinitionQueryHelper::escape($versionField->getStorageName()));
  223.         }
  224.         $chunks array_chunk($pks500true);
  225.         foreach ($chunks as $pks) {
  226.             $query->resetQueryPart('where');
  227.             $params = [];
  228.             $tupleCount 0;
  229.             foreach ($pks as $pk) {
  230.                 $newIds = [];
  231.                 /** @var Field&StorageAware $field */
  232.                 foreach ($pkFields as $field) {
  233.                     $id $pk[$field->getPropertyName()] ?? null;
  234.                     if ($id === null) {
  235.                         continue 2;
  236.                     }
  237.                     $newIds[] = Uuid::fromHexToBytes($id);
  238.                 }
  239.                 foreach ($newIds as $newId) {
  240.                     $params[] = $newId;
  241.                 }
  242.                 ++$tupleCount;
  243.             }
  244.             if ($tupleCount <= 0) {
  245.                 continue;
  246.             }
  247.             $placeholders $this->getPlaceholders(\count($pkFields), $tupleCount);
  248.             $columns '`' implode('`,`'array_keys($pkFields)) . '`';
  249.             if (\count($pkFields) > 1) {
  250.                 $columns '(' $columns ')';
  251.             }
  252.             $query->andWhere($columns ' IN (' $placeholders ')');
  253.             if ($versionField) {
  254.                 $query->andWhere('version_id = ?');
  255.                 $params[] = Uuid::fromHexToBytes($parameters->getContext()->getContext()->getVersionId());
  256.             }
  257.             $query->setParameters($params);
  258.             $result $query->executeQuery()->fetchAllAssociative();
  259.             $primaryKeyBag $parameters->getPrimaryKeyBag();
  260.             foreach ($result as $state) {
  261.                 $values = [];
  262.                 foreach ($pkFields as $storageKey => $field) {
  263.                     $values[$field->getPropertyName()] = Uuid::fromBytesToHex($state[$storageKey]);
  264.                 }
  265.                 if ($versionField) {
  266.                     $values[$versionField->getPropertyName()] = $parameters->getContext()->getContext()->getVersionId();
  267.                 }
  268.                 $primaryKeyBag->addExistenceState($definition$values$state);
  269.             }
  270.             foreach ($pks as $pk) {
  271.                 if (!$primaryKeyBag->hasExistence($definition$pk)) {
  272.                     $primaryKeyBag->addExistenceState($definition$pk, []);
  273.                 }
  274.             }
  275.         }
  276.     }
  277.     /**
  278.      * @param array<mixed> $array
  279.      */
  280.     private static function isAssociative(array $array): bool
  281.     {
  282.         foreach ($array as $key => $_value) {
  283.             if (!\is_int($key)) {
  284.                 return true;
  285.             }
  286.         }
  287.         return false;
  288.     }
  289.     private function executeJsonUpdate(JsonUpdateCommand $command): void
  290.     {
  291.         /*
  292.          * mysql json functions are tricky.
  293.          *
  294.          * TL;DR: cast objects and arrays to json
  295.          *
  296.          * This works:
  297.          *
  298.          * SELECT JSON_SET('{"a": "b"}', '$.a', 7)
  299.          * SELECT JSON_SET('{"a": "b"}', '$.a', "str")
  300.          *
  301.          * This does NOT work:
  302.          *
  303.          * SELECT JSON_SET('{"a": "b"}', '$.a', '{"foo": "bar"}')
  304.          *
  305.          * Instead, you have to do this, because mysql cannot differentiate between a string and a json string:
  306.          *
  307.          * SELECT JSON_SET('{"a": "b"}', '$.a', CAST('{"foo": "bar"}' AS json))
  308.          * SELECT JSON_SET('{"a": "b"}', '$.a', CAST('["foo", "bar"]' AS json))
  309.          *
  310.          * Yet this does NOT work:
  311.          *
  312.          * SELECT JSON_SET('{"a": "b"}', '$.a', CAST("str" AS json))
  313.          *
  314.          */
  315.         $values = [];
  316.         $sets = [];
  317.         $types = [];
  318.         $query = new QueryBuilder($this->connection);
  319.         $query->update('`' $command->getDefinition()->getEntityName() . '`');
  320.         foreach ($command->getPayload() as $attribute => $value) {
  321.             // add path and value for each attribute value pair
  322.             $values[] = '$."' $attribute '"';
  323.             $types[] = Types::STRING;
  324.             if (\is_array($value) || \is_object($value)) {
  325.                 $types[] = Types::STRING;
  326.                 $values[] = json_encode($value\JSON_PRESERVE_ZERO_FRACTION \JSON_UNESCAPED_UNICODE);
  327.                 // does the same thing as CAST(?, json) but works on mariadb
  328.                 $identityValue \is_object($value) || self::isAssociative($value) ? '{}' '[]';
  329.                 $sets[] = '?, JSON_MERGE("' $identityValue '", ?)';
  330.             } else {
  331.                 if (!\is_bool($value)) {
  332.                     $values[] = $value;
  333.                 }
  334.                 $set '?, ?';
  335.                 if (\is_float($value)) {
  336.                     $types[] = \PDO::PARAM_STR;
  337.                     $set '?, ? + 0.0';
  338.                 } elseif (\is_int($value)) {
  339.                     $types[] = \PDO::PARAM_INT;
  340.                 } elseif (\is_bool($value)) {
  341.                     $set '?, ' . ($value 'true' 'false');
  342.                 } else {
  343.                     $types[] = \PDO::PARAM_STR;
  344.                 }
  345.                 $sets[] = $set;
  346.             }
  347.         }
  348.         $storageName $command->getStorageName();
  349.         $query->set(
  350.             $storageName,
  351.             sprintf(
  352.                 'JSON_SET(IFNULL(%s, "{}"), %s)',
  353.                 EntityDefinitionQueryHelper::escape($storageName),
  354.                 implode(', '$sets)
  355.             )
  356.         );
  357.         $identifier $command->getPrimaryKey();
  358.         foreach ($identifier as $key => $_value) {
  359.             $query->andWhere(EntityDefinitionQueryHelper::escape($key) . ' = ?');
  360.         }
  361.         $query->setParameters([...$values, ...array_values($identifier)], $types);
  362.         RetryableQuery::retryable($this->connection, function () use ($query): void {
  363.             $query->executeStatement();
  364.         });
  365.     }
  366.     /**
  367.      * @param array<string, mixed> $payload
  368.      *
  369.      * @return array<string, mixed>
  370.      */
  371.     private function escapeColumnKeys(array $payload): array
  372.     {
  373.         $escaped = [];
  374.         foreach ($payload as $key => $value) {
  375.             $escaped[EntityDefinitionQueryHelper::escape($key)] = $value;
  376.         }
  377.         return $escaped;
  378.     }
  379.     /**
  380.      * @param list<WriteCommand> $commands
  381.      */
  382.     private function generateChangeSets(array $commands): void
  383.     {
  384.         $primaryKeys = [];
  385.         $definitions = [];
  386.         foreach ($commands as $command) {
  387.             if (!$command instanceof ChangeSetAware || !$command instanceof WriteCommand) {
  388.                 continue;
  389.             }
  390.             if (!$command->requiresChangeSet()) {
  391.                 continue;
  392.             }
  393.             $entity $command->getDefinition()->getEntityName();
  394.             $primaryKeys[$entity][] = $command->getPrimaryKey();
  395.             $definitions[$entity] = $command->getDefinition();
  396.         }
  397.         if (empty($primaryKeys)) {
  398.             return;
  399.         }
  400.         $states = [];
  401.         foreach ($primaryKeys as $entity => $ids) {
  402.             $query $this->connection->createQueryBuilder();
  403.             $definition $definitions[$entity];
  404.             $query->addSelect('*');
  405.             $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
  406.             $this->addPrimaryCondition($query$ids);
  407.             $states[$entity] = $query->executeQuery()->fetchAllAssociative();
  408.         }
  409.         foreach ($commands as $command) {
  410.             if (!$command instanceof ChangeSetAware || !$command instanceof WriteCommand) {
  411.                 continue;
  412.             }
  413.             if (!$command->requiresChangeSet()) {
  414.                 continue;
  415.             }
  416.             $entity $command->getDefinition()->getEntityName();
  417.             $command->setChangeSet(
  418.                 $this->calculateChangeSet($command$states[$entity])
  419.             );
  420.         }
  421.     }
  422.     /**
  423.      * @param list<array<string, string>> $primaryKeys
  424.      */
  425.     private function addPrimaryCondition(DbalQueryBuilderAlias $query, array $primaryKeys): void
  426.     {
  427.         $all = [];
  428.         $i 0;
  429.         foreach ($primaryKeys as $primaryKey) {
  430.             $where = [];
  431.             foreach ($primaryKey as $field => $value) {
  432.                 ++$i;
  433.                 $field EntityDefinitionQueryHelper::escape($field);
  434.                 $where[] = $field ' = :param' $i;
  435.                 $query->setParameter('param' $i$value);
  436.             }
  437.             $all[] = implode(' AND '$where);
  438.         }
  439.         $query->andWhere(implode(' OR '$all));
  440.     }
  441.     /**
  442.      * @param list<array<string, mixed>> $states
  443.      */
  444.     private function calculateChangeSet(WriteCommand $command, array $states): ChangeSet
  445.     {
  446.         foreach ($states as $state) {
  447.             // check if current loop matches the command primary key
  448.             $primaryKey array_intersect($command->getPrimaryKey(), $state);
  449.             if (\count(array_diff_assoc($command->getPrimaryKey(), $primaryKey)) === 0) {
  450.                 return new ChangeSet($state$command->getPayload(), $command instanceof DeleteCommand);
  451.             }
  452.         }
  453.         return new ChangeSet([], [], $command instanceof DeleteCommand);
  454.     }
  455.     private function getPlaceholders(int $columnCountint $tupleCount): string
  456.     {
  457.         if ($columnCount 1) {
  458.             // multi column pk. Example: (product_id, language_id) IN ((p1, l1), (p2, l2), (px,lx),...)
  459.             $tupleStr '(?' str_repeat(',?'$columnCount 1) . ')';
  460.         } else {
  461.             // single column pk. Example: category_id IN (c1, c2, c3,...)
  462.             $tupleStr '?';
  463.         }
  464.         return $tupleStr str_repeat(',' $tupleStr$tupleCount 1);
  465.     }
  466.     private function getParentField(EntityDefinition $definition): ?FkField
  467.     {
  468.         if (!$definition->isInheritanceAware()) {
  469.             return null;
  470.         }
  471.         /** @var ManyToOneAssociationField|null $parent */
  472.         $parent $definition->getFields()->get('parent');
  473.         if (!$parent) {
  474.             throw new ParentFieldNotFoundException($definition);
  475.         }
  476.         if (!$parent instanceof ManyToOneAssociationField) {
  477.             throw new InvalidParentAssociationException($definition$parent);
  478.         }
  479.         $fk $definition->getFields()->getByStorageName($parent->getStorageName());
  480.         if (!$fk) {
  481.             throw new CanNotFindParentStorageFieldException($definition);
  482.         }
  483.         if (!$fk instanceof FkField) {
  484.             throw new ParentFieldForeignKeyConstraintMissingException($definition$fk);
  485.         }
  486.         return $fk;
  487.     }
  488.     /**
  489.      * @param array<string, string> $primaryKey
  490.      *
  491.      * @return array<string, mixed>
  492.      */
  493.     private function getCurrentState(EntityDefinition $definition, array $primaryKeyWriteCommandQueue $commandQueue): array
  494.     {
  495.         $commands $commandQueue->getCommandsForEntity($definition$primaryKey);
  496.         $useDatabase true;
  497.         $state = [];
  498.         foreach ($commands as $command) {
  499.             if ($command instanceof DeleteCommand) {
  500.                 $state = [];
  501.                 $useDatabase false;
  502.                 continue;
  503.             }
  504.             if (!$command instanceof InsertCommand && !$command instanceof UpdateCommand) {
  505.                 continue;
  506.             }
  507.             $state array_replace_recursive($state$command->getPayload());
  508.             if ($command instanceof InsertCommand) {
  509.                 $useDatabase false;
  510.             }
  511.         }
  512.         if (!$useDatabase) {
  513.             return $state;
  514.         }
  515.         $hexPrimaryKey Uuid::fromBytesToHexList($primaryKey);
  516.         $currentState $this->primaryKeyBag === null null $this->primaryKeyBag->getExistenceState($definition$hexPrimaryKey);
  517.         if ($currentState === null) {
  518.             $currentState $this->fetchFromDatabase($definition$primaryKey);
  519.         }
  520.         $parent $this->getParentField($definition);
  521.         if ($parent && \array_key_exists('parent'$currentState)) {
  522.             $currentState[$parent->getStorageName()] = $currentState['parent'];
  523.             unset($currentState['parent']);
  524.         }
  525.         return array_replace_recursive($currentState$state);
  526.     }
  527.     /**
  528.      * @param array<string, string> $primaryKey
  529.      *
  530.      * @return array<string, mixed>
  531.      */
  532.     private function fetchFromDatabase(EntityDefinition $definition, array $primaryKey): array
  533.     {
  534.         $query $this->connection->createQueryBuilder();
  535.         $query->from(EntityDefinitionQueryHelper::escape($definition->getEntityName()));
  536.         $fields $definition->getPrimaryKeys();
  537.         /** @var Field&StorageAware $field */
  538.         foreach ($fields as $field) {
  539.             if (!\array_key_exists($field->getStorageName(), $primaryKey)) {
  540.                 if (!\array_key_exists($field->getPropertyName(), $primaryKey)) {
  541.                     throw new PrimaryKeyNotProvidedException($definition$field);
  542.                 }
  543.                 $primaryKey[$field->getStorageName()] = $primaryKey[$field->getPropertyName()];
  544.                 unset($primaryKey[$field->getPropertyName()]);
  545.             }
  546.             $param 'param_' Uuid::randomHex();
  547.             $query->andWhere(EntityDefinitionQueryHelper::escape($field->getStorageName()) . ' = :' $param);
  548.             $query->setParameter($param$primaryKey[$field->getStorageName()]);
  549.         }
  550.         $query->addSelect('1 as `exists`');
  551.         if ($definition->isChildrenAware()) {
  552.             $query->addSelect('parent_id');
  553.         } elseif ($definition->isInheritanceAware()) {
  554.             $parent $this->getParentField($definition);
  555.             if ($parent !== null) {
  556.                 $query->addSelect(
  557.                     EntityDefinitionQueryHelper::escape($parent->getStorageName())
  558.                     . ' as `parent`'
  559.                 );
  560.             }
  561.         }
  562.         $exists $query->executeQuery()->fetchAssociative();
  563.         if (!$exists) {
  564.             $exists = [];
  565.         }
  566.         return $exists;
  567.     }
  568.     /**
  569.      * @param array<string, mixed> $data
  570.      * @param array<string, mixed> $state
  571.      * @param array<string, string> $primaryKey
  572.      */
  573.     private function isChild(EntityDefinition $definition, array $data, array $state, array $primaryKeyWriteCommandQueue $commandQueue): bool
  574.     {
  575.         if ($definition instanceof EntityTranslationDefinition) {
  576.             return $this->isTranslationChild($definition$primaryKey$commandQueue);
  577.         }
  578.         if (!$definition->isInheritanceAware()) {
  579.             return false;
  580.         }
  581.         /** @var Field&StorageAware $fk */
  582.         $fk $this->getParentField($definition);
  583.         //foreign key provided, !== null has parent otherwise not
  584.         if (\array_key_exists($fk->getPropertyName(), $data)) {
  585.             return isset($data[$fk->getPropertyName()]);
  586.         }
  587.         /** @var Field $association */
  588.         $association $definition->getFields()->get('parent');
  589.         if (isset($data[$association->getPropertyName()])) {
  590.             return true;
  591.         }
  592.         return isset($state[$fk->getStorageName()]);
  593.     }
  594.     /**
  595.      * @param array<string, mixed> $state
  596.      */
  597.     private function wasChild(EntityDefinition $definition, array $state): bool
  598.     {
  599.         if (!$definition->isInheritanceAware()) {
  600.             return false;
  601.         }
  602.         $fk $this->getParentField($definition);
  603.         return $fk !== null && isset($state[$fk->getStorageName()]);
  604.     }
  605.     /**
  606.      * @param array<string, string> $primaryKey
  607.      */
  608.     private function isTranslationChild(EntityTranslationDefinition $definition, array $primaryKeyWriteCommandQueue $commandQueue): bool
  609.     {
  610.         $parent $definition->getParentDefinition();
  611.         if (!$parent->isInheritanceAware()) {
  612.             return false;
  613.         }
  614.         /** @var FkField $fkField */
  615.         $fkField $definition->getFields()->getByStorageName(
  616.             $parent->getEntityName() . '_id'
  617.         );
  618.         $parentPrimaryKey = [
  619.             'id' => $primaryKey[$fkField->getStorageName()],
  620.         ];
  621.         if ($parent->isVersionAware()) {
  622.             $parentPrimaryKey['versionId'] = $primaryKey[$parent->getEntityName() . '_version_id'];
  623.         }
  624.         $existence $this->getExistence($parent$parentPrimaryKey, [], $commandQueue);
  625.         return $existence->isChild();
  626.     }
  627. }