verclam/smart-fetch-bundle

此包将帮助您避免在Symfony Doctrine实体中使用n+1查询

安装: 13

依赖项: 0

建议者: 0

安全: 0

星标: 1

关注者: 1

分支: 0

开放问题: 9

类型:symfony-bundle

0.2 2024-04-12 11:04 UTC

README

通过Smart Fetch Bundle避免N+1查询,增强项目性能。

最新更新

有关最新更改的说明,请阅读CHANGELOG,有关代码中所需更改的说明,请阅读文档中的UPGRADE章节。

要求

  • Verclam/Smart-Fetch-Bundle的master版本与Symfony >=5.4版本兼容。
  • "doctrine/persistence" 和 "doctrine/doctrine-bundle" 需要至少 >=2.2 版本。
  • 需要PHP >=8.0 版本。

安装

使用Composer非常简单,运行

composer require verclam/smart-fetch-bundle

使用方法

1 - 配置

无需操作,此包已准备好使用,您可以直接在项目中使用它并实现它。

2 - 在控制器方法中使用

要在控制器方法中使用此包,您必须将#[SmartFetch]属性添加到参数或方法。

    #[Route('{id}/user', name: 'user_get', requirements: [ 'id' => '\d+' ], methods: ['GET'])]
    #[SmartFetch(
        queryName: 'id',
        class: User::class, 
        joinEntities: 
        [
            'articles', 
            'articles.comments', 
            'articles.categories',
            'comments.likedBy'
        ],
        argumentName: 'user'
    )]
    public function index(User $user): Response
    {

        return $this->render('user/get.html.twig', [
            'user'              => $user,
        ]);
    }
  • queryName:用于获取实体'必填'的URL中查询参数的名称。
  • class:要获取的实体类'必填'。
  • joinEntities:要获取的关联,关联的顺序很重要。
  • argumentName:方法参数中的变量名称'如果获取多个实体则为必填'。

3 - 在控制器参数中使用

    #[Route('{id}/user', name: 'user_get', requirements: [ 'id' => '\d+' ], methods: ['GET'])]
    public function index(
        #[SmartFetch(
            queryName: 'id',
            joinEntities:
            [
                'articles',
                'articles.comments',
                'articles.categories',
                'comments.likedBy'
            ],
        )] User $user
    ): Response
    {

        return $this->render('user/get.html.twig', [
            'user'              => $user,
        ]);
    }
  • queryName:用于获取实体'必填'的URL中查询参数的名称。
  • joinEntities:要获取的关联,关联的顺序很重要。
  • argumentName:非必填。
  • class:非必填。

实体

假设您有一个项目,其中用户可以发表文章和评论,用户还可以喜欢评论,文章可以属于多个类别。此项目的实体将如下所示

用户

    // ... other properties
    #[ORM\Column(length: 255)]
    private ?string $firstname = null;

    #[ORM\Column(length: 255)]
    private ?string $lastname = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
    private ?\DateTimeInterface $birthDate = null;

    #[ORM\OneToMany(mappedBy: 'createdBy', targetEntity: Article::class)]
    private Collection $articles;

    #[ORM\OneToMany(mappedBy: 'createdBy', targetEntity: Comment::class)]
    private Collection $comments;

    #[ORM\ManyToMany(targetEntity: Comment::class, mappedBy: 'likedBy')]
    private Collection $likedComments;

文章

    // ... other properties
    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $content = null;

    #[ORM\ManyToOne(inversedBy: 'articles')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $createdBy = null;

    #[ORM\ManyToMany(targetEntity: Category::class, inversedBy: 'articles')]
    private Collection $categories;

    #[ORM\OneToMany(mappedBy: 'article', targetEntity: Comment::class)]
    private Collection $comments;

