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

Ger*_*rgő 5 php symfony doctrine-orm

我正在使用嵌入的 Symfony 表单Tag从文章编辑器中添加和删​​除实体。Article是协会的拥有方

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

    public function addArticle(Article $articles)
    {
        $this->articles[] = $articles;
    }
}
Run Code Online (Sandbox Code Playgroud)

我已设置$name为唯一,因为我想每次在表单中输入相同的名称时都使用相同的标签。但它不能这样工作,我得到了例外:

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

我需要更改什么才能使用article_tag提交标记名称时的默认连接表,该Tag表已在表中?

小智 6

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

解决方案部分解释如下: http //docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-刷新期间的约束失败

您的代码已经完成了一半:

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

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

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

您可以在此处阅读有关fetch="EXTRA_LAZY"选项的信息。

您可以在此处阅读有关indexBy="name"选项的信息。

接下来,我修改了我的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;
}
Run Code Online (Sandbox Code Playgroud)

注:null 合并运算符需要 PHP7+。

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

$this->tags->contains($tags)
Run Code Online (Sandbox Code Playgroud)

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

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


Don*_*sto 4

两个主要解决方案

第一的

使用数据转换器

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

第二

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

您可以在此处了解有关 prePersist 的更多信息

第二个解决方案的提示

创建一个自定义存储库方法来搜索和获取旧标签(如果有)