如何避免与Doctrine的多对多关系中的重复条目?

时间:2014-02-11 20:49:12

标签: php symfony doctrine-orm

我正在使用embed Symfony form直接在文章编辑器中添加和删除Tag个实体。 Article是关联中的owning side

class Article
{
    /**
     * @ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
     */
    private $tags;

    public function addTag(Tag $tags)
    {
        if (!$this->tags->contains($tags)) // It is always true.
            $this->tags[] = $tags;
    }
}

条件在这里没有帮助,因为它始终是真的,如果它不是,则根本不会有任何新标签持久存储到数据库。这是Tag实体:

class Tag
{
    /**
     * @Column(unique=true)
     */
    private $name

    /**
     * @ManyToMany(targetEntity="Articles", mappedBy="tags")
     */
    private $articles;

    public function addArticle(Article $articles)
    {
        $this->articles[] = $articles;
    }
}

我已将$name设置为唯一,因为我希望每次在表单中输入相同的名称时都使用相同的标记。但它并没有这样做,我得到了例外:

  

完整性约束违规:1062重复条目

我需要更改以使用article_tag,提交标记名称时的默认联接表,已经在Tag表格中?

2 个答案:

答案 0 :(得分:4)

两个主要解决方案

第一

使用data transformer

class TagsTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * used to give a "form value"
     */
    public function transform($tag)
    {
        if (null === $tag) {
            //do proper actions
        }

        return $issue->getName();
    }

    /**
     * used to give "a db value"
     */
    public function reverseTransform($name)
    {
        if (!$name) {
            //do proper actions
        }

        $issue = $this->om
            ->getRepository('YourBundleName:Tag')
            ->findOneBy(array('name' => $name))
        ;

        if (null === $name) {
            //create a new tag
        }

        return $tag;
    }
}

第二

使用生命周期回调。您可以在prePersist实体上使用article触发器吗?通过这种方式,您可以检查预先存在的tags并让entity manager为您管理它们(因此他不需要尝试继续导致错误)。

您可以详细了解prePersist here

第二个解决方案

为搜索创建自定义存储库方法并获取旧标记(如果有)

答案 1 :(得分:4)

我几个月来一直在与类似的问题作斗争,最后找到了一个似乎在我的应用程序中运行良好的解决方案。它是一个复杂的应用程序,具有相当多的多对多关联,我需要以最高效率处理它们。

解决方案部分解释如下:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush

你的代码已经到了一半了:

public function addTag(Tag $tags)
{
    if (!$this->tags->contains($tags)) // It is always true.
        $this->tags[] = $tags;
}

基本上我添加到这里的是设置 indexedBy =" name" fetch =" EXTRA_LAZY" 拥有关系的一面,在你的情况下是文章实体(你可能需要水平滚动代码块以查看添加)

class Article
{
    /**
     * @ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
     */
    private $tags;

您可以在此处阅读 fetch =" EXTRA_LAZY" 选项:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html

您可以通过在Google上搜索" Doctrine + indexed association"来阅读 indexBy =" name" 选项没有引号,并查找作为Doctrine的文档的一部分的结果(不能发布另一个链接,因为我的声誉不够高)。

接下来,我修改了 addTag()方法的版本,如下所示:

public function addTag(Tag $tags)
{
    // Check for an existing entity in the DB based on the given
    // entity's PRIMARY KEY property value
    if ($this->tags->contains($tags)) {
        return $this; // or just return;
    }

    // This prevents adding duplicates of new tags that aren't in the
    // DB already.
    $tagKey = $tag->getName() ?? $tag->getHash();
    $this->tags[$tagKey] = $tags;
}

注意: ?? null coalesce运算符需要PHP7 +。

通过将标记的获取策略设置为 EXTRA_LAZY ,以下语句会导致Doctrine执行SQL查询以检查数据库中是否存在具有同名的标记(请参阅上面的相关 EXTRA_LAZY 链接了解更多信息:

$this->tags->contains($tags)

注意: 如果设置了传递给它的实体的 PRIMARY KEY 字段,则只能返回true。当使用 ArrayCollection :: contains()等方法时,Doctrine只能基于该实体的PRIMARY KEY查询数据库/实体映射中的现有实体。如果Tag实体的名称属性只是 UNIQUE KEY ,那可能就是为什么它总是返回false。您需要 PRIMARY KEY 才能有效地使用 contains()等方法。

if块之后的 addTag()方法中的其余代码通过 PRIMARY KEY 属性中的值为标记的ArrayCollection创建一个键(首选(如果不是null)或标记实体的哈希(搜索Google" PHP + spl_object_hash",由Doctrine用于索引实体)。因此,您正在创建一个索引关联,因此如果您在刷新之前将同一实体添加两次,则只需在同一个键上重新添加,但不会重复。