malef / associate
此库允许通过关联收集对象,并为 Doctrine ORM 提供实体加载优化,以解决 N+1 查询问题。
Requires
- php: ^7.2
- nicmart/tree: ^0.2.7
- symfony/options-resolver: ^3.0|^4.0|^5.0
- symfony/property-access: ^3.0|^4.0|^5.0
Requires (Dev)
- ext-sqlite3: ^7.2
- doctrine/orm: ^2.4,>=2.4.5
- friendsofphp/php-cs-fixer: ^2.16.3
- nelmio/alice: ^3.5
- overtrue/phplint: ^1.1
- phpmd/phpmd: ^2.6
- phpstan/phpstan: ^0.12
- phpunit/dbunit: ^4.0
- phpunit/phpunit: ^7.0
- symfony/yaml: ^3.0|^4.0|^5.0
- webonyx/graphql-php: ^0.9,>=0.9.0
Suggests
- doctrine/orm: ^2.4,>=2.4.5
- webonyx/graphql-php: ^0.9,>=0.9.0
This package is auto-updated.
Last update: 2024-09-21 01:47:34 UTC
README
目录
介绍
此库为 Doctrine ORM 提供实体获取优化,以解决 N+1 查询问题。它与 webonyx/graphql-php
的 Deferred
实现配合得很好,可以显著减少数据库查询次数。
许可证
此组件采用 MIT 许可证。请参阅 LICENSE
文件中的完整许可证。
入门
使用以下方法将此组件包含到您的项目中(假设已全局安装 Composer):
$ composer require malef/associate
有关 Composer 的更多信息,请参阅其 简介。
要获取实体管理器的 EntityLoader
实例,您可以使用库提供的门面。对于更复杂的情况,您还需要 AssociationTreeBuilder
实例,可以使用 new
或门面进行实例化。
use Malef\Associate\DoctrineOrm\Facade; use Malef\Associate\DoctrineOrm\Association\AssociationTreeBuilder; $facade = new Facade($entityManager); $entityLoader = $facade->createEntityLoader(); $associationTreeBuilder1 = $facade->createAssociationTreeBuilder(); $associationTreeBuilder2 = new AssociationTreeBuilder();
您还可以使用这些类来定义适用于您所选择的框架的 DI 容器的服务。
这就完成了!现在您可以开始了!
使用示例
高效加载关联实体并解决 N+1 查询问题
原因
假设我们正在使用 doctrine/orm 构建一个电子商务网站。我们将遇到的一个问题是 N+1 查询问题。它发生在我们从数据库中获取一些实体,然后尝试通过获取器遍历它们的关联(例如,在序列化期间)。
以一个例子来说明,我们可能有一些需要列出的产品。每个产品都有一些变体,我们还需要显示这些变体。如果我们简单地将这组产品提供给我们的模板(或序列化器,如果我们提供某些 API),则在第一次尝试访问由 Doctrine ORM 管理的相应 PersistentCollection
时,将分别获取每个产品的变体。虽然这可以正常工作,但它将产生每个 Product
实例的一个 SELECT
查询。因此,如果我们以这种方式列出 100 个产品,我们将执行 101 个数据库查询,并且如果需要跟踪更多关系,这个数字还会增加。
一些 ORM 正在解决这个问题的某些基本案例。例如,在 Eloquent ORM 中,您可以使用 延迟 eager 加载。尽管如此,它仍然仅限于一次遍历一个关系。遗憾的是,Doctrine ORM 并没有提供类似的助手。
您可以在 Benjamin Eberlei 撰写的 5 Doctrine ORM 性能陷阱,您应该避免 中了解更多关于此问题的信息 - 请参阅标题为 Lazy-Loading 和 N+1 查询 的部分。其中指出了四种解决此问题的方法。
预加载(解决方案 3)可能是最简单的方法,但在许多情况下我们发现它太僵硬了。问题在于,它将始终加载相关实体,而通常我们只需要在少数特定情况下访问它们。
其他解决方案更加灵活,例如使用专门的DQL查询(方案1)或收集实体标识符后触发实体预加载(方案2)。然而,这些方案会导致代码繁琐,并且需要根据给定的关联是“一对一”还是“一对多”类型,以及已初始化的实体位于关联的逆向或拥有方进行调整。此外,我们有时需要联合其他实体进行过滤,我们不能简单地在一个查询中获取所有所需内容,因为需要由Doctrine ORM进行填充的结果集会变得过大。最后,如果已经初始化了一些\Doctrine\Common\Persistence\Proxy
实例或\Doctrine\ORM\PersistentCollection
实例,则可以应用一些微小的优化,从而跳过它们。
这个库试图以干净和封装的方式实现方案1和2,使其容易在多个场景中使用。
基本用法
在上面的示例中,只需在之前给出的代码之前添加以下内容
use Malef\Associate\DoctrineOrm\Facade; $facade = new Facade($entityManager); $entityLoader = $facade->createEntityLoader(); $entityLoader->load($products, 'variants', Product::class);
执行此代码片段后,所有给定产品的变体会通过单个SELECT
查询加载,调用getVariants
不会导致任何额外的查询。
可能的输入参数
Malef\Associate\DoctrineOrm\Loader\EntityLoader::load
方法接受以下参数
-
$entities
- 包含根实体的iterable
实例,加载器应为其加载关联;例如,可以是普通的array
或Doctrine的Collection
; -
$associations
- 包含一个或多个按顺序连接的关系名称的点分隔字符串(例如'profile'
,'order.item.product.variant'
);或包含一个或多个关系名称的array
(例如['profile']
,['order', 'item', 'product', 'variant']
);或Malef\Associate\DoctrineOrm\Association\AssociationTree
的实例(有关如何使用它的详细信息,请参阅下一节);只有最后一种情况支持在跟随关系时向多个方向分支; -
$entityClass
- 可选;包含包含在第一个数组中的实体的类名;如果没有给出,则实体加载器将尝试自动检测它;所有实体都需要共享单个实体类(或使用Doctrine继承功能的情况下的超类);
类似于EntityLoader::load
的方法的输入参数(如Malef\Associate\DoctrineOrm\Loader\DeferredEntityLoader::createDeferred
和Malef\Associate\DoctrineOrm\Loader\DeferredEntityLoaderFactory::create
)接受类似的参数。
加载多个关系
如果使用点分隔的字符串或字符串数组作为$associations
参数,则可以按顺序加载多个关联的实体。假设我们有一个Product
实体,它有许多Variant
,这些变体又有多个可用的Offer
,我们可以使用以下值作为$associations
-
'variants.offers'
, -
['variants', 'offers']
, -
按如下方式构建的
Malef\Associate\DoctrineOrm\Association\AssociationTree
的实例$associationTree = $associationTreeBuilder ->associate('variants') ->associate('offers') ->create();
这将允许我们稍后使用如$product->getVariants()
或$product->getVariants()->getOffers()
等方法,而不会产生任何额外的查询。
如果我们想跟随多个非顺序关联(即它们在某些方面分支成多个路径),我们唯一的选项是使用Malef\Associate\DoctrineOrm\Association\AssociationTree
。假设对于Offer
实体,我们有一个Seller
和多个Bidder
,我们可以使用以下代码
$associationTree = $associationTreeBuilder ->associate('variants') ->associate('offers') ->diverge() ->associate('seller') ->endDiverge() ->diverge() ->associate('bidders') ->endDiverge() ->create();
这样我们也可以调用$product->getVariants()->getOffers()->getSeller()
和$product->getVariants()->getOffers()->getBidders()
,而不会产生任何额外的查询。
分块
如果产品或相关实体的数量很多,则它们将被分块处理,并且每个块的关系将单独加载。块大小默认设置为 1000
,但您可以自由更改它,或者将其设置为 null
以禁用分块。
限制
重要! 从反向开始时,不可能减少一对一关联的查询数量 - Doctrine ORM 默认为每个实体发出单独的 SELECT
来加载它们。为了解决这个问题,您可以考虑将此类关联更改为一对一(并在之后使用此库),或者如果可能,使用可嵌入的关联(在这种情况下,嵌入的实体将与包含它们的实体使用相同的查询加载)。
延迟关联遍历以批量加载实体
如果您正在使用 Doctrine ORM 的项目并为其提供 GraphQL API,则此库可以很好地与 webonyx/graphql-php 提供的 Deferred
类一起使用。您可以在其文档的 解决 N+1 问题 部分了解更多关于此方法背后的基本概念。
假设我们需要实现一个 resolve
函数,该函数将返回 Variant
实例以用于 Product
实例。基本实现可能如下所示
$resolve = function(Product $product) { return $product->getVariants()->getValues(); };
但是,使用这种方法,我们最终仍然会对数据库执行 N+1 次查询。为了减轻这个问题,并有效地加载这些对象,我们可以使用 DeferredEntityLoader
的实例,如下所示
use Malef\Associate\DoctrineOrm\Facade; $facade = new Facade($entityManager); $deferredEntityLoader = $facade ->createDeferredEntityLoaderFactory() ->create('variants', Product::class); $resolve = function(Product $product) use ($deferredEntityLoader) { return $deferredEntityLoader->createDeferred( [$product], function() use ($product) { return $product->getVariants()->getValues(); } ); };
Et voilà!在此处,DeferredEntityLoader
将在 GraphQL 查询结果构建的同时累积所有实体。当 GraphQL 库尝试解决我们在 resolve
函数中返回的 Deferred
时,收集器将使用 EntityLoader
根据之前提供的关联树尽可能有效地加载所有实体。