如何构建一个 Doctrine 查询来查找缺少特定类型的 OneToMany 关系的元素?

How to build a Doctrine query which would find elements with missing particular type of OneToMany relation?

假设我们有两个处于 OneToMany 关系中的实体,例如:

User 实体与 Rewards 实体和每个 实体有 OneToMany 关系奖励可能有不同的类型:例如'forOnboarding', 'forReferring', 'forBeingReerred'.

User {
    /**
     * @var Reward[]
     *
     * @ORM\OneToMany(targetEntity="Reward", mappedBy="rewardee"}
     */
    private $rewards;
}


Reward {
    /*
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User", inversedBy="rewards")
     * @ORM\JoinColumn(nullable=false)
     */
    private $rewardee;

    /** @var string */
    private $type;
}

很容易找到与确切奖励类型相关的用户,例如:

    public function findUsersWithRewardType(string $rewardType): ?array
    {
        $qb = $this->createQueryBuilder('u')
            ->leftJoin('u.rewards', 'r')
            ->where('r.type = :reward_type')
            ->setParameter('reward_type', $rewardType);
        $query = $qb->getQuery();
        $result = $query->getResult();

        return $result;
    }

现在我想构建一个查询来查找所有缺少特定类型奖励的用户。这有点棘手,因此我们将不胜感激!

由于 OneToMany 关系的性质,我不能简单地将条件从等于更改为不等于,也不能使用关系为空的条件(如下例所示),因为它不会 return 有效结果,因为用户可能与另一种类型的奖励有关。

    public function findUsersWithoutRewardType(string $rewardType): ?array
    {
        $qb = $this->createQueryBuilder('u');
        $qb
            ->leftJoin('u.rewards', 'r')
            ->where('r.type = :reward_type')
            ->setParameter('reward_type', $rewardType)
            ->andWhere('r is NULL');
        $query = $qb->getQuery();
        $result = $query->getResult();

        return $result;
    }

PS:即使所有用户都与奖励无关,上述方法也不起作用。可能是因为 属性 User->rewards 实际上是 ArrayCollection 并且从未设置为 null。

谢谢!

我设法使用以下 DQL 找到了可行的解决方案:

    public function findUsersWithoutRewardType(string $rewardType): ?array
    {
        if (!in_array($rewardType, Reward::TYPE__OPTIONS)) {
            new \Exception(sprintf('Invalid type %s. Possible options are: %s.', $rewardType, implode(',', Reward::TYPE__OPTIONS)));
        }

        $query = $this
            ->getEntityManager()
            ->createQuery('SELECT u.username FROM App\Entity\User u WHERE u.enabled = true AND NOT EXISTS (SELECT  1 FROM App\Entity\Reward r WHERE u = r.rewardee AND r.type = :reward_type) ORDER BY u.createdAt DESC')
            ->setParameters(['reward_type' => $rewardType]);
        $users = $query->getResult();

        return $users;
    }

有没有办法实现与查询生成器等效的东西?

我也找到了一个与查询生成器等效的解决方案:

    public function findUsersWithoutRewardType(string $rewardType, $maxResults = 10): ?array
    {
        if (!in_array($rewardType, Reward::TYPE__OPTIONS)) {
            new \Exception(sprintf('Invalid type %s. Possible options are: %s.', $rewardType, implode(',', Reward::TYPE__OPTIONS)));
        }

        $qb = $this->createQueryBuilder('u');
        $qb
            ->where('u.enabled = true')
            ->andWhere($qb->expr()->not($qb->expr()->exists($this->getEntityManager()->createQueryBuilder()->select('r')->from(Reward::class, 'r')->where('u = r.rewardee')->andWhere('r.type = :reward_type')->getDQL())))
            ->orderBy('u.createdAt', 'DESC')
            ->setParameter('reward_type', $rewardType)
            ->setMaxResults($maxResults);
        $query = $qb->getQuery();
        $users = $query->getResult();

        return $users;
    }