提问人:KotBehemot 提问时间:6/20/2017 最后编辑:KotBehemot 更新时间:10/25/2018 访问量:1797
为什么 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?
问:
我很难看到 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;
}
如果您需要任何其他信息,请询问。谢谢。
答:
问题出在你和函数上。-- 他们以递归方式相互调用,而没有在添加新条目之前检查其集合中的内容,这就是您得到重复插入的原因。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()”错误。我还假设你已经在这些课程中名列前茅。ArrayCollection
use 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()
评论
indexBy: id
undefined index