Laravel Blade 中使用 Eloquent 模型 6 个须知

Laravel

在接触 Laravel 过程中,最常见的关于性能问题之一就是在 Blade 模板中使用 Eloquent 方法和模型关系查询,从而导致多了一些不必要的查询和循环。在这篇文章里,我将会介绍一些不同的情况下如何进行更高效的处理。


情况 1、使用 belongsTo() 加载关联关系时,最好使用预加载的方式。

大家最常见的情况就是使用 @foreach 来循环每条记录,甚至某些要展示的列还要来自于父模型中的字段值。

@foreach ($sessions as $session)
<tr>
  <td>{{ $session->created_at }}</td>
  <td>{{ $session->user->name }}</td>
</tr>
@endforeach

很明显,这个例子中 session 从属于 user。在模型 app/Session.php 中定义一个 user() 方法:

public function user()
{
    return $this->belongsTo(User::class);
}

现在的代码是没有问题的,但是在控制器中不同的代码会带来不同的性能问题。

控制器中性能低下的写法:

public function index()
{
    $sessions = Session::all();
    return view('sessions.index', compact('sessions');
}

控制器中性能高效的写法:

public function index()
{
    $sessions = Session::with('user')->get();
    return view('sessions.index', compact('sessions');
}

注意到两种写法的不同了吗?在查询的时候就提前加载关联关系,就是 预加载 .

如果你不这么做,当我们在模板中进行循环的时候,每次都要从数据库中查询一次当前 session 所属用户的信息。因此,如果表中有100条 session 记录的话,那么最终将会产生101次查询,其中1次用于查询 session 列表,另外100次用于查询每个相关的用户。

所以,千万要时刻牢记预加载的重要性。


情况 2. 加载 hasMany() 关系

另一种典型的情况是 您需要在父记录循环中列出所有子 数据。

@foreach ($posts as $post)
<tr>
  <td>{{ $post->title }}</td>
  <td>
    @foreach ($post->tags as $tag)
      <span class="tag">{{ $tag->name }}</span>
    @endforeach
  </td>
</tr>
@endforeach

猜猜看 – 在这里也同样适用.如果您不使用 “预加载” , 那么每条数据都会直接请求数据库.

所以, 在您的控制器中, 您应该这么写 :

public function index()
{
    $posts = Post::with('tags')->get(); // 不使用 Post::all()!
    return view('posts.index', compact('posts'));
}

情况 3. 在 hasMany() 的关系中不使用括号

假设您有一个带有投票的民意调查,并且想显示所有民意测验的投票数。
当然,您正在控制器中进行 预加载 :

public function index()
{
    $polls = Poll::with('votes')->get();
    return view('polls', compact('polls'));
}

然后 在 Blade 文件中 您这样写:

@foreach ($polls as $poll)
    <b>{{ $poll->question }}</b> 
    ({{ $poll->votes()->count() }})
    <br />
@endforeach

看起来没有什么问题 ,但是还是要请您注意  ->votes() 中的(), 使用()则会在每次轮询后将会有一个查询。因为它没有去获取预加载数据, 所以他将再次从Eloquent 中进行操作

所以 您应该这么写: {{ $poll->votes->count() }}. 去除掉 ().

顺便说一句,这同样适用于belongsTo关系。在Blade中加载关系时请勿使用 ()。

说句题外话: 我之前在 StackOverflow 中, 看到过更糟糕的例子。
例如 {{$ poll-> votes()-> get()-> count()}} 或者 @ foreach($ poll-> votes()-> get()as $ vote)...

请使用Laravel Debugbar
这种方式进行验证 ,并查看SQL查询的数量。


情况 4、关联模型对应记录不存在怎么办?

在 Laravel 中最常见的一个错误就是 “trying to get property of non-object”,相信你之前也一定遇到过这个错误信息。

通常情况下该错误都是类似下面这种代码导致的:

<td>{{ $payment->user->name }}</td>

原因是无法保证 payment 所属用户一定存在。可能用户已经被软删除,也可能是数据库中缺少对应的外键。

解决方法取决于你所使用 Laravel/PHP 的版本。在 Laravel 5.7 之前, 为了避免这种错误,可以设定默认值,语法如下:

{{ $payment->user->name or 'Anonymous' }}

自从 Laravel 5.7 开始, 新语法 遵循 PHP7 中引入的常见 PHP 运算符:

{{ $payment->user->name ?? 'Anonymous' }}

另外,你也可以在模型级别上设定默认值。

public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

调用 withDefault() 方法,在关联模型实例不存在时,会返回一个空模型。

不仅如此,你也可以在调用 withDefault() 方法的同时设置模型属性的默认值。

public function user()
{
    return $this->belongsTo(User::class)
      ->withDefault(['name' => 'Anonymous']);
}

情况 5、 在 Blade 模板中使用关联关系时,尽量避免使 where 语句。

你有没有在模板中见过这样的代码呢?

@foreach ($posts as $post)
    @foreach ($post->comments->where('approved', 1) as $comment)
        {{ $comment->comment_text }}
    @endforeach
@endforeach

使用 where(‘approved’, 1) 条件来过滤已经预加载的 comments 数据。

确实可以达到效果并且不会引起任何的性能问题。但是个人建议最好遵循 MVC 原则,逻辑应该在 View 外部,也就是“逻辑”层中的某个位置。比如 Eloquent 模型中,可以在 app/Post.php 中单独定义一种关联关系。

public function comments()
{
    return $this->hasMany(Comment::class);
}

public function approved_comments()
{
    return $this->hasMany(Comment::class)->where('approved', 1);
}

接下来你就可以在 Controller/Blade 中调用自定义的特殊方法。就像这样:$posts = Post::with(‘approved_comments’)->get();


方案6.避免访问器发生非常复杂的情况

最近,在一个项目中,我有一个任务:用信封图标表示消息,用任务的价格表示任务,该任务应该从包含该价格的最后一条消息中获取。听起来很复杂,确实如此。

我先是这样写的:

@foreach ($jobs as $job)
    ...
    @if ($job->messages->where('price is not null')->count())
        {{ $job->messages->where('price is not null')->sortByDesc('id')->first()->price }}
    @endif
@endforeach

当然,需要检查价格是否存在,然后获取该价格的最后一条消息,但是这个操作不应该出现在 blade 模板中。

所以我最终在 Eloquent 上使用 访问器 并在 app/Job.php 定义了以下内容:

public function getPriceAttribute()
{
    $price = $this->messages
        ->where('price is not null')
        ->sortByDesc('id')
        ->first();
    if (!$price) return 0;

    return $price->price;
}

当然,在这种复杂情况下,也很容易陷入 N + 1 查询问题,或者只是偶然地多次启动查询。因此,请使用 Laravel Debugbar 查找缺陷。

另外,我可以推荐一个名为 Laravel N+1查询检测器 的第三方包。


额外说一点。在研究此主题时,我在 Laracasts 上看到一个糟糕的代码示例。有人给出以下代码,并寻求意见。不幸的是,现在的项目中经常看到这样的代码。因为它可以正常运行...

@foreach($user->payments()->get() as $payment)
<tr>
    <td>{{$payment->type}}</td>
    <td>{{$payment->amount}}$</td>
    <td>{{$payment->created_at}}</td>
    <td>
        @if($payment->method()->first()->type == 'PayPal')
            <div><strong>Paypal: </strong> 
            {{ $payment->method()->first()->paypal_email }}</div>
        @else
            <div><strong>Card: </strong>
            {{ $payment->payment_method()->first()->card_brand }} **** **** **** 
            {{ $payment->payment_method()->first()->card_last_four }}</div>
        @endif
    </td>
</tr>
@foreach
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://laraveldaily.com/calling-eloquen...

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

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 2

谢谢分享

4年前 评论
zhanghaidi

非常感谢!学习了很多

3年前 评论

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