使用Doctrine ODM加载相关文档导致过多的查询。

4

我正在尝试在Web API上减少数据库查询次数,但遇到了困难。

我的数据库有3个集合:playgroundwidgettoken

一个游乐场(playground)有多个小部件(widgets),一个小部件有一个令牌(token)。

每个关系使用referencesOne / referenceMany

以下是我简化后的模型:

/**
 * @MongoDB\Document()
 */
class Widget
{
    /**
     * @MongoDB\ReferenceOne(targetDocument="Token", inversedBy="widgets")
     */
    protected $token;

    /**
     * @MongoDB\ReferenceOne(targetDocument="Playground", inversedBy="widgets")
     */
    protected $playground;
}

/**
 * @MongoDB\Document()
 */
class Playground
{
    /**
     * @MongoDB\ReferenceMany(targetDocument="Widget", mappedBy="playground")
     */
    protected $widgets;
}

/**
 * @MongoDB\Document()
 */
class Token
{
    /**
     * @MongoDB\ReferenceMany(targetDocument="Widget", mappedBy="token")
     */
    protected $widgets;
}

我需要使用完整的 playground,包括所有的 widgets 和 tokens,但默认情况下,Doctrine 执行了太多的查询:一个用于获取 playground(好的),一个用于获取所有映射的 widgets(好的),而对于每个 widget,都会执行一次查询以获取 token(不好)。是否有一种方式可以一次性查询所有的 tokens 而不是逐个获取?
我已经查看了 prime,但它似乎不能解决我的问题...
除了使用查询构建器并手动转换所有对象以减少查询计数之外,是否还有其他方法?
编辑: 正如我在评论中所添加的,我所要寻找的是将 playground 及其所有依赖项作为一个大对象获取,对其进行 json 编码,并将其返回到响应中。
目前的做法是查询 playground 并编码它,但是 Doctrine 以非高效的方式填充了这些依赖项:首先是查询获取 playgroung,然后,有另一个查询来获取相关的 widgets,对于每个 widget 都会有一个查询以获取其 token。
由于一个 playground 可能有数百个 widgets,因此会导致数百个数据库查询。
我所要寻找的是一种告诉 Doctrine 仅使用 3 个查询(一个用于获取 playgroung,一个用于获取 widgets,一个用于获取 tokens)获取所有这些数据的方法。

我不太明白这里的问题。你想要数据库中的所有“Token”吗?你可以使用$tokenRepository->findAll();来获取所有的“Token”,或者使用$tokenRepository->findBy(array $criteria);,其中$criteria是你的查询参数。 - Etshy
@Etshy 不,我不是在寻找关于令牌的findAll方法,我想要一个游乐场及其所有依赖项,并将文档返回为json格式。Doctrine没有以高效的方式查询数据库:它所做的是1)获取游乐场,2)获取相关小部件,3)对于每个小部件,获取相关令牌。由于一个游乐场可能有数百个小部件,这会导致数百个数据库查询。我正在寻找一种告诉Doctrine循环遍历所有游乐场小部件以获取相关令牌ID,然后一次性获取所有令牌的方法。这样无论小部件数量如何,总是只有3个查询。 - ᴄʀᴏᴢᴇᴛ
哦,好的。我不确定你怎么做到的。你可以尝试像这样使用 findBy(['widgets.id' => $widgetsIds]),其中 $widgetsIds 是一个 ID 数组。但我真的不确定 findBy 能否使用这样的数组。 - Etshy
你考虑过使用聚合吗?因为在我看来,这里你所需要的就是一个聚合管道。 - lovubuntu
编写自定义查询可能更简单且性能更高。 - Dan
@lovubuntu 这就是我计划使用的,但由于我正在使用DBRef来存储关系,所以我无法使用$lookup(请参见此处https://medium.com/@alcaeus/introducing-doctrine-mongodb-odm-1-2-bd85d6c8261a),除非有一种方法将所有现有文档迁移到新的ref格式? - ᴄʀᴏᴢᴇᴛ
2个回答

1
更新: 由于$playground中的ArrayCollection应包含所有小部件,至少作为代理对象存在(或在访问时应加载),因此以下操作应该有效地获取所有所需令牌...
由于文档管理器保留所有受管理对象,它应该防止额外的查询发生。(请注意从execute中省略的赋值)。
$qb = $dm->createQueryBuilder('Token')->findBy(['widget' => $playground->getWidgets()]);
$qb->getQuery()->execute();

避免Doctrine ORM陷阱的页面-第5点的启发

原始回答

老实说,我对mongodb不太熟悉,但根据Doctrine的基础引用,你应该能够通过以下方式相当轻松地填充你的 playground:

$qb = $dm->createQueryBuilder('Widget')->findBy(['playground' => $playground]);
$qb->field('token')->prime(true);
$widgets = $qb->getQuery()->execute();

然而,我可能是非常错误的。

@ᴄʀᴏᴢᴇᴛ 这可能是一个愚蠢的问题:在mongodb中的Widget中不包含playground或令牌的ID?我想知道我的逻辑反转是否有效(请参见更新)。 - Jakumi
我太快地读了你的答案,是的,我可以从小部件中获取令牌(小部件具有令牌ID)。这更或多或少是我最终所做的,但我并不真的喜欢这个解决方案,因为这样,我在一个变量中拥有游乐场,而在另一个变量中拥有小部件+令牌。因此,我无法使用$playground->getWidgets()(否则Doctrine将运行数据库查询,因为没有结果缓存),或者我必须手动将小部件设置到游乐场中,我认为这不是一个好的实践...无论如何,似乎没有更好的选择...也许问题出在数据库设计上。 - ᴄʀᴏᴢᴇᴛ
这是所需的查询语句:$playgroundRepository->createQueryBuilder()->field('widgets.token')->prime()->field('id')->equals($playgroundId);,但不幸的是,我无法预先加载深层关系。 - ᴄʀᴏᴢᴇᴛ
1
@ᴄʀᴏᴢᴇᴛ,我认为你低估了DocumentManager的作用。当你已经加载所有token时,它将防止“n + 1”查询的发生。请检查(只需通过 $playground->getWidgets() 然后 $widget->getToken())。 - Jakumi
@ᴄʀᴏᴢᴇᴛ 很高兴我能帮到你 ;o) - Jakumi
显示剩余2条评论

-1

那个怎么样:

class Foo
{
    /** @var \Doctrine\ORM\EntityManagerInterface */
    private $entityManager;

    public function __construct(\Doctrine\ORM\EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function getAll(): array {
        $qb = $this->entityManager->createQueryBuilder();
        return $qb
            ->select('p,t,w')
            ->from(Playground::class, 'p')
            ->join(Widget::class, 'w')
            ->join(Token::class, 't')
            ->getQuery()
            ->getResult();
    }
}

至少在使用mysql后端时,这可以解决“n+1问题”。不过对于MongoDB我不确定。


1
不幸的是,MongoDB 中没有 join - ᴄʀᴏᴢᴇᴛ

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