Я создаю небольшое приложение, используя 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 для поддержки механизма входа в систему.
Когда я временно взломал код контроллера, чтобы отключить безопасность, и попытался обновить таблицу радио как анонимную, прослушиватель сущности 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
Я решил проблему.
Doctrine обрабатывает сущности в указанном порядке. Во-первых, вновь созданные объекты (запланированные для INSERT) имеют приоритет. Затем сохраняемые сущности (запланированные для ОБНОВЛЕНИЯ) обрабатываются в том же порядке, в котором они были извлечены из базы данных. Из внутреннего прослушивателя объектов я не могу предсказать или обеспечить предпочтительный порядок.
Когда я пытаюсь обновить дату последней активности пользователя в прослушивателе сущности RadioTable, изменения, внесенные в сущность пользователя, не сохраняются. Это связано с тем, что на очень ранней стадии компонент безопасности загружает мой объект пользователя из БД, а затем Symfony подготавливает объект RadioTable для контроллера (например, с помощью преобразователя параметров).
Чтобы решить эту проблему, мне нужно сказать Doctrine пересчитать набор изменений сущности пользователя. Вот что я сделал.
Я создал небольшой трейт для прослушивателей сущностей:
Внутри прослушивателей сущностей я делаю это:
Есть другое решение. Можно вызвать
$entityManager->flush($user)
, но он правильно работает только для UPDATE, генерирует бесконечный цикл для INSERT. Чтобы избежать бесконечного цикла, можно проверить$unitOfWork->isScheduledForInsert($radioTable)
.Это решение хуже, потому что генерирует дополнительные транзакции и SQL-запросы.