Doctrine实体监听器的行为不一致

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Doctrine实体监听器的行为不一致相关的知识,希望对你有一定的参考价值。

我正在使用Symfony 4和Doctrine创建一个小应用程序。有用户(用户实体),他们拥有某种称为无线电表(RadioTable实体)的内容。无线电台包含无线电台(Radiostation实体)。 RadioStation.radioTableId与RadioTable(多对一)相关,RadioTable.ownerId与User(多对一)有关。

也许我应该注意到这是我的第一个SF项目。

使用注释配置实体,这样:

<?php 

namespace AppEntity;

/**
 * @ORMEntity(repositoryClass="AppRepositoryUserRepository")
 */
class User implements UserInterface, Serializable, EncoderAwareInterface
{
    /**
     * @ORMOneToMany(targetEntity="AppEntityRadioTable", mappedBy="owner", orphanRemoval=true)
     */
    private $radioTables;

    /**
     * @ORMColumn(type="date")
     */
    private $lastActivityDate;
}

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

namespace AppEntity;

/**
 * @ORMEntity(repositoryClass="AppRepositoryRadioTableRepository")
 * @ORMEntityListeners({"AppEventListenerRadioTableListener"})
 */
class RadioTable
{
    /**
     * @ORMManyToOne(targetEntity="AppEntityUser", inversedBy="radioTables")
     * @ORMJoinColumn(nullable=false, onDelete="cascade")
     */
    private $owner;

    /**
     * @ORMColumn(type="datetime")
     */
    private $lastUpdateTime;
}

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

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryRadioStationRepository")
 * @ORMEntityListeners({"AppEventListenerRadioStationListener"})
 */
class RadioStation
{
    /**
     * @ORMManyToOne(targetEntity="AppEntityRadioTable")
     * @ORMJoinColumn(nullable=false, onDelete="cascade")
     */
    private $radioTable;
}

我需要在添加,删除或修改无线电台时在适当的RadioTable实体中更新$lastUpdateTime。此外,当创建,删除或更新无线电表时,我需要更新无线电表所有者(用户类)的$lastActivityDate。我试图通过使用实体侦听器来实现这一点:

<?php

namespace AppEventListener;

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

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

namespace AppEventListener;

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 */评论标记。

在这个looong故事之后,我想问一个问题:我为什么要这样做?为什么我必须为用户对象手动调用flush(),但仅当使用安全组件并且用户已登录时?

答案

我解决了这个问题。

Doctrine以指定的顺序处理实体。首先,新创建的实体(计划用于INSERT)具有优先权。接下来,持久化实体(计划为UPDATE)的处理顺序与从数据库中提取的顺序相同。从实体监听器内部,我无法预测或执行首选订单。

当我尝试在RadioTable的实体侦听器中更新用户的上一个活动日期时,不会保留在用户实体中所做的更改。这是因为在很早的阶段,安全组件从DB加载我的User对象,然后Symfony为控制器准备RadioTable对象(例如通过param转换器)。

要解决这个问题,我需要告诉Doctrine重新计算用户实体变更集。这就是我做的。

我为我的实体监听器创建了一个小特征:

<?php

namespace AppEventListenerEntityListener;

use DoctrineCommonEventArgs;

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 AppEventListenerEntityListener;

use AppEntityRadioTable;
use DoctrineCommonEventArgs;
use DoctrineORMMappingPreFlush;
use DoctrineORMMappingPreRemove;

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查询。

以上是关于Doctrine实体监听器的行为不一致的主要内容,如果未能解决你的问题,请参考以下文章

通过findOneBy方法获取的Doctrine实体的行为与通常类似,但会触发致命错误

在 Doctrine (symfony2) 中对可翻译实体进行版本控制

QSerialPort 不一致的行为取决于起始波特率

如何使用symfony获取Doctrine实体持久集合数据值

Doctrine symfony使用OneToMany删除实体 - ManyToOne关系

Doctrine 2.5 意外的关联获取行为 [Symfony 3]