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\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()
调用顺序,这通常会导致奇怪的未定义索引错误。$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;
}
除此之外,我假设两个实体的构造函数都将属性初始化为新的
ArrayCollection
实例,否则您将收到“尝试在 null 上调用 add()”错误。我还假设您在这些课程中取得了 use Doctrine\Common\Collections\ArrayCollection;
的成绩。
对于标签类:
public function __construct()
{
$this->actor = new ArrayCollection();
}
对于演员类:
public function __construct()
{
$this->tags = new ArrayCollection();
}
加载现有实体时,这些将不会覆盖/擦除关系。它只是确保在尝试向新实体添加元素之前正确设置新实体的 ArrayCollection
实例。
$this->actor
。这不应该是 $this->actors
吗?
如果是这样,请调整上面的示例以使用
$this->actors
而不是
$this->actor
。最后
在这种情况下不要使用
$em->clear()
。这将导致实体管理器知道的所有对象都处于非托管状态,这意味着它们不会在您的函数中保留任何进一步的更改,直到您再次$em->merge()
它们。如果当你想维持像ManyToMany这样的关系时仍然面临这个问题,请确保在“$manager->clear()”末尾清除管理器 - Symfony 6.4。我正在研究一些装置,Adambean 的最后一句话救了我。