在Laravel开发中,关联子表数据筛选是后台列表、数据导出等场景的高频需求,错误的实现方式很容易导致N+1查询问题,影响系统性能。下面我们通过实际案例来学习with闭包和whereHas的正确用法。

一、基础场景准备
我们先假设有两个常见的关联模型,用户模型User和文章模型Post,一个用户可以有多篇文章,文章表有status字段标识发布状态,1为已发布,0为未发布。
模型关联定义如下:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
// 定义用户与文章的关联,一个用户有多篇文章
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
// 定义文章与用户的关联,一篇文章属于一个用户
public function user()
{
return $this->belongsTo(User::class);
}
}二、whereHas:筛选主表时过滤关联子表条件
如果我们想要查询「有已发布文章的用户」,也就是主表是用户,筛选条件是关联的文章表满足status=1,这时候就需要用到whereHas方法。
whereHas的作用是在查询主表时,加入对关联表的exists子查询,只返回满足关联条件的主表记录,不会默认加载关联数据。
基础用法示例
<?php
use App\Models\User;
// 查询有已发布文章的用户
$users = User::whereHas('posts', function ($query) {
// 闭包内筛选关联文章的条件
$query->where('status', 1);
})->get();
// 如果同时需要加载这些用户的已发布文章,可以结合with使用
$usersWithPosts = User::whereHas('posts', function ($query) {
$query->where('status', 1);
})->with('posts')->get();多条件筛选示例
如果需要更复杂的关联筛选,比如查询「有2024年之后发布的已发布文章的用户」,可以在闭包内添加多个条件:
<?php
use App\Models\User;
$users = User::whereHas('posts', function ($query) {
$query->where('status', 1)
->where('created_at', '>', '2024-01-01');
})->get();三、with闭包:加载关联时过滤子表数据
如果我们已经查询出了主表数据,想要在加载关联关系的时候只加载满足特定条件的子表数据,而不是加载所有关联数据,这时候就需要用到with方法的闭包形式。
比如我们查询所有用户,但是每个用户只加载他的已发布文章,未发布的文章不加载,就可以这样写:
<?php
use App\Models\User;
// 查询所有用户,同时只加载每个用户的已发布文章
$users = User::with(['posts' => function ($query) {
$query->where('status', 1);
}])->get();
// 遍历的时候,每个用户的posts集合里只有已发布的文章
foreach ($users as $user) {
echo $user->name . '的已发布文章数量:' . $user->posts->count();
}with闭包中指定字段
如果只需要关联文章的个别字段,还可以在闭包内使用select方法指定,减少数据传输量:
<?php
use App\Models\User;
$users = User::with(['posts' => function ($query) {
$query->where('status', 1)
->select('id', 'title', 'user_id'); // 必须包含关联的外键user_id,否则关联无法匹配
}])->get();四、两者的区别与使用场景
很多开发者容易混淆这两个方法的使用场景,我们可以通过下面的对比表来明确区别:
| 方法 | 作用 | 是否过滤主表 | 是否默认加载关联 | 适用场景 |
|---|---|---|---|---|
whereHas | 筛选满足关联条件的主表记录 | 是 | 否 | 需要筛选出符合关联条件的主表数据时使用 |
with闭包 | 加载关联时过滤子表数据 | 否 | 是 | 需要加载主表数据,同时只加载符合条件的关联子表数据时使用 |
五、常见错误与注意事项
- 不要在
with闭包里忘记加关联的外键字段,否则关联数据无法正确匹配,会出现关联数据为空的情况。 whereHas如果不结合with使用,后续访问关联数据会触发额外的查询,容易产生N+1问题,需要同时加载关联的话要两者结合。- 如果关联筛选条件比较复杂,建议把闭包内的逻辑封装成模型的作用域,提高代码复用性。
六、作用域封装示例
我们可以把常用的关联筛选条件封装成模型作用域,方便后续复用:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
// 封装作用域:筛选有已发布文章的用户
public function scopeHasPublishedPosts(Builder $query)
{
return $query->whereHas('posts', function ($q) {
$q->where('status', 1);
});
}
// 封装作用域:加载已发布的文章
public function scopeWithPublishedPosts(Builder $query)
{
return $query->with(['posts' => function ($q) {
$q->where('status', 1);
}]);
}
}
// 使用时更简洁
$users = User::hasPublishedPosts()->withPublishedPosts()->get();通过合理使用with闭包和whereHas,我们可以高效地完成Laravel中关联子表数据的筛选,避免性能问题,同时让代码更简洁易维护。