为什么 Doctrine 试图复制多对多关系,即使我事先检查它是否存在?为什么它会在同花顺时随机崩溃?

Why does Doctrine try to duplicate a Many-To-Many relationship even though I check if it exists beforehand? And why does it randomly crash on flush?

提问人:KotBehemot 提问时间:6/20/2017 最后编辑:KotBehemot 更新时间:10/25/2018 访问量:1797

问:

我很难看到 Doctrine 无法按预期工作。

我的代码试图做什么。

我正在我的 Symfony 3 Web 应用程序中编写一个 CLI 命令,它应该在我的数据库中整理一个标签表。有 Actor,也有 Tags。Actor 和 Tags 之间存在多对多关系(双向)。我的命令导入一个 CSV 文件,其中一列列出了当前标签,而在另一列中有一些替代品。它逐行浏览文件,找到现有的标签,读取它与 Actor 的所有当前关系,删除标签,创建一个新的标签(替换)或使用现有的标签,并将已删除标签的所有 Actor 关系附加到它。

代码(其关键部分)

protected function doReplace(InputInterface $input, OutputInterface $output, $input_file)
{
    $em = $this->getContainer()->get('doctrine')->getManager();
    $con = $em->getConnection();

    //open the input CSV
    $input_fhndl = fopen($input_file, 'r');
    if (!$input_fhndl)
        throw new \Exception('Unable to open file!');

    //do everything in a big transaction, so that if anything fails
    //everything rolls back and there's no half-finished information
    //in the DB
    $con->beginTransaction();
    try
    {
        //I was trying to use official Doctrine recommendation for batch inserts
        //to clear the entity manager after a bunch of operations, 
        //but it does neither help nor make things worse
//      $batchSize = 20;
        $i = 0;
        //reading the file line by line
        while (($line = fgetcsv($input_fhndl)) !== false) 
        {
            //$line[0] - source tag ID (the one to be substituted)
            //$line[1] - source tag type ('language' or 'skill')
            //$line[2] - source tag value (e.g. 'pole dancing (advanced)')
            //$line[3] - replacement tag value (e.g. 'pole dancing')

            $i++;

            if ($i === 1) //omit table headers
                continue;

            $line[3] = trim($line[3]);
            if ($line[3] === null || $line[3] === '') //omit lines with no replacements
                continue;

            //getting the tag to be replaced
            $src_tag = $em->getRepository('AppBundle:Tag')
                ->find($line[0]);
            if (!$src_tag)
            {
                //if the tag that is supposed to be replaced doesn't exist, just skip it
                continue;
            }


            $replacement_tag = null;
            $skip = false;

            //if the replacement value is '!' they just want to delete the original
            //tag without replacing it
            if (trim($line[3]) === '!')
            {
                $output->writeln('Removing '.$line[2].' ');
            }
            //here comes the proper replacement
            else
            {
                //there can be a few replacement values for one source tag
                //in such case they're separated with | in the input file
                $replacements = explode('|', $line[3]);

                foreach ($replacements as $replacement)
                {
                    $skip = false;
                    $output->write('Replacing '.$line[2].' with '.trim($replacement).'. ');

                    //getOrCreateTag looks for a tag with the same type and value as the replacement
                    //if it finds one, it retrieves the entity, if it doesn't it creates a new one
                    $replacement_tag = $this->getOrCreateTag($em, $src_tag->getTagType(), trim($replacement), $output);
                    if ($replacement_tag === $src_tag) //delete the original tag only if it is different from the replacement
                    {
                        $skip = true;
                    }
                    else
                    {
                        //we iterate through deleted Tag's relationships with Actors
                        foreach ($src_tag->getActors() as $actor)
                        {
                            //this part used to be the many-to-many fail point but i managed to fix it by removing indexBy: id line from Actor->Tag relation definition
                            if (!$replacement_tag->getActors() || !$replacement_tag->getActors()->contains($actor))
                                $replacement_tag->addActor ($actor);
                        }
                        $em->persist($replacement_tag);
                        //...and if I uncomment this flush()
                        //I get errors like Notice: Undefined index: 000000005f12fa20000000000088a5f2
                        //from Doctrine internals
                        //even though it should be harmless
//                      $em->flush();
                    }
                }
            }

            if (!$skip) //delete the original tag only if it is different from the replacement
            {
                $em->remove($src_tag);
                $em->flush(); //this flush both deletes the original tag and sets up the new one
                //with its relations
            }

//          if (($i % $batchSize) === 0) {
//              $em->flush(); // Executes all updates.
//              $em->clear(); // Detaches all objects from Doctrine!
//          }
        }
        $em->flush(); //one final flush just in case
        $con->commit();
    }
    catch (\Exception $e)
    {
        $output->writeln('<error> Something went wrong! Rolling back... </error>');
        $con->rollback();
        throw $e;
    }

    //closing the input file
    fclose($input_fhndl);
}

