【问题标题】:How to avoid duplicate entries in a many-to-many relationship with Doctrine?如何避免与 Doctrine 的多对多关系中的重复条目?
【发布时间】:2014-02-11 20:49:12
【问题描述】:

我正在使用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 表中?

【问题讨论】:

标签: php symfony doctrine-orm


【解决方案1】:

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

这里部分解释了解决方案: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;

You can read up about the fetch="EXTRA_LAZY" option here.

You can read up about indexBy="name" option here.

接下来,我将您的 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;
}

注意: ?? 空合并运算符需要 PHP7+。

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

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

注意: 只有在设置了传递给它的实体的 PRIMARY KEY 字段时才能返回 true。当使用 ArrayCollection::contains() 等方法时,Doctrine 只能根据实体的 PRIMARY KEY 查询数据库/实体映射中的现有实体。如果 Tag 实体的 name 属性只是一个 UNIQUE KEY,这可能就是它总是返回 false 的原因。您需要 PRIMARY KEY 才能有效地使用 contains() 等方法。

if 块之后的 addTag() 方法中的其余代码通过 PRIMARY KEY 属性中的值为标签的 ArrayCollection 创建一个键(如果不为空,则首选)或 Tag 实体的哈希(在 Google 中搜索“PHP + spl_object_hash”,Doctrine 使用它来索引实体)。因此,您正在创建一个索引关联,因此如果您在刷新之前添加了两次相同的实体,它只会在相同的键处重新添加,但不会重复。

【讨论】:

    【解决方案2】:

    两种主要解决方案

    首先

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

    第二

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

    您可以了解更多关于 prePersist here

    第二个解决方案的提示

    为搜索和获取旧标签(如果有)制作自定义存储库方法

    【讨论】:

    • 标签在Article 实体的prePersist 事件之前持久化,所以我不能只遍历$tags 来检查它们是否已经存在(使用自定义TagRepository)。那么我应该如何将自定义存储库方法连接到生命周期回调呢?难道不用事件监听器吗?
    • @Gergő:我想prePersist 可以被归类为事件侦听器,但我不确定......但是如果在文章之前保留标签,您应该遵循我提出的第一个解决方案跨度>
    • 我创建了一个成功返回现有标签对象的数据转换器,但 Doctrine 仍然尝试执行INSERT 而不是UPDATE,所以它只是抛出了一个异常。我该如何解决?
    • @Gergő:你肯定做错了什么,但我不知道是什么。如果您尝试持久化预取的数据库对象,它将导致更新,这是教义逻辑。
    • 我已经向new question 询问了有关异常的更多详细信息。
    猜你喜欢
    • 2016-11-22
    • 1970-01-01
    • 1970-01-01
    • 2019-10-03
    • 1970-01-01
    • 1970-01-01
    • 2017-10-29
    • 2013-04-08
    • 1970-01-01
    相关资源
    最近更新 更多