首页 > 解决方案 > 排除多对多关系中标记故事中的标签列表

问题描述

我有两个实体,Story 和 Tag,它们以多对多关系链接。

我正在尝试获取所有不包含一个或多个标签(或查询、排除过滤器)的故事。因此,如果一个故事有标签 A、B 和 C,而我排除了标签 B 和 E,则不应显示该故事。当然,如果它同时具有 B 和 E 标签,也不会。

将版本缩减为以下相关文件。

故事实体

<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\StoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=StoryRepository::class)
 */
class Story
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private int $id;

    /**
     * @ORM\ManyToMany(targetEntity="Tag", inversedBy="stories")
     */
    private Collection $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getTags()
    {
        return $this->tags;
    }

    public function addTag(Tag $tag): void
    {
        $this->tags->add($tag);
    }

    /**
     * Setting tags, clearing existing ones.
     */
    public function setTags($tags): void
    {
        $this->tags->clear();
        foreach ($tags as $tag) {
            if (is_a($tag, Tag::class)) {
                $this->tags->add($tag);
            }
        }
    }

    public function removeTag(Tag $tag): void
    {
        if ($this->tags->contains($tag)) {
            $this->tags->removeElement($tag);
        }
    }
}

标记实体

<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\TagRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=TagRepository::class)
 */
class Tag
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private int $id;

    /**
     * @ORM\ManyToMany(targetEntity="Story", mappedBy="tags")
     */
    private Collection $stories;

    public function __construct()
    {
        $this->stories = new ArrayCollection();
    }

    public function getId(): int
    {
        return $this->id;
    }
}

所以 Story 实体是拥有方。我正在使用查询生成器来查询 StoryRepository 类中的故事。

有一个有效的包含功能,但逻辑略有不同,所有给定的标签都必须存在才能显示故事。我正在使用这段代码来实现这一点,其中$includeTags是一个 Tag 实体数组:

$qb = $this->createQueryBuilder('s');
$qb->leftJoin('s.tags', 't');

$qb->join('s.tags', 'it', Expr\Join::WITH, 'it IN (:includeTags)');
$qb->setParameter('includeTags', $includeTags);

现在为了排除我天真地尝试了这样的事情:

$qb->leftJoin('s.tags', 'et', Expr\Join::ON, 'et NOT IN (:excludeTags)');
$qb->setParameter('excludeTags', $excludeTags);

这导致Error: Expected end of string, got 'ON' Symfony 中的 DQL 查询异常向我显示以下 DQL 输出:SELECT s FROM App\Entity\Story s LEFT JOIN s.tags t LEFT JOIN s.tags et ON et NOT IN (:excludeTags)

另一种尝试是

$qb->andWhere($qb->expr()->notIn('s.tags', array_map(function($tag) { return $tag->getId(); }, $excludeTags)));

哪个返回[Semantical Error] line 0, col X near 'tags NOT IN(1)': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.

直接输入实体会在表达式处理上产生错误,将其缩减为如下所示的准系统:

$qb->andWhere('s.tags NOT IN (:excludeTags)');
$qb->setParameter('excludeTags', $excludeTags);

带来与上面相同的语义错误,只是用(:excludeTags)而不是(1)

我很确定我遗漏了一些明显的东西,但我找不到任何答案来搜索一般的术语,其中排除列表或实体数组(甚至只是 id 数组)从另一个像这样在 Doctrine 中链接的多对多实体. 还专门搜索了标签,因为我确定其他人之前一定已经这样做过,但我发现没有一个能与我在这里需要的东西相近。

按下,我可以只用 SQL 来做,我知道我可以排除那些相对容易的,但我不知道如何做到这一点“教义方式”(tm)仍然能够从查询生成器和所有其他好东西。任何帮助深表感谢。

更新: 我确实注意到我换了一条线。所以我尝试了以下行,当然还有参数。

$qb->leftJoin('s.tags', 'et', Expr\Join::WITH, 'et NOT IN (:excludeTags)');

现在这导致查询运行得很好,它什么也没做,它仍然显示应该过滤掉的故事。获取从中生成的 SQL,我可以清楚地看到原因:

... LEFT JOIN story_tag s5_ ON s0_.id = s5_.story_id LEFT JOIN tag t4_ ON t4_.id = s5_.tag_id AND (t4_.id NOT IN (?)) ...

输入高度赞赏。

更新 2

纯 sql 中带有子查询的此查询按预期返回正确的数据:

SELECT id FROM story WHERE id NOT IN (SELECT story_id FROM story_tag WHERE tag_id IN (1))

我非常想避免做一个子查询,因为我可以使用包含标签查询,但如果不可能,那么我就咬紧牙关。

标签: phpsymfonydoctrine-orm

解决方案


好的,我也是在 Stackoverflow 上偶然偶然发现的,请参见此处:教义 2 中的子查询 notIN 函数

解决方案是以下几行,提醒一下,这$excludeTags是一个标签实体数组。

$qb->andWhere(':excludeTags NOT MEMBER OF s.tags');
$qb->setParameter('excludeTags', $excludeTags);

现在这有效,但它也创建了一个子选择:

... AND NOT EXISTS (SELECT 1 FROM story_tag s4_ WHERE s4_.story_id = s0_.id AND s4_.tag_id IN (?)) ...

我现在会使用它,但如果有人有更好的解决方案来回避子查询,请告诉我,我很乐意选择你的答案。


推荐阅读