ManyToMany Doctrine join 必须 return 一行

ManyToMany Doctrine join must return one row

假设你在 Doctrine 中有一个经典的 ManyToMany 关系。例如,official documentation 中描述的那个:

/** @Entity */
class User
{
    // ...

    /**
     * Many Users have Many Groups.
     * @ManyToMany(targetEntity="Group")
     * @JoinTable(name="users_groups",
     *      joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")}
     *      )
     */
    private $groups;

    // ...

    public function __construct() {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }
}

/** @Entity */
class Group
{
    /**
    * @ORM\Column(type="string")
    */
    private $type;
}

为了简化示例,只有 2 种可能的组类型:A 或 B。一个用户不能属于具有相同类型的 2 个组。因此,每个用户可以属于 0 到 2 个组。

现在假设我想 return 每个用户一行(不使用 getArrayResults() 方法进行水合作用,如下所示:

----------------------------------------------------------------------------
| user.id    | group_A.type | group_A.id     | group_B.type | group_B.id   |
----------------------------------------------------------------------------
| 1          | A            | 10             | B            | 20           |
----------------------------------------------------------------------------
| 2          | A            | 10             | B            | 21           |
----------------------------------------------------------------------------
| 3          | A            | 11             | NULL         | NULL         |
----------------------------------------------------------------------------

如何在 DQL 中翻译该结果?我可以用 innerJoin() 方法得到它是所有用户都有一个组。但如果不是这样,一个leftJoin()查询return多于2行。

这种查询的主要问题是由于 class 用户和组之间的非系统性 link。如果每个用户 link 编到每个组,一个简单的 DQL 查询将起作用:

$qb->select('u.id, gA.type, gA.id, gB.type, gB.id')
    ->from('User', 'u')
    ->innerJoin('u.groups', 'gA', 'WITH', 'gA.type = "A"')
    ->innerJoin('u.groups', 'gB', 'WITH', 'gB.type = "B"')

但在上面给出的示例中,结果将是因为用户 #3 没有 link 组 #B :

----------------------------------------------------------------------------
| user.id    | group_A.type | group_A.id     | group_B.type | group_B.id   |
----------------------------------------------------------------------------
| 1          | A            | 10             | B            | 20           |
----------------------------------------------------------------------------
| 2          | A            | 10             | B            | 21           |
----------------------------------------------------------------------------

通过使用 leftJoin 子句,结果将是最大的 table 超过 3 条记录(上例中为 5 条)。

所以解决方案是查询中没有 where 子句是使用 combine MAX 和 groupBy :

$qb->select('u.id, gA.type, max(gA.id), gB.type, max(gB.id)')
    ->from('User', 'u')
    ->innerJoin('u.groups', 'gA', 'WITH', 'gA.type = "A"')
    ->innerJoin('u.groups', 'gB', 'WITH', 'gB.type = "B"')
    ->groupBy('u.id')

这将给出预期的结果:

----------------------------------------------------------------------------
| user.id    | group_A.type | group_A.id     | group_B.type | group_B.id   |
----------------------------------------------------------------------------
| 1          | A            | 10             | B            | 20           |
----------------------------------------------------------------------------
| 2          | A            | 10             | B            | 21           |
----------------------------------------------------------------------------
| 3          | A            | 11             | NULL         | NULL         |
----------------------------------------------------------------------------

如果在查询中应用 where 子句,可以避免这种情况:

$qb->select('u.id, gA.type, gA.id, gB.type, gB.id')
    ->from('User', 'u')
    ->innerJoin('u.groups', 'gA', 'WITH', 'gA.type = "A"')
    ->innerJoin('u.groups', 'gB', 'WITH', 'gB.type = "B"')
    ->where('gA.type = 10 AND gB.type = 20)

给出以下结果:

----------------------------------------------------------------------------
| user.id    | group_A.type | group_A.id     | group_B.type | group_B.id   |
----------------------------------------------------------------------------
| 1          | A            | 10             | B            | 20           |
----------------------------------------------------------------------------