【问题标题】:Persisting other entities inside preUpdate of Doctrine Entity Listener在 Doctrine Entity Listener 的 preUpdate 中持久化其他实体
【发布时间】:2015-08-24 10:25:59
【问题描述】:

为了清楚起见,我继续讨论开始here

Doctrine Entity Listener 中,在 preUpdate 方法中(我可以访问实体的任何字段的旧值和新值),我试图保留一个与焦点无关的实体。

基本上我有实体 A,当我在 project_notification 表中更改要写入的字段之一中的值时,字段 oldValue、newValue 以及其他字段。

如果我不在 preUpdate 方法中刷新,则新的通知实体不会存储在 DB 中。如果我刷新它,我会进入一个无限循环。

这是 preUpdate 方法:

public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
{
    if ($event->hasChangedField('riskToleranceFlag')) {
    $project = $tolerances->getProject();                
    $em = $event->getEntityManager();
    $notification = new ProjectNotification();
    $notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
    $notification->setValueTo($event->getNewValue('riskToleranceFlag'));
    $notification->setEntity('Entity'); //TODO substitute with the real one
    $notification->setField('riskToleranceFlag');
    $notification->setProject($project);
    $em->persist($notification);


    // $em->flush(); // gives infinite loop
    }
}

谷歌搜索了一下,我发现你不能在侦听器中调用刷新,here 建议将要持久化的内容存储在数组中,以便稍后在 onFlush 中刷新。尽管如此,它不起作用(并且可能它不应该起作用,因为在您调用 preUpdate 后侦听器类的实例会被破坏,因此当您稍后调用 onFlush 时,无论您存储为类级别的受保护属性的任何内容都会丢失,还是我错过了什么?)。

这是监听器的更新版本:

class ProjectTolerancesListener
{
    protected $toBePersisted = [];

    public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
    {
        $uow = $event->getEntityManager()->getUnitOfWork();
//        $hasChanged = false;

        if ($event->hasChangedField('riskToleranceFlag')) {
        $project = $tolerances->getProject();                
        $notification = new ProjectNotification();
        $notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
        $notification->setValueTo($event->getNewValue('riskToleranceFlag'));
        $notification->setEntity('Entity'); //TODO substitute with the real one
        $notification->setField('riskToleranceFlag');
        $notification->setProject($project);

        if(!empty($this->toBePersisted))
            {
            array_push($toBePersisted, $notification);
            }
        else
            {
            $toBePersisted[0] = $notification;
            }
        }
    }

    public function postFlush(LifecycleEventArgs $event)
    {
        if(!empty($this->toBePersisted)) {

            $em = $event->getEntityManager();

            foreach ($this->toBePersisted as $element) {

                $em->persist($element);
            }

            $this->toBePersisted = [];
            $em->flush();
        }
    }
}

也许我可以通过从侦听器内部触发一个事件来解决这个问题,其中包含在刷新后执行我的日志记录操作所需的所有信息......但是:

1) 我不知道我能不能做到

2) 好像有点矫枉过正

谢谢!

