Рубрики
Uncategorized

Доктрина: КАК восстановиться после отката транзакции

краткое сообщение, объясняющее, как восстановиться после отката транзакции при использовании библиотеки doctrine ORM. С тегами php, doctrine, orm, symfony.

Мы все хотели бы иметь дело только со счастливыми путями, но, к сожалению, на самом деле случаются ошибки, и мы должны с ними справляться.

Одним из способов устранения беспорядка после возникновения ошибки являются транзакции базы данных и, в частности, откаты. Транзакция – это мощный инструмент, позволяющий нашему приложению не оставлять базу данных в неизвестном или поврежденном состоянии.

К сожалению, большинство ORM не очень хорошо оснащены для борьбы с откатами.

Давайте взглянем на небольшой пример из реального мира. Нашему приложению необходимо отправить несколько электронных писем клиентам и записать, что электронное письмо было отправлено. Обработка выполняется пакетно, выполняя действие для одного клиента за раз.

public function sendWeeklyNewsletter(): void
{
    $customers = $repository->findAll();

    foreach ($customers as $customer) {
        try {
            $entityManager->transactional(
                function () use ($customer) {
                    $this->sendWeeklyNewsletter($customer);
                    $this->recordEmailNotification($customer);
                }
            );
        } catch (EmailNotTransmittedException $e) {

        }
    }
}

Допустим, первое электронное письмо не удалось отправить (случайные сбои, неверная электронная почта и т. Д.), Вы бы подумали, что оно должно работать, транзакционный откатит транзакцию и продолжит обработку следующего клиента.

К сожалению, это не так, вы были бы вознаграждены этим исключением Фатальная ошибка PHP: Неперехваченная доктрина\ORM\ORMException: EntityManager закрыт. .

Взглянув на код EntityManager::транзакционный , можно было бы объяснить, почему.

    /**
     * {@inheritDoc}
     */
    public function transactional($func)
    {
        if (!is_callable($func)) {
            throw new \InvalidArgumentException('Expected argument of type "callable", got "' . gettype($func) . '"');
        }

        $this->conn->beginTransaction();

        try {
            $return = call_user_func($func, $this);

            $this->flush();
            $this->conn->commit();

            return $return ?: true;
        } catch (Throwable $e) {
            $this->close();
            $this->conn->rollBack();

            throw $e;
        }
    }

В основном потому, что иметь дело с откатом сложно. Разработчики доктрины решают закрыть диспетчер сущностей, как только произойдет откат. В документации содержится такое предупреждение:

В результате этой процедуры все ранее управляемые или удаленные экземпляры EntityManager отсоединяются. Состояние отсоединенных объектов будет соответствовать состоянию на момент отката транзакции. Состояние объектов никоим образом не откатывается, и поэтому объекты теперь не синхронизированы с базой данных. Приложение может продолжать использовать отсоединенные объекты, зная, что их состояние потенциально больше не является точным. Если вы намерены запустить другую единицу работы после возникновения исключения, вам следует сделать это с помощью нового EntityManager.

TD;LR вы не можете использовать диспетчер сущностей после того, как произошел откат.

Теперь, как мы выполняем нашу пакетную обработку, если мы не можем оправиться от ошибок!?!?

Нам нужно создавать нового менеджера сущностей каждый раз, когда происходит откат. Это может усложниться, если вы используете контейнер, потому что он будет использовать один и тот же экземпляр entitymanager везде.

Взгляните на Абстрактный менеджер реестра , , в этом реестре есть метод reset Manager

В любом случае, если вы используете платформу symfony, пакет doctrine bridge предоставляет реализацию реестра doctrine и позволяет сбросить диспетчер сущностей. Я почти уверен, что интеграция с другими фреймворками должна обеспечивать ту же функцию.

Таким образом, наш код становится:

public function sendWeeklyNewsletter(): void
{
    $customers = $repository->findAll();

    foreach ($customers as $customer) {
        try {
            $entityManager->transactional(
                function () use ($customer) {
                    $this->sendWeeklyNewsletter($customer);
                    $this->recordEmailNotification($customer);
                }
            );
        } catch (EmailNotTransmittedException $e) {
            $this->registry->resetManager();
        }
    }
}

Внимательный читатель, возможно, уловил проблему в нашей логике. Менеджер сущностей уже перешел в наш класс, поэтому его нельзя поменять местами. Фактически пакет symfony doctrine использует замечательную библиотеку proxy manager , написанную Ocramius, для упаковки объекта EntityManager, чтобы его можно было динамически менять местами.

В любом случае, вы можете взглянуть на реализацию реестра.

Счастливой пакетной обработки!

Оригинал: “https://dev.to/ragezbla/doctrine-how-to-recover-from-rolled-back-transaction-47o4”