【问题标题】:Inconsistent behavior of Doctrine's entity listenerDoctrine 的实体监听器的行为不一致
【发布时间】:2018-09-13 20:15:58
【问题描述】:

我正在使用 Symfony 4 & Doctrine 创建一个小应用程序。有用户(用户实体),他们拥有某种称为单选表(RadioTable 实体)的内容。无线电表包含无线电台(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。另外,当创建、删除或更新单选表时,我需要更新单选表所有者(用户类)的$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 的 Security 组件来支持登录机制。

当我暂时破解控制器代码以禁用安全性并尝试将单选表更新为匿名时,RadioTable 实体侦听器正常工作并且用户实体已成功修改并持久化到数据库

要解决这个问题,我需要手动与 Doctrine 的实体管理器交谈,并以用户实体作为参数调用flush()(没有参数我正在做无限循环)。此行由/* hack */ 注释标记。

在这个冗长的故事之后,我想问一个问题:为什么我必须这样做?为什么我必须为用户对象手动调用flush(),但前提是使用安全组件并且用户已登录?

【问题讨论】:

  • 看看你的refresh*()函数会很有帮助。
  • 我怀疑不同之处在于您在实体级别的生命周期事件中间调用它们(例如,而不是在侦听器中)。 可能是您的 RadioStation 在您的 RadioTable 之前处理,因此对 RadioTable 的更改随后会被持久化,而 User 在 RadioStation 之前处理,因此在处理 RadioStation(用户已处理)之后,您必须手动执行回到持久用户。只是一个猜测。调试/跟踪将有助于回答。

标签: symfony doctrine entity listener symfony4


【解决方案1】:

我解决了这个问题。

Doctrine 以指定的顺序处理实体。首先,新创建的实体(计划为 INSERT)具有优先权。接下来,持久化实体(计划为 UPDATE)按照从数据库中获取的顺序进行处理。从内部实体侦听器中,我无法预测或执行首选顺序。

当我尝试在 RadioTable 的实体侦听器中更新用户的最后活动日期时,在用户实体中所做的更改不会保留。这是因为在很早的阶段,Security 组件从 DB 加载我的 User 对象,然后 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-&gt;flush($user),但它仅适用于更新,为插入生成无限循环。为了避免死循环,可以检查$unitOfWork-&gt;isScheduledForInsert($radioTable)

此解决方案更糟糕,因为它会生成额外的事务和 SQL 查询。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多