Laravel WhereHas() 和 With()

2025-06-10

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);
 }
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用预先加载来将此操作简化为两个查询。构建查询时,可以使用with()方法指定哪些关系需要预先加载:

$books = Book::with('author')->get();
foreach ($books as $book) {
 echo $book->author->name;
}
Enter fullscreen mode Exit fullscreen mode
查询结果:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, …)
Enter fullscreen mode Exit fullscreen mode

检索模型记录时,您可能希望根据关系的存在来限制结果。例如,假设您想检索所有书名以 PHP 开头的作者。为此,您可以将关系名称传递给whereHas()方法,并在 has 查询中定义额外的查询约束。

$authors = Author::whereHas('books', function (Builder $query) {
 $query->where('title', 'like', 'PHP%');
})->get();
Enter fullscreen mode Exit fullscreen mode
查询结果:
select * from authors WHERE EXISTS(SELECT * FROM authors WHERE authors.id = books.author_id and books.title like 'PHP%');
Enter fullscreen mode Exit fullscreen mode

执行此查询后,您会获得所有至少有一本书以 PHP 开头的作者,对吗?是的。
现在,如果您循环遍历作者并访问书籍关系,如下所示:

foreach ($authors as $author) {
 echo $author->book->title;
}
Enter fullscreen mode Exit fullscreen mode

您最终将得到N+1,为了解决这个问题,您肯定会使用with()方法来预先加载书籍:

$authors = Author::with('books')
 ->whereHas('books', function (Builder $query) {
 $query->where('title', 'like', 'PHP%');
 })
 ->get();
 
Enter fullscreen mode Exit fullscreen mode
查询结果:
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, ….)
Enter fullscreen mode Exit fullscreen mode

到目前为止一切顺利,我们解决了 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'
 },
 …
 ]
 }
 …
]
Enter fullscreen mode Exit fullscreen mode

为了从 whereHas 中获得我们想要的相同结果,我们需要在with()方法中使用相同的条件查询。

$authors = Author::with(['books' => fn($query) => $query->where('title', 'like', 'PHP%')])
 ->whereHas('books', fn ($query) => 
 $query->where('title', 'like', 'PHP%')
 )
 ->get();
Enter fullscreen mode Exit fullscreen mode
查询结果:
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%');
Enter fullscreen mode Exit fullscreen mode

我想你已经注意到了,第二个查询的条件和第一个查询相同。
现在,我们想要的结果如下:

[
 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'
 },
 …
 ]
 }
 …
]
Enter fullscreen mode Exit fullscreen mode

 - - - - - - 最后,在所有地方执行此查询并重复相同的条件很麻烦,因此我们将在Author模型
中使用本地范围

public function scopeWithWhereHas($query, $relation, $constraint){
 return $query->whereHas($relation, $constraint)
 ->with([$relation => $constraint]);
}
Enter fullscreen mode Exit fullscreen mode

现在,通过这样的调用方式,我们的代码变得更加简洁:

Author::withWhereHas('books', fn($query) =>
 $query->where('title', 'like', 'PHP%')
)->get();
Enter fullscreen mode Exit fullscreen mode
查询结果:
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%');
Enter fullscreen mode Exit fullscreen mode

由于许多模型可能需要此查询生成器,因此我们将为其创建一个宏,以便所有模型都具有该范围,您只需在 * AppServiceProvider::boot() *下添加此代码片段

use Illuminate\Database\Eloquent\Builder;

Builder::macro('withWhereHas', fn($relation, $constraint) =>
 $this->whereHas($relation, $constraint)->with([$relation => $constraint]);
);
Enter fullscreen mode Exit fullscreen mode

好了,这个话题到此结束,希望大家已经理解了。如果你有任何类似的想法或观点,欢迎分享。
谢谢。

鏂囩珷鏉ユ簮锛�https://dev.to/othmane_nemli/laravel-wherehas-and-with-550o
PREV
Google Chrome 的新更新终于允许您组织和分组标签!
NEXT
React Query - 一个实用的例子。欢迎来到 movies-react-query 👋 欢迎来到 movies-react-query 👋