【问题讨论】:

    标签: symfony doctrine-orm


    【解决方案1】:

    不要使用 preUpdate,使用 onFlush - 这允许您访问 UnitOfWork API,然后您可以持久化实体。

    例如(这是我在 2.3 中的做法,可能会在新版本中更改)

        $this->getEntityManager()->persist($entity);
        $metaData = $this->getEntityManager()->getClassMetadata($className);
        $this->getUnitOfWork()->computeChangeSet($metaData, $entity);
    

    【讨论】:

    【解决方案2】:

    我将所有功劳归功于理查德为我指明了正确的方向,所以我接受了他的回答。尽管如此,我也会为未来的访问者发布我的答案以及完整的代码。

    class ProjectEntitySubscriber implements EventSubscriber
    {
        public function getSubscribedEvents()
        {
            return array(
                'onFlush',
            );
        }
    
        public function onFlush(OnFlushEventArgs  $args)
        {
            $em = $args->getEntityManager();
            $uow = $em->getUnitOfWork();
    
            foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) {
                if ($entity instanceof ProjectTolerances) {
                    foreach ($uow->getEntityChangeSet($entity) as $keyField => $field) {
                        $notification = new ProjectNotification();
                        // place here all the setters
                        $em->persist($notification);
                        $classMetadata = $em->getClassMetadata('AppBundle\Entity\ProjectNotification');
                        $uow->computeChangeSet($classMetadata, $notification);
                    }
                }
            }
        }
    }
    

    【讨论】:

    • 我花了一点时间才意识到这一点,但在这个答案中,您远离了 Doctrine Entity Listener 并实现了 Doctrine Event Listener。实体侦听器没有onFlushpostFlush 事件。
    【解决方案3】:

    正如 David Baucum 所说,最初的问题涉及 Doctrine Entity Listeners,但作为解决方案,操作最终使用了 Event Listener。

    我相信会有更多人偶然发现这个话题,因为无限循环问题。 对于那些采用接受的答案的人,请注意 onFlush 事件(使用上面的事件侦听器时)与所有可能在队列中等待更新的实体一起执行,而实体侦听器仅在使用它被“分配”到的实体。

    我使用 symfony 4.4 和 API 平台设置了一个自定义审计系统,并且我只使用了一个实体监听器就达到了预期的结果。

    注意:经过测试和工作,命名空间和函数已被修改,这纯粹是为了演示如何在 Doctrine 实体侦听器中操作另一个实体。

    // this goes into the main entity
    /**
    * @ORM\EntityListeners({"App\Doctrine\MyEntityListener"})
    */
    
    <?
    // App\Doctrine\MyEntityListener.php
    
    namespace App\Doctrine;
    
    use Doctrine\ORM\EntityManagerInterface;
    use Doctrine\ORM\Event\PreUpdateEventArgs;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\Security;
    
    // whenever an Employee record is inserted/updated
    // log changes to EmployeeAudit
    use App\Entity\Employee;
    use App\Entity\EmployeeAudit;
    
    private $security;
    private $currentUser;
    private $em;
    private $audit;
    
    public function __construct(Security $security, EntityManagerInterface $em) {
        $this->security = $security;
        $this->currentUser = $security->getUser();
        $this->em = $em;
    }
    
    // HANDLING NEW RECORDS
    
    /**
     * since prePersist is called only when inserting a new record, the only purpose of this method
     * is to mark our object as a new entry
     * this method might not be necessary, but for some reason, if we set something like
     * $this->isNewEntry = true, the postPersist handler will not pick up on that
     * might be just me doing something wrong
     *
     * @param Employee $obj
     * @ORM\PrePersist()
     */
    public function prePersist(Employee $obj){
        if(!($obj instanceof Employee)){
            return;
        }
        $isNewEntry = !$obj->getId();
        $obj->markAsNewEntry($isNewEntry);// custom Employee method (just sets an internal var to true or false, which can later be retrieved)
    }
    
    /**
     * @param Employee $obj
     * @ORM\PostPersist()
     */
    public function postPersist(Employee $obj){
        // in this case, we can flush our EmployeeAudit object safely
        $this->prepareAuditEntry($obj);
    }
    
    // END OF NEW RECORDS HANDLING
    
    // HANDLING UPDATES
    
    /**
     * @see {https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html}
     * @param Employee $obj
     * @param PreUpdateEventArgs $args
     * @ORM\PreUpdate()
     */
    public function preUpdate(Employee $obj, PreUpdateEventArgs $args){
        $entity = $args->getEntity();
        $changeset = $args->getEntityChangeSet();
    
        // we just prepare our EmployeeAudit obj but don't flush anything
        $this->audit = $this->prepareAuditEntry($obj, $changeset, $flush = false);
    }
    
    /**
     * @ORM\PostUpdate()
     */
    public function postUpdate(){
        // if the preUpdate handler was called, $this->audit should exist
        // NOTE: the preUpdate handler DOES NOT get called, if nothing changed
        if($this->audit){
            $this->em->persist($this->audit);
            $this->em->flush();
        }
        // don't forget to unset this
        $this->audit = null;
    }
    
    // END OF HANDLING UPDATES
    
    // AUDITOR
    
    private function prepareAuditEntry(Employee $obj, $changeset = [], $flush = true){
        if(!($obj instanceof Employee) || !$obj->getId()){
            // at this point, we need a DB id
            return;
        }
    
        $audit = new EmployeeAudit();
        // this part was cut out, since it is custom
        // here you would set things to your EmployeeAudit object
        // either get them from $obj, compare with the changeset, etc...
    
        // setting some custom fields
        // in case it is a new insert, the changedAt datetime will be identical to the createdAt datetime
        $changedAt = $obj->isNewInsert() ? $obj->getCreatedAt() : new \DateTime('@'.strtotime('now'));
        $changedFields = array_keys($changeset);
        $changedCount = count($changedFields);
        $changedBy = $this->currentUser->getId();
        $entryId = $obj->getId();
    
        $audit->setEntryId($entryId);
        $audit->setChangedFields($changedFields);
        $audit->setChangedCount($changedCount);
        $audit->setChangedBy($changedBy);
        $audit->setChangedAt($changedAt);
    
        if(!$flush){
            return $audit;
        }
        else{
            $this->em->persist($audit);
            $this->em->flush();
        }
    }
    
    

    我们的想法是不要在 preUpdate 中保留/刷新任何内容(准备数据除外,因为您可以访问变更集和内容),并在更新的情况下执行 postUpdate,或在新插入的情况下执行 postPersist。

    【讨论】:

    • 谢谢!我以这种方式实现并让它按预期工作?
    【解决方案4】:

    使用Lifecycle Listener 而不是EntityListener 可能更适合这种情况(我发现symfony docs 提供了对不同选项的更好概述)。这是由于onFlush,一个非常强大的事件,不适用于EntityListeners。在计算所有变更集之后和执行数据库操作之前调用此事件。

    在这个答案中,我使用 Entity Listener 探索选项。

    使用preUpdate:此事件提供PreUpdateEventArgs,可以轻松找到所有将要更改的值。但是,在处理完插入后,此事件会在 UnitOfWork#commit 内触发。因此,现在不可能在当前事务中添加要持久化的新实体。

    使用preFlush:此事件发生在flush 操作开始时。变更集可能尚不可用,但我们可以将原始值与当前值进行比较。当需要进行许多更改时,此方法可能不适合。这是一个示例实现:

        public function preFlush(Order $order, PreFlushEventArgs $eventArgs)
        {
            // Create a log entry when the state was changed
            $entityManager = $eventArgs->getEntityManager();
            $unitOfWork = $entityManager->getUnitOfWork();
            $originalEntityData = $unitOfWork->getOriginalEntityData($order);
            $newState = $order->getState();
            if (empty($originalEntityData)) {
                // We're dealing with a new order
                $oldState = "";
            } else {
                $stateProperty = 'state';
                $oldState = $originalEntityData[$stateProperty];
                // Same behavior as in \Doctrine\ORM\UnitOfWork:720: Existing
                // changeset is ignored when the property was changed
                $entityChangeSet = $unitOfWork->getEntityChangeSet($order);
                $stateChanges = $entityChangeSet[$stateProperty] ?? [];
                if ($oldState == $newState && $stateChanges) {
                    $oldState = $stateChanges[0] ?? "";
                    $newState = $stateChanges[1] ?? "";
                }
            }
            if ($oldState != $newState) {
                $statusLog = $this->createOrderStatusLog($order, $oldState, $newState);
                $unitOfWork->scheduleForInsert($statusLog);
                $unitOfWork->computeChangeSet($entityManager->getClassMetadata('App\Entity\OrderStatusLog'), $statusLog);
            }
        }
    

    使用 postFlush/postUpdate:使用这些事件会导致第二个数据库事务,这是不可取的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-04-13
      • 1970-01-01
      • 1970-01-01
      • 2021-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多