Непоследовательное поведение прослушивателя сущностей Doctrine

Я создаю небольшое приложение, используя Symfony 4 и Doctrine. Есть пользователи (сущности User), и они владеют каким-то контентом, называемым радиотаблицами (сущность RadioTable). Таблицы Radio содержат радиостанции (сущность RadioStation). RadioStation.radioTableId связан с RadioTable (многие к одному), а RadioTable.ownerId связан с пользователем (многие к одному).

Может быть, я должен заметить, что это мой первый проект с SF.

Сущности настраиваются с помощью аннотаций следующим образом:

<?php 

namespace App\Entity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User implements UserInterface, \Serializable, EncoderAwareInterface
{
    /**
     * @ORM\OneToMany(targetEntity="App\Entity\RadioTable", mappedBy="owner", orphanRemoval=true)
     */
    private $radioTables;

    /**
     * @ORM\Column(type="date")
     */
    private $lastActivityDate;
}

// -----------------

namespace App\Entity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RadioTableRepository")
 * @ORM\EntityListeners({"App\EventListener\RadioTableListener"})
 */
class RadioTable
{
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="radioTables")
     * @ORM\JoinColumn(nullable=false, onDelete="cascade")
     */
    private $owner;

    /**
     * @ORM\Column(type="datetime")
     */
    private $lastUpdateTime;
}

// -----------------

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RadioStationRepository")
 * @ORM\EntityListeners({"App\EventListener\RadioStationListener"})
 */
class RadioStation
{
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\RadioTable")
     * @ORM\JoinColumn(nullable=false, onDelete="cascade")
     */
    private $radioTable;
}

Мне нужно обновить $lastUpdateTime в соответствующем объекте RadioTable при добавлении, удалении или изменении радиостанций. Кроме того, мне нужно обновить $lastActivityDate владельца радиотаблицы (класс пользователя), когда радиотаблица создается, удаляется или обновляется. Я пытаюсь добиться этого, используя прослушиватели сущностей:

<?php

namespace App\EventListener;

class RadioStationListener
{
    /**
     * @PreFlush
     * @PreRemove
     */
    public function refreshLastUpdateTimeOfRadioTable(RadioStation $radioStation)
    {
        $radioStation->getRadioTable()->refreshLastUpdateTime();
    }
}

// -----------------------------

namespace App\EventListener;

class RadioTableListener
{
    /**
     * @PreFlush
     * @PreRemove
     */
    public function refreshLastActivityDateOfUser(RadioTable $radioTable, PreFlushEventArgs $args)
    {
        $radioTable->getOwner()->refreshLastActivityDate();

        /* hack */
        $args->getEntityManager()->flush($radioTable->getOwner());
        /* hack */
    }
}

(В методах refresh*() я просто создаю новый экземпляр \DateTime для соответствующего поля сущности.)

Я столкнулся с проблемой. Когда я пытался обновить/удалить/создать радиостанции, прослушиватель RadioStation работал правильно, и соответствующий класс RadioTable был успешно обновлен. Но когда я попытался обновить таблицу радио, класс пользователя был обновлен, но не был сохранен в базе данных Doctrine.

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

См. также:  Почему Symfony Guard запускает событие security.interactive_login при каждом запросе?

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

Когда я временно взломал код контроллера, чтобы отключить безопасность, и попытался обновить таблицу радио как анонимную, прослушиватель сущности RadioTable работал правильно, и сущность пользователя была успешно изменена и сохранена в базе данных.

Чтобы решить эту проблему, мне нужно вручную поговорить с менеджером сущностей Doctrine и вызвать flush() с сущностью пользователя в качестве аргумента (без аргумента я делаю бесконечный цикл). Эта строка отмечена комментарием /* hack */.

После этой долгой истории хочется задать вопрос: ПОЧЕМУ я должен это делать? ПОЧЕМУ я должен вручную вызывать flush() для объекта пользователя, но только если используется компонент безопасности и пользователь вошел в систему?

Было бы полезно увидеть ваши refresh*() функции.   —  person TomaszGasior    schedule 14.09.2018

Я подозреваю, что разница заключается в том, что вы вызываете их в середине события жизненного цикла на уровне сущности (а не в прослушивателе, например). Может быть, что ваша RadioStation обрабатывается до RadioTable, так что изменения в RadioTable затем сохраняются, тогда как User обрабатывается до RadioStation, поэтому после обработки RadioStation (с уже обработанным User) вам нужно вручную перейти назад, чтобы сохранить пользователя. Просто предположение. Отладка/трассировка поможет ответить.   —  person TomaszGasior    schedule 14.09.2018

Понравилась статья? Поделиться с друзьями:
IT Шеф
Комментарии: 1
  1. TomaszGasior

    Я решил проблему.

    Doctrine обрабатывает сущности в указанном порядке. Во-первых, вновь созданные объекты (запланированные для INSERT) имеют приоритет. Затем сохраняемые сущности (запланированные для ОБНОВЛЕНИЯ) обрабатываются в том же порядке, в котором они были извлечены из базы данных. Из внутреннего прослушивателя объектов я не могу предсказать или обеспечить предпочтительный порядок.

    Когда я пытаюсь обновить дату последней активности пользователя в прослушивателе сущности RadioTable, изменения, внесенные в сущность пользователя, не сохраняются. Это связано с тем, что на очень ранней стадии компонент безопасности загружает мой объект пользователя из БД, а затем Symfony подготавливает объект RadioTable для контроллера (например, с помощью преобразователя параметров).

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

    Я создал небольшой трейт для прослушивателей сущностей:

    <?php
    
    namespace App\EventListener\EntityListener;
    
    use Doctrine\Common\EventArgs;
    
    trait EntityListenerTrait
    {
        // There is need to manually enforce update of associated entities,
        // for example when User entity is modified inside RadioTable entity event.
        // It's because associations are not tracked consistently inside Doctrine's events.
        private function forceEntityUpdate(object $entity, EventArgs $args): void
        {
            $entityManager = $args->getEntityManager();
    
            $entityManager->getUnitOfWork()->recomputeSingleEntityChangeSet(
                $entityManager->getClassMetadata(get_class($entity)),
                $entity
            );
        }
    }
    

    Внутри прослушивателей сущностей я делаю это:

    <?php
    
    namespace App\EventListener\EntityListener;
    
    use App\Entity\RadioTable;
    use Doctrine\Common\EventArgs;
    use Doctrine\ORM\Mapping\PreFlush;
    use Doctrine\ORM\Mapping\PreRemove;
    
    class RadioTableListener
    {
        use EntityListenerTrait;
    
        /**
         * @PreFlush
         * @PreRemove
         */
        public function refreshLastActivityDateOfUser(RadioTable $radioTable, EventArgs $args): void
        {
            $user = $radioTable->getOwner();
    
            $user->refreshLastActivityDate();
            $this->forceEntityUpdate($user, $args);
        }
    }
    

    Есть другое решение. Можно вызвать $entityManager->flush($user), но он правильно работает только для UPDATE, генерирует бесконечный цикл для INSERT. Чтобы избежать бесконечного цикла, можно проверить $unitOfWork->isScheduledForInsert($radioTable).

    Это решение хуже, потому что генерирует дополнительные транзакции и SQL-запросы.

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: