Symfony/Doctrine:优化n+1查询

4

背景

想象一组提案,每个用户都可以为提案投赞成或反对票。

所以我有两个模型:

  • 一个 Proposal(提案)
  • 一个与 proposaluseropinion(赞成或反对)相关的 Vote(投票)

现在我想要显示所有的提案,包括每个提案的额外信息:

  • 有多少个赞成票?
  • 有多少个反对票?
  • 当前用户是否已经赞成?
  • 当前用户是否已经反对?

问题

这很容易实现,但这意味着每次显示提案都需要4个请求(称为n+1查询问题)。 我来自Rails世界,在那里可以使用急切加载或一些巧妙的查询轻松解决这个问题。但我无法用doctrine解决它,需要一些帮助!

我尝试过的方法

使用神奇字段

第一种解决方案是制作自定义查询:选择所有提案,包括赞成票数、反对票数、当前用户是否已经赞成或反对。

查询现在返回比模型描述多4个列:count_upvotescount_downvoteshas_user_upvotedhas_user_downvoted

为了能够与doctrine一起使用,我必须将这些字段添加到我的Proposal实体中。

对我来说,这不是一个好的解决方案,因为它意味着必须始终使用此查询以避免混乱的模型,其中这些字段可能为空。

使用摘要

第二种解决方案是创建另一个对象供视图使用。该对象是由当前用户作为参数创建的。该对象通过优化的查询创建,并包含以下方法:

  • getAllProposals()
  • getUpvotes($a_proposal)
  • getDownvotes($a_proposal)
  • hasUserUpvoted($a_proposal)
  • hasUserDownvoted($a_proposal)

对我来说,为了视图优化而被迫创建一个对象确实有点过度。

使用扩展实体

这第三种解决方案使用唯一的查询获取提案、它们的赞成票、反对票,并检查用户是否已经赞成或反对。

它创建了一个新的“扩展”实体,

public function __construct(
    Proposal $badgeProposal,
    $upvotesCount,
    $downvotesCount,
    $hasUserUpvoted,
    $hasUserDownvoted
) {

对我来说,这是更好的解决方案,但目前无法实现,因为我不知道如何从SQL行中简单地填充提议(但我正在努力寻找)。

你的回合

那么,还有其他解决方案吗?这应该是Doctrine开发人员熟知的问题。

1个回答

6

您是否已经进行了实验或只是想到了这个问题?

对我来说,一个简单的 fetch="EAGER" 就解决了这个问题,从而只产生了一个查询。采用以下设置:

class Proposal {
    /**
     * @ORM\OneToMany(targetEntity="Vote", mappedBy="proposals", fetch="EAGER")
     */
    protected $votes;
}

class Vote {
    /**
     * @ORM\ManyToOne(targetEntity="Proposal", inversedBy="votes")
     * @ORM\JoinColumn(..)
     */
    protected $proposal;
    /**
     * @ORM\ManyToOne(targetEntity="Opinion", inversedBy="votes")
     * @ORM\JoinColumn(..)
     */
    protected $opinion;
    /**
     * @ORM\ManyToOne(targetEntity="User", inversedBy="votes")
     * @ORM\JoinColumn(..)
     */
    protected $user;
}

class Opinion/User {
    /**
     * @ORM\OneToMany(targetEntity="Vote", mappedBy="proposals")
     */
    protected $votes;
}

实际上,我进一步编辑了Proposal实体,添加了以下方法。
class Proposal {
    public function getCountOpinion($id) {
        $count = 0;
        foreach ($this->getVotes() as $vote) {
            if ($vote->getOpinion()->getId() === $id) {
                $count++;
            }
        }
        return $count;
    }

    public function getUserVote($user) {
        foreach ($this->getVotes() as $vote) {
            if ($vote->getUser() == $user) {
                return $vote;
            }
        }
        return null;
    }
}

正如我在开头提到的那样,这仍然只导致了一个查询。但是,你会发现这里有很多 for。如果对于每个提案都有大量的投票,则可能希望缓存结果(例如,仅一次迭代投票,获取所有的countOpinion和userVote)。

附注:不要忘记为具有ArrayCollections(OneToMany)的类添加构造函数。


嗨@kero!非常有趣的提议。这个方法非常有效。我认为你的方法的一个负面点是,我们会将所有投票实体进行填充,即使我们从未使用过它,因为我们只是“计数”它。但是非常有趣。 - pierallard
你是对的。我猜另一个解决方案是使用 DQL 编写自定义查询,使用 SUM 等函数。 - kero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接