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

问题描述 投票:0回答:2

Doctrine 未能按预期工作,这让我感到非常难过。

我的代码试图做什么。

我正在 Symfony 3 Web 应用程序中编写一个 CLI 命令,该命令应该整理数据库中的标签表。有演员,也有标签。 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 many-to-many undefined-index
2个回答
2
投票

问题出在您的

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

除此之外,我假设两个实体的构造函数都将属性初始化为新的

ArrayCollection
实例,否则您将收到“尝试在 null 上调用 add()”错误。我还假设您在这些课程中取得了
use Doctrine\Common\Collections\ArrayCollection;
的成绩。

对于标签类:

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

对于演员类:

public function __construct()
{
    $this->tags = new ArrayCollection();
}
加载现有实体时,这些将不会覆盖/擦除关系。它只是确保在尝试向新实体添加元素之前正确设置新实体的

ArrayCollection 实例。

关键点:您的 YAML 定义使用属性名称“actors”,但您的类函数引用 $this->actor

。这不应该是

$this->actors吗?

 如果是这样,请调整上面的示例以使用 
$this->actors
 而不是 
$this->actor

最后
在这种情况下不要使用
$em->clear()

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

它们。
如果当你想维持像ManyToMany这样的关系时仍然面临这个问题,请确保在“$manager->clear()”末尾清除管理器 - Symfony 6.4。我正在研究一些装置,Adambean 的最后一句话救了我。


0
投票
最后在这种情况下不要使用 $em->clear() 。这将导致所有 实体管理器知道将成为非托管对象 状态,这意味着他们不会继续对您的内容进行任何进一步的更改 函数直到你再次 $em->merge() 它们。

© www.soinside.com 2019 - 2024. All rights reserved.