protected function getOrCreateTag($em, $tag_type, $value, $output)
{
    $value = trim($value);

    $replacement_tag = $em
        ->createQuery('SELECT t FROM AppBundle:Tag t WHERE t.tagType = :tagType AND t.value = :value')
        ->setParameter('tagType', $tag_type)
        ->setParameter('value', $value)
        ->getOneOrNullResult();

    if (!$replacement_tag)
    {
        $replacement_tag = new Tag();
        $replacement_tag->setTagType($tag_type);
        $replacement_tag->setValue($value);
        $output->writeln('Creating new.');
    }
    else
    {
        $output->writeln('Using existing.');
    }

    return $replacement_tag;
}

它是如何失败的

即使我做了这个检查: $replacement_tag->getActors()->contains($actor) Doctrine 试图创建重复的 Actor-Tag 关系:

[Doctrine\DBAL\Exception\UniqueConstraintViolationException]
An exception occurred while executing 'INSERT INTO actor_tags (actor_id, tag_id) VALUES (?, ?)' with params [280, 708]:
SQLSTATE[23505]: Unique violation: 7 ERROR:  duplicate key value violates unique constraint "actor_tags_pkey"
DETAIL:  Key (actor_id, tag_id)=(280, 708) already exists.

我设法通过从 Actor->Tag 关系定义中删除来解决上述问题(它是偶然存在的)。indexBy: id

此外,当我做一些理论上无害的修改时,比如取消注释注释的 flush() 调用或不使用大事务,我得到这个

即使没有对代码进行任何修改,在导入的某个时候我也会得到这个:

  [Symfony\Component\Debug\Exception\ContextErrorException]
  Notice: Undefined index: 000000001091cbbe000000000b4818c6


Exception trace:
 () at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:2907
 Doctrine\ORM\UnitOfWork->getEntityIdentifier() at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php:543
 Doctrine\ORM\Persisters\Collection\ManyToManyPersister->collectJoinTableColumnParameters() at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php:473
 Doctrine\ORM\Persisters\Collection\ManyToManyPersister->getDeleteRowSQLParameters() at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php:77
 Doctrine\ORM\Persisters\Collection\ManyToManyPersister->update() at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:388
 Doctrine\ORM\UnitOfWork->commit() at /src/__sources/atm/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:359
 Doctrine\ORM\EntityManager->flush() at /src/__sources/atm/src/AppBundle/Command/AtmReplaceTagsCommand.php:176
 AppBundle\Command\AtmReplaceTagsCommand->doReplace() at /src/__sources/atm/src/AppBundle/Command/AtmReplaceTagsCommand.php:60
 AppBundle\Command\AtmReplaceTagsCommand->execute() at /src/__sources/atm/vendor/symfony/symfony/src/Symfony/Component/Console/Command/Command.php:262
 Symfony\Component\Console\Command\Command->run() at /src/__sources/atm/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:848
 Symfony\Component\Console\Application->doRunCommand() at /src/__sources/atm/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:190
 Symfony\Component\Console\Application->doRun() at /src/__sources/atm/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Console/Application.php:80
 Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /src/__sources/atm/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:121
 Symfony\Component\Console\Application->run() at /src/__sources/atm/bin/console:28

