Doctrine 实体监听器的不一致行为
Inconsistent behavior of Doctrine's entity listener
我正在使用 Symfony 4 和 Doctrine 创建一个小应用程序。有用户(用户实体),他们拥有某种称为 radio tables(RadioTable 实体)的内容。 Radio table 包含无线电台(RadioStation 实体)。 RadioStation.radioTableId 与 RadioTable(多对一)相关,RadioTable.ownerId 与 User(多对一)相关。
也许我应该注意到这是我与 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;
}
当添加、删除或修改无线电台时,我需要在适当的 RadioTable 实体中更新 $lastUpdateTime
。此外,当创建、删除或更新无线电 table 时,我需要更新无线电 table 所有者(用户 class)的 $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
的新实例。)
我遇到了问题。当我尝试 update/remove/create 广播电台时,RadioStation 侦听器工作正常并且相关的 RadioTable class 已成功更新。但是当我尝试更新 radio table 时,用户 class 被更新但没有被 Doctrine 持久化到数据库中。
我很困惑,因为这些实体侦听器中的代码结构非常相似。
部分找到了问题的原因。很明显,只有所有者可以修改自己的无线电 tables 并且用户必须登录才能修改它们。我正在使用 Symfony 的安全组件来支持登录机制。
当我临时破解控制器代码以禁用安全性并尝试将无线电 table 更新为匿名时,RadioTable 实体侦听器正常工作并且用户实体已成功修改 并持久保存到数据库。
为了解决这个问题,我需要手动与 Doctrine 的实体管理器交谈,并以用户实体作为参数调用 flush()
(没有参数我正在做无限循环)。此行由 /* hack */
注释标记。
在这个looong故事之后,我想问一个问题:为什么我必须这样做?为什么我必须为用户对象手动调用 flush()
,但前提是使用了安全组件并且用户已登录?
我解决了这个问题。
Doctrine 以指定顺序处理实体。首先,新创建的实体(计划用于 INSERT)具有优先权。接下来,持久化实体(计划更新)按照从数据库中获取的相同顺序进行处理。从实体侦听器内部,我无法预测或执行首选顺序。
当我尝试在 RadioTable 的实体侦听器中更新用户的最后 activity 日期时,在用户实体中所做的更改不会保留。这是因为在很早的阶段,安全组件从数据库加载我的用户对象,然后 然后 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 查询。
我正在使用 Symfony 4 和 Doctrine 创建一个小应用程序。有用户(用户实体),他们拥有某种称为 radio tables(RadioTable 实体)的内容。 Radio table 包含无线电台(RadioStation 实体)。 RadioStation.radioTableId 与 RadioTable(多对一)相关,RadioTable.ownerId 与 User(多对一)相关。
也许我应该注意到这是我与 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;
}
当添加、删除或修改无线电台时,我需要在适当的 RadioTable 实体中更新 $lastUpdateTime
。此外,当创建、删除或更新无线电 table 时,我需要更新无线电 table 所有者(用户 class)的 $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
的新实例。)
我遇到了问题。当我尝试 update/remove/create 广播电台时,RadioStation 侦听器工作正常并且相关的 RadioTable class 已成功更新。但是当我尝试更新 radio table 时,用户 class 被更新但没有被 Doctrine 持久化到数据库中。
我很困惑,因为这些实体侦听器中的代码结构非常相似。
部分找到了问题的原因。很明显,只有所有者可以修改自己的无线电 tables 并且用户必须登录才能修改它们。我正在使用 Symfony 的安全组件来支持登录机制。
当我临时破解控制器代码以禁用安全性并尝试将无线电 table 更新为匿名时,RadioTable 实体侦听器正常工作并且用户实体已成功修改 并持久保存到数据库。
为了解决这个问题,我需要手动与 Doctrine 的实体管理器交谈,并以用户实体作为参数调用 flush()
(没有参数我正在做无限循环)。此行由 /* hack */
注释标记。
在这个looong故事之后,我想问一个问题:为什么我必须这样做?为什么我必须为用户对象手动调用 flush()
,但前提是使用了安全组件并且用户已登录?
我解决了这个问题。
Doctrine 以指定顺序处理实体。首先,新创建的实体(计划用于 INSERT)具有优先权。接下来,持久化实体(计划更新)按照从数据库中获取的相同顺序进行处理。从实体侦听器内部,我无法预测或执行首选顺序。
当我尝试在 RadioTable 的实体侦听器中更新用户的最后 activity 日期时,在用户实体中所做的更改不会保留。这是因为在很早的阶段,安全组件从数据库加载我的用户对象,然后 然后 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 查询。