Laravel 性能优化:优化 ORM 性能使应用程序高可用

Laravel

大家好,我是Valerio,来自意大利的软件工程师,也是Inspector的CTO。

在本文中,我将分享一套我正在几乎所有后端服务中使用的ORM优化策略。

我确信我们每个人都会抱怨机器或应用程序运行缓慢甚至死机,然后花时间在咖啡机上等待长时间运行的查询结果。

我们该如何解决?

开始吧!

数据库是共享资源

为什么数据库会导致如此多的性能问题?

我们经常忘记每个请求都不独立于其他请求。

如果一个请求很慢,似乎会影响其他请求…对吗?

同时在应用程序中运行的所有进程都使用数据库。即使只有一个设计不当的访问也可能会危害整个系统的性能。

因此,请谨慎看待「不优化代码也是可以的」。缓慢的数据库访问可能会使数据库紧张,从而给用户带来负面的体验。

N+1 个数据库查询问题

N + 1 问题是什么?

这是使用ORM与数据库进行交互时遇到的一个典型问题。这不是SQL编码问题。

当您使用Eloquent之类的ORM时,它并不总是很清楚将进行什么查询以及何时进行查询。对于这个特定问题,我们谈论关系和饥饿加载(预加载)。

任何ORM都允许您声明实体之间的关系,从而提供一个出色的API来导航我们的数据库结构。

*「文章和作者」 *是一个很好的例子。

/*
 * 每篇文章都属于一个作者
 */
$article = Article::find("1");
echo $article->author->name; 

/*
 * 每个作者有多个文章
 */
foreach (Article::all() as $article)
{
    echo $article->title;
}

但是我们需要谨慎的在循环中使用关联关系。

看下面的例子。

我们要在文章标题旁边添加作者的名字。多亏了ORM,我们可以导航Article与Author之间的一对一关系以获取其名称。

听起来真的很简单:

// 初始查询以获取所有文章
$articles = Article::all();

foreach ($articles as $article)
{
    // 获取作者对象以便于打印作者名字
    echo $article->title . ' by ' . $article->author->name;
}

我们陷入了陷阱。

此循环生成1个初始查询以获取所有文章:

译者注: 原文似乎有表达错误,应该是 「此函数生成一个初始查询以获取所有文章」而不是loop(循环)

SELECT * FROM articles;

然后 N 个查询来获得文章的作者以便打印作者的「名字」字段。如果作者名字是一样的也是如此。

SELECT * FROM author WHERE id = [articles.author_id]

恰好 N+1 个查询。

看起来好像没有这么重要的问题。 十五或二十个问题可能看起来不是一个需要立即解决的问题。 请仔细阅读本文的第一部分:

  • 数据库是所有进程共享的资源。
  • 数据库计算机资源有限,或者如果使用托管服务,则更多的数据库负载可能意味着更多的成本。
  • 如果您的数据库位于单独的计算机上,则所有数据都需要以额外的网络延迟进行传输。

[解决方案]使用预加载

Laravel documentation 所述, 我们很容易陷入 N + 1 的查询问题, 因为在访问Eloquent关联作为属性时 ($article->author), 关联数据为「延迟加载」. 这意味着关联数据在你第一次访问该属性的时候,才会真正的加载

然而,我们可以用一种简单的方法来加载所有关联数据,所以,当你以属性的方式访问Eloquent关联时,它不会运行新的查询,因为ORM已经加载了该数据。

这种策略称之为「预加载」,所有的ORM都支持此策略

// 作者使用「with」进行预加载.
$articles = Article::with('author')->get();

foreach ($articles as $article)
{
    // 作者不会在每次迭代中运行查询。
    echo $article->author->name;
}

Eloquent提供了with()方法来进行预加载关联。

在这种情况下,只执行两个查询。

首先需要加载所有的文章:

SELECT * FROM articles;

第二种是通过with()方法,它将查询所有的作者:

SELECT * FROM authors WHERE id IN (1, 2, 3, 4, ...);

Eloquent将在内部映射数据以照常使用:

$article->author->name;

优化查询语句

长期以来,我一直认为在select查询中显式声明字段数并不会带来明显的性能提升,因此我利用了仅获取查询所有字段的简单性。

此外,对特定select的字段列表进行硬编码,这不是一个容易维护的代码语句。

这种说法背后的最大错误是从数据库角度来看这可能是正确的。

但是我们使用的是ORM,因此将从数据库中选择的数据加载到PHP端的内存中,由ORM进行管理。我们获取的字段越多,该过程将占用的内存越多。

Laravel Eloquent提供了select方法来将查询限制为仅我们需要的列:

$articles = Article::query()
    ->select('id', 'title', 'content') // 只获取你需要的字段
    ->latest()
    ->get();

排除字段PHP不必处理此数据,因此可以显着减少内存消耗。

不选择所有内容还可以改善排序,分组和连接的性能,因为数据库可以以这种方式节省内存。

使用 MySQL 视图

视图是在其他表的顶部生成并存储在数据库中的select查询。

在执行 SELECT 查询的时候,Laravel 框架会将您的查询转换为 SQL 查询,当确认没有错误的时候再执行它。

Mysql 视图是一个预编译过的 SQL 查询,mysql 可以直接运行该查询。

在数据筛选方面,使用 mysql 查询的效率要比 php 更高。

更多 Mysql 的操作请查看: www.mysqltutorial.org/

在 Eloquent Model 中添加 Mysql 视图。

Mysql 视图是一个虚拟的表,但是 Eloquent ORM 会以普通表的形式处理他。

这就是为什么我们可以通过 Eloquent ORM 直接操作他。

class ArticleStats extends Model
{
    /**
     * Mysql 视图名称
     */
    protected $table = "article_stats_view";

    /**
     * 如果视图结果中存在 "author_id" 字段
     * 我们可以通过它直接找到 Auth 的数据关联。
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

表关联,分页查询都可以像普通 Eloquent ORM 一样操作,并没有什么不同。

总结

先到这里,希望以上文章可以给您的产品开发带来方便或者启发。

我曾经使用 Eloquent ORM 编写过的事例代码,其中的一些 Eloquent ORM 实现方式,同样适用于您的代码。

常言道,工欲善其事,必先利其器。

感谢您的阅读,如果想要了解更多 Inspector信息,请访问 https://www.inspector.dev.

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/inspector/make-your-appli...

译文地址:https://learnku.com/laravel/t/44270

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!