每隔几行做一次也无济于事。$em->clear()

我试过了什么

  • 我尝试更改调用序列,这通常会导致奇怪的未定义索引错误。flush()
  • 我试着注释掉这笔大交易(无济于事)。
  • 我试着在每 20 条记录后打电话——这也根本没有改变任何事情。$em->clear()

我将不胜感激任何帮助。

其他信息

Actor->Tag 关系的 YAML 定义(针对 Actor 实体):

 manyToMany:
    tags:
      targetEntity: AppBundle\Entity\Tag
      inversedBy: actors
      #indexBy: id
#the above line caused the Many-To-Many duplicate problem - commenting it out fixed that part of the problem.
      joinTable:
        name: actor_tags
        joinColumns:
          actor_id:
            referencedColumnName: id
        inverseJoinColumns:
          tag_id:
            referencedColumnName: id

Tag->Actor 关系的 YAML 定义(针对 Tag 实体):

  manyToMany:
    actors:
      targetEntity: AppBundle\Entity\Actor
      mappedBy: tags

Tag::addActor()函数定义

public function addActor(\AppBundle\Entity\Actor $actor)
{
    $this->actor[] = $actor;
    $actor->addTag($this);

    return $this;
}

Actor::addTag()函数定义

public function addTag(\AppBundle\Entity\Tag $tag)
{
    $this->tags[] = $tag;

    $this->serializeTagIds();

    return $this;
}

如果您需要任何其他信息,请询问。谢谢。

php symfony doctrine-orm 对多 未定义索引

评论

0赞 KotBehemot 6/20/2017
所以我部分设法解决了这个问题。在我从 Actor->Tag 关系定义中删除该行后,我不再获得多对多重复关系。但是无论我做什么,我仍然会遇到错误:-(indexBy: idundefined index
0赞 nicolallias 3/21/2018
这里也有类似的问题,注释 $em->clear() 可以解决问题。仍在寻找更好的解决方案。

答:

2赞 Adambean 10/25/2018 #1

问题出在你和函数上。-- 他们以递归方式相互调用,而没有在添加新条目之前检查其集合中的内容,这就是您得到重复插入的原因。Tag::addActor()Actor::addTag()

更改函数,以便首先检查实例,如下所示:ArrayCollection

public function addActor(\AppBundle\Entity\Actor $actor)
{
    if (!$this->actor->contains($actor)) {
        $this->actor->add($actor);
    }

    $actor->addTag($this);

    return $this;
}

public function addTag(\AppBundle\Entity\Tag $tag)
{
    if (!$this->tags->contains($tag)) {
        $this->tags->add($tag);
    }

    $this->serializeTagIds();

    return $this;
}

除此之外,我假设两个实体的构造函数都将属性初始化为新实例,否则您将收到“尝试在 null 上调用 add()”错误。我还假设你已经在这些课程中名列前茅。ArrayCollectionuse Doctrine\Common\Collections\ArrayCollection;

对于 Tag 类:

public function __construct()
{
    $this->actor = new ArrayCollection();
}

对于 Actor 类:

public function __construct()
{
    $this->tags = new ArrayCollection();
}

加载现有实体时,这些不会覆盖/擦除关系。它只是确保在尝试向新实体添加元素之前正确设置了 ArrayCollection 实例。

关键点:您的 YAML 定义使用属性名称“actors”,但您的类函数引用 $this->actor。那不应该是$this>演员吗?如果这是真的,请调整上面的示例以使用而不是 。$this->actors$this->actor

最后,在这种情况下不要使用 $em->clear()。这将导致实体管理器知道的所有对象都处于非托管状态,这意味着它们不会在您的函数中保留任何进一步的更改,直到您再次更改它们。$em->merge()

评论

0赞 KotBehemot 12/30/2018
谢谢你的回复!很长一段时间没有机会与 Doctrine 合作,但我肯定会在某个时候测试您的解决方案。