Laravel WhereHas() 和 With()
快速回忆:
当将Eloquent 关系作为属性访问时,相关模型会被“延迟加载”。这意味着关系数据直到您第一次访问该属性时才会真正加载。但是,Eloquent 可以在您查询父模型时“预先加载”关系。预先加载可以缓解“N+1”查询问题。为了说明 N+1 查询问题,请考虑一个“属于” Author 模型的 Book 模型:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
public function author()
{
return $this->belongsTo(Author::class);
}
}
我们可以使用预先加载来将此操作简化为两个查询。构建查询时,可以使用with()方法指定哪些关系需要预先加载:
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
查询结果:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, …)
检索模型记录时,您可能希望根据关系的存在来限制结果。例如,假设您想检索所有书名以 PHP 开头的作者。为此,您可以将关系名称传递给whereHas()方法,并在 has 查询中定义额外的查询约束。
$authors = Author::whereHas('books', function (Builder $query) {
$query->where('title', 'like', 'PHP%');
})->get();
查询结果:
select * from authors WHERE EXISTS(SELECT * FROM authors WHERE authors.id = books.author_id and books.title like 'PHP%');
执行此查询后,您会获得所有至少有一本书以 PHP 开头的作者,对吗?是的。
现在,如果您循环遍历作者并访问书籍关系,如下所示:
foreach ($authors as $author) {
echo $author->book->title;
}
您最终将得到N+1,为了解决这个问题,您肯定会使用with()方法来预先加载书籍:
$authors = Author::with('books')
->whereHas('books', function (Builder $query) {
$query->where('title', 'like', 'PHP%');
})
->get();
查询结果:
select * from authors WHERE EXISTS(SELECT * FROM authors WHERE authors.id = books.author_id and books.title like 'PHP%');
select * from books where `books`.`author_id` in (1, 5, 11, 22, 46, 62, ….)
到目前为止一切顺利,我们解决了 N+1 问题,但是等等,你注意到了吗?我注意到了,第二个查询只返回了第一个查询中所选作者的所有书籍,对吧?是的。
第一个查询完成了它的任务,只返回了书籍以 PHP 开头的作者,但第二个查询(预先加载)会返回每个作者的所有书籍,这意味着如果我循环遍历作者并调用 book relationship,我们不仅会看到以 PHP 开头的书籍,还会看到其他书籍。
[
App\Author : {
id: 1
name: "author 1",
…,
books: [
App\Books: {
….
title: 'PHP'
},
App\Books: {
….
title: 'Java'
},
App\Books: {
….
title: 'How to use'
},
…
]
}
…
]
为了从 whereHas 中获得我们想要的相同结果,我们需要在with()方法中使用相同的条件查询。
$authors = Author::with(['books' => fn($query) => $query->where('title', 'like', 'PHP%')])
->whereHas('books', fn ($query) =>
$query->where('title', 'like', 'PHP%')
)
->get();
查询结果:
select * from authors WHERE EXISTS(SELECT * FROM authors WHERE authors.id = books.author_id and books.title like 'PHP%');
select * from books where `books`.`author_id` in (1, 5, 11, 22, 25, 27, 35, 39, 46, 62, ….) and books.title like 'PHP%');
我想你已经注意到了,第二个查询的条件和第一个查询相同。
现在,我们想要的结果如下:
[
App\Author : {
id: 1
name: "author 1",
…,
books: [
App\Books: {
….
title: 'PHP'
},
…
]
},
App\Author : {
id: 2
name: "author 2",
…,
books: [
App\Books: {
….
title: 'PHP'
},
App\Books: {
….
title: 'PHP Laravel'
},
…
]
}
…
]
- - - - - - 最后,在所有地方执行此查询并重复相同的条件很麻烦,因此我们将在Author模型
中使用本地范围
public function scopeWithWhereHas($query, $relation, $constraint){
return $query->whereHas($relation, $constraint)
->with([$relation => $constraint]);
}
现在,通过这样的调用方式,我们的代码变得更加简洁:
Author::withWhereHas('books', fn($query) =>
$query->where('title', 'like', 'PHP%')
)->get();
查询结果:
select * from authors WHERE EXISTS(SELECT * FROM authors WHERE authors.id = books.author_id and books.title like 'PHP%');
select * from books where `books`.`author_id` in (1, 5, 11, 22, 25, 27, 35, 39, 46, 62, ….) and books.title like 'PHP%');
由于许多模型可能需要此查询生成器,因此我们将为其创建一个宏,以便所有模型都具有该范围,您只需在 * AppServiceProvider::boot() *下添加此代码片段
use Illuminate\Database\Eloquent\Builder;
Builder::macro('withWhereHas', fn($relation, $constraint) =>
$this->whereHas($relation, $constraint)->with([$relation => $constraint]);
);
好了,这个话题到此结束,希望大家已经理解了。如果你有任何类似的想法或观点,欢迎分享。
谢谢。