Doctrine在postPersist事件中插入数据。

18

我希望在实体持久化和更新时添加新的Feed项目。我编写了这个事件监听器(postUpdate相同):

public function postPersist(LifecycleEventArgs $args)
{
    $entity = $args->getEntity();
    $em = $args->getEntityManager();

    if ($entity instanceof FeedItemInterface) {
        $feed = new FeedEntity();
        $feed->setTitle($entity->getFeedTitle());
        $feed->setEntity($entity->getFeedEntityId());
        $feed->setType($entity->getFeedType());
        if($entity->isFeedTranslatable()) {
            $feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
        }
        $em->persist($feed);
        $em->flush();
    }
}

但是我得到了:

完整性约束冲突:1062主键为“PRIMARY”的重复条目'30-2'

并且在日志中有两个插入操作:

INSERT INTO interview_scientificdirection (interview_id, scientificdirection_id) VALUES (?, ?) ([30,2]) INSERT INTO interview_scientificdirection (interview_id, scientificdirection_id) VALUES (?, ?) ([30,2])

scientificdirection是我们要持久化实体的多对多关系表。 在前端应用程序中一切正常,但在Sonata Admin中我遇到了这个问题 :(

4个回答

34
如果您需要持久化其他对象,则Doctrine中的postPersist或postUpdate处理程序不是正确的去处。今天我遇到了同样的问题,因为我需要在该处理程序中生成一些消息条目。
问题在于此时postPersist处理程序在flush事件期间调用,而不是之后。因此,您无法在此处持久化其他对象,因为它们之后不会被刷新。此外,在postPersist处理程序中不能调用flush,因为这可能会导致重复条目(正如您所经历的那样)。
一种方法是使用Doctrine中的onFlush处理程序,文档在此处:https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/events.html#onflush 如果您不需要数据库对象的插入ID,则可以使用Doctrine中的onFlush事件。但如果您需要这些ID,则存在问题,因为实体尚未在该处理程序中写入数据库。
对我而言,解决方案有些不同。我目前正在开发一个symfony2项目,需要插入的数据库对象的id(用于稍后的回调和更新)。
我在symfony2中创建了一个新服务,它基本上就像我的消息队列一样。在postPersist更新期间,我只需填写队列中的条目即可。我在kernel.response上注册了另一个处理程序,然后将这些条目取出并将其持久化到数据库中。(大致类似于这个:http://symfony.com/doc/current/cookbook/service_container/event_listener.html
我希望我在这里没有太过离题,但由于这是我真正苦苦挣扎的事情,我希望有些人可能会觉得这很有用。
这个服务的条目为:
 amq_messages_chain:
   class: Acme\StoreBundle\Listener\AmqMessagesChain

 amqflush:
   class: Acme\StoreBundle\Listener\AmqFlush
   arguments: [ @doctrine.orm.entity_manager, @amq_messages_chain, @logger ]
   tags:
     - { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }

 doctrine.listener:
  class: Acme\StoreBundle\Listener\AmqListener
  arguments: [ @logger, @amq_messages_chain ]
  tags:
    - { name: doctrine.event_listener, event: postPersist }
    - { name: doctrine.event_listener, event: postUpdate }
    - { name: doctrine.event_listener, event: prePersist }

你不能使用doctrine.listener来实现这个,因为这会导致循环依赖(因为你需要实体管理器作为服务,但实体管理器需要该服务...)
这个方法非常有效。如果您需要更多信息,请不要犹豫,问我,我很乐意在此添加一些示例。

你想要创建一个事件记录并知道它的ID,需要使用消息队列吗? - nucleartux
不,我需要在onFlush处理程序中即将持久化的实体的ID。由于工作单元只是在准备阶段,您没有实体的ID,这些实体将在onFlush处理程序完成后刷新。或者我误解了你的问题? - jhoffrichter
是的,你误解了我的意思 :) 如果我需要实体发生此事件的ID而不使用消息队列软件,我能否在事件中创建新实体? - nucleartux
@jhoffrichter 为什么要将监听器附加到请求事件上?如果这段代码在控制台中执行会怎样?你尝试过使用 Doctrine 的 postFlush 吗? - Francesc Rosas
这是一个有趣的解决方案,我也可能会使用它。在我的情况下,我正在使用Doctrine生命周期事件来检测多个实体属性的更改,然后通过创建新的日志实体并尝试将其持久化到数据库中来记录它们。“排队”我需要制作的日志,然后在内核事件上持久化它们应该很好地工作。 - Brian
非常聪明的解决方案! - Djagu

31
Francesc的答案是错误的,因为postFlush事件中的更改集已经为空。 jhoffrichter的第二个答案可能可行,但过于复杂。 正确的做法是在postPersist事件中持久化实体,并在postFlush事件中再次调用flush。但是只有在postPersist事件中有所更改时才需要执行此操作,否则将创建一个无限循环。
public function postPersist(LifecycleEventArgs $args) {

    $entity = $args->getEntity();
    $em = $args->getEntityManager();

    if($entity instanceof FeedItemInterface) {
        $feed = new FeedEntity();
        $feed->setTitle($entity->getFeedTitle());
        $feed->setEntity($entity->getFeedEntityId());
        $feed->setType($entity->getFeedType());
        if($entity->isFeedTranslatable()) {
            $feed->getEnTranslation()->setTitle($entity->getFeedTitle('en'));
        }
        $em->persist($feed);
        $this->needsFlush = true;
    }
}

public function postFlush(PostFlushEventArgs $eventArgs)
{
    if ($this->needsFlush) {
        $this->needsFlush = false;
        $eventArgs->getEntityManager()->flush();
    }
}

这不太好,因为这样做会产生两个不同的事务。因此,只有其中一个可能失败,留下一个实体。也就是说,除非你明确执行$em->beginTransaction(); - M. Ivanov
11
嘿@chris,我不是想成为一个批评者,但Doctrine[文档](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#postflush)中说:“EntityManager#flush()不能在[postFlush的]侦听器中安全地调用。”你对此有什么想法? - Ian Phillips
3
@chris,正如Ian在上面所说的,postFlush()内部的flush()不安全。这样真的可以吗?在我看来好像不行。 - Nuno Pereira

3

jhoffrichter的解决方案非常有效。如果您使用控制台命令,应为事件command.terminate添加标记。否则,在控制台命令中无法正常工作。请参见https://dev59.com/bXjZa4cB1Zd3GeqPgqqG#19737608

config.yml

amq_messages_chain:
   class: Acme\StoreBundle\Listener\AmqMessagesChain

amqflush:
   class: Acme\StoreBundle\Listener\AmqFlush
   arguments: [ @doctrine.orm.entity_manager, @amq_messages_chain, @logger ]
   tags:
     - { name: kernel.event_listener, event: kernel.response, method: onResponse, priority: 5 }
     - { name: kernel.event_listener, event: command.terminate, method: onResponse }

doctrine.listener:
  class: Acme\StoreBundle\Listener\AmqListener
  arguments: [ @logger, @amq_messages_chain ]
  tags:
    - { name: doctrine.event_listener, event: postPersist }
    - { name: doctrine.event_listener, event: postUpdate }
    - { name: doctrine.event_listener, event: prePersist }

2

好的,这里是我在SF 2.0和2.2中的做法:

监听器类:

<?php
namespace YourNamespace\EventListener;

use Doctrine\ORM\Mapping\PostPersist;


/*
 * ORMListener class
 *
 * @author:        Marco Aurélio Simão
 * @description:   Listener para realizar operações em qualquer objeto manipulado pelo Doctrine 2.2
 */

use Doctrine\ORM\UnitOfWork;

use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\PrePersist;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Mapping\PostUpdate;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Enova\EntitiesBundle\Entity\Entidades;

use Doctrine\ORM\Event\LifecycleEventArgs;

use Enova\EntitiesBundle\Entity\Tagged;
use Enova\EntitiesBundle\Entity\Tags;

class ORMListener
{
    protected $extra_update;

    public function __construct($container)
    {
        $this->container    = $container;
        $this->extra_update = false;
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        $securityContext = $this->container->get('security.context');
        $em              = $args->getEntityManager();

        $uow             = $em->getUnitOfWork();
        $cmf             = $em->getMetadataFactory();

        foreach ($uow->getScheduledEntityInsertions() AS $entity)
        {
            $meta = $cmf->getMetadataFor(get_class($entity));

            $this->updateTagged($em, $entity);
        }

        foreach ($uow->getScheduledEntityUpdates() as $entity)
        {
            $meta = $cmf->getMetadataFor(get_class($entity));

            $this->updateTagged($em, $entity);
        }
    }

    public function updateTagged($em, $entity)
    {
      $entityTags = $entity->getTags();

      $a = array_shift($entityTags);
      //in my case, i have already sent the object from the form, but you could just replace this part for new Object() etc

      $uow      = $em->getUnitOfWork();
      $cmf      = $em->getMetadataFactory();
      $meta     = $cmf->getMetadataFor(get_class($a));

      $em->persist($a);

      $uow->computeChangeSet($meta, $a);
    }

}

Config.yml:

services:
    updated_by.listener:
        class: YourNamespace\EventListener\ORMListener
        arguments: [@service_container]
        tags:
            - { name: doctrine.event_listener, event: onFlush, method: onFlush }

希望这能有所帮助;)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接