评论

    // ... other properties
    #[ORM\Column(type: Types::TEXT)]
    private ?string $content = null;

    #[ORM\ManyToOne(inversedBy: 'comments')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $createdBy = null;

    #[ORM\ManyToOne(inversedBy: 'comments')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Article $article = null;

    #[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'likedComments')]
    private Collection $likedBy;

类别

    // ... other properties
    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'categories')]
    private Collection $articles;

控制器

现在,假设您想获取一个包含所有文章和评论的用户以及每个文章的所有类别以及喜欢每篇文章评论的所有用户,并在视图中显示它,控制器和视图。

控制器

    #[Route('{id}/user', name: 'user_get', requirements: [ 'id' => '\d+' ], methods: ['GET'])]
    public function show(User $user): Response
    {
        return $this->render('user/show.html.twig', [
            'user' => $user,
        ]);
    }

视图

    <h1>Hello {{ user }}! ✅</h1>

    The article that you created are:
    <ul>
        {% for article in user.articles %}
            <li>
                {{ article.title }}
                This article has {{ article.comments|length }} comments which are.
                <ul>
                    {% for comment in article.comments %}
                        <li>
                            {{ comment.content ~ ' created by:' ~ comment.createdBy ~ ' and liked by :'  }}
                            {% for liker in comment.likedBy %}
                                {{ liker }}
                            {% endfor %}
                        </li>
                    {% endfor %}
                </ul>
                Category:
                {% for category in article.categories %}
                    {{ category.name }}
                {% endfor %}
            </li>
        {% endfor %}
    </ul>

您认为Symfony和Doctrine会做什么?

img.png 爆炸,是的,因为控制器中的用户将使用以下简单的SQL查询进行检索

SELECT t0.id AS id_1, t0.firstname AS firstname_2, t0.lastname AS lastname_3, t0.birth_date AS birth_date_4 FROM user t0 WHERE t0.id = '11';

所有关联都将是一个代理,每次访问关联时,都会执行一个新的SQL查询来检索单个实体。当视图渲染时,如果查看Symfony分析工具栏的“Doctrine”部分,您将看到它以黄色颜色显示,换句话说,是警告,这可能是一个N+1查询。

以下是查询度量

img_1.png

img_2.png

如何避免这种情况?

使用Smart Fetch Bundle,您可以在单个查询中获取所有想要的关联,您还可以获取关联的关联,关联的关联的关联,依此类推。

如何操作?

安装包之后,你只需将 #[SmartFetch] 属性添加到属性或方法中,并指出你想要获取的内容。

示例

控制器
在方法上
    #[Route('{id}/user', name: 'user_get', requirements: [ 'id' => '\d+' ], methods: ['GET'])]
    #[SmartFetch(
        queryName: 'id',
        class: User::class, 
        joinEntities: 
        [
            'articles', 
            'articles.comments', 
            'articles.categories',
            'comments.likedBy'
        ],
        argumentName: 'user'
    )]
    public function index(User $user): Response
    {

        return $this->render('user/get.html.twig', [
            'user'              => $user,
        ]);
    }
在参数上
    #[Route('{id}/user', name: 'user_get', requirements: [ 'id' => '\d+' ], methods: ['GET'])]
    public function index(
        #[SmartFetch(
            queryName: 'id',
            joinEntities:
            [
                'articles',
                'articles.comments',
                'articles.categories',
                'comments.likedBy'
            ],
        )] User $user
    ): Response
    {

        return $this->render('user/get.html.twig', [
            'user'              => $user,
        ]);
    }

您认为Symfony和Doctrine会做什么?

没什么问题,你想要的 User 及其所有关系将在一个查询中获取。查询指标如下

img.png img_1.png

这难道不令人惊叹吗?😍

不要浪费时间,安装包并享受它。

PS:如果你打算使用这个包在一次查询中检索几百个实体,请先停下来再考虑一下,因为这并不合理。当 Doctrine 将数千行数据填充到实体中时,它可能会崩溃,所以我们的建议是使用这个包来在接受n+1程度和执行连接之间找到合适的平衡。