广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Laravel模型如何定义查询作用域?局部作用域与全局作用域实用指南

Laravel 的 Eloquent ORM 提供了一种把常用查询条件封装为模型方法的机制,也就是查询作用域。通过定义作用域,开发者可以把散落在控制器、服务类中的 where 条件统一管理起来,这样不仅减少了代码重复,也更容易表达查询的业务含义。查询作用域的核心思想是接收当前查询构建器实例,在其上添加过滤条件后返回,使调用方可以继续链式调用其他查询方法。

Laravel模型如何定义查询作用域?局部作用域与全局作用域实用指南

Laravel 将查询作用域分为局部作用域和全局作用域两大类。局部作用域需要显式调用,适合按需组合的过滤条件;全局作用域则会在模型的所有查询中自动生效,适合软删除、多租户隔离这类强制约束。下面分别进行说明。

局部作用域:封装可复用的过滤逻辑

局部作用域的命名规则非常直观:在 Eloquent 模型里定义一个以 scope 开头、后面跟着驼峰命名的方法,比如 scopePublished。该方法至少接收一个参数,即 Laravel 自动注入的查询构建器实例。你可以在方法体内调用 where、orderBy、whereHas 等查询条件,最后返回构建器实例即可。调用时省略 scope 前缀,例如 Article::published() 会触发 scopePublished 方法。

下面是一个 Article 模型的示例,它定义了已发布和近期发布两个局部作用域。第一个作用域不接收额外参数,第二个作用域接收一个天数参数并设置了默认值。

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }
    public function scopeRecent($query, $days = 7)
    {
        return $query->where('published_at', '>=', now()->subDays($days));
    }
}

完成定义后,可以像调用查询构建器方法一样使用它们。例如 Article::published()->recent(30)->orderBy('published_at', 'desc')->get() 会先过滤已发布文章,再限定发布时间在最近 30 天内,最后排序并取出结果。局部作用域返回的是 Builder 实例,所以能够继续链式调用其它查询方法。这种方式让控制器中的查询语句变得简洁,业务约束也集中在模型层,方便统一调整。

局部作用域也可以包含条件判断。比如希望某些作用域只在传入特定条件时才生效,可以在方法里使用 if 判断,最后确保始终返回查询构建器。这样可以避免调用方在外部写大量三元表达式,提高代码可读性。

全局作用域:让约束默认作用于所有查询

全局作用域与局部作用域最大的区别在于它不需要手动调用,模型上的每一个查询都会自动应用该作用域。实现全局作用域最规范的方式是创建一个独立类,让这个类实现 Scope 接口。该接口要求实现 apply 方法,方法中接收 Builder 和 Model 两个参数,然后为查询构建器添加约束。

以多租户项目为例,假设所有项目数据都需要按当前登录用户所属租户过滤,可以创建 TenantScope 类。代码如下:

<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('tenant_id', auth()->user()->tenant_id);
    }
}

接着在目标模型的 booted 方法中注册这个作用域。模型示例:

<?php
namespace App\Models;
use App\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new TenantScope());
    }
}

注册完成后,Project::all()、Project::find(1) 甚至关联查询都会自动附加 tenant_id 过滤条件。这种机制能有效防止开发者遗漏租户隔离逻辑。需要注意的是,全局作用域对模型所有查询都生效,因此作用域内部应尽量保持简单和确定,避免出现依赖外部状态而导致查询结果不符合预期的情况。

Laravel 也支持使用匿名闭包来注册全局作用域,适合作用域逻辑非常短小且不需要复用类的场景。写法如下:

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            $builder->where('tenant_id', auth()->id());
        });
    }
}

如果临时需要绕过全局作用域,可以使用 withoutGlobalScope 指定要移除的作用域类名,或者使用 withoutGlobalScopes 移除全部全局作用域。例如后台数据审计需要查看所有租户的项目,可以这样写:Project::withoutGlobalScope(TenantScope::class)->get()。合理使用移除方式能在灵活性和安全性之间找到平衡。

动态作用域与组合使用场景

局部作用域的一大优势是支持任意数量的参数,这让同一个作用域能适应不同的查询条件。例如订单模型可以定义一个 scopeStatus 来按状态过滤,再定义一个 scopePaidBetween 来限定支付时间区间。这样不同业务模块只需要传入各自关心的参数,而不必重复编写 where 条件。

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
    public function scopeStatus($query, $status)
    {
        return $query->where('status', $status);
    }
    public function scopePaidBetween($query, $start, $end)
    {
        return $query->where('paid_at', '>=', $start)
                     ->where('paid_at', '<=', $end);
    }
}

调用时可以用 Order::status('shipped')->paidBetween(now()->subMonth(), now())->get() 组合多个作用域。组合后的 SQL 会按照作用域调用顺序叠加 where 条件,但执行计划取决于数据库索引和查询优化器。对于大数据量场景,建议使用 toSql 方法检查最终 SQL,确认条件顺序和参数绑定是否符合预期。

除了简单 where 条件,作用域内部还可以使用 whereHas、withCount 等查询构建器能力。比如定义一个 scopeHasComments 来筛选至少有一条评论的文章,能够把关联查询逻辑封装起来。闭包型全局作用域也可以捕获外部变量实现一定程度的动态化,但类形式的全局作用域可以通过构造函数传递配置,更利于单元测试和多场景复用。

常见误区与性能排查建议

在定义作用域时最容易出现的问题是方法内部的返回值不统一。例如某些分支返回了 Builder 实例,另一些分支却直接返回模型集合或布尔值,这会导致后续链式调用失败。务必保证所有代码路径都返回查询构建器实例,哪怕没有添加任何条件,也要返回传入的 $query。

另一个常见的坑是全局作用域对关联查询的影响。由于全局作用域会自动附加到关联模型查询上,如果关联模型也定义了全局作用域,查询结果可能会比预期更少。此时可以通过 withoutGlobalScope 在关联闭包中临时移除作用域,或者认真梳理作用域边界,避免把非强制性的业务过滤做成全局作用域。作用域内部也尽量不要硬编码状态值,应该通过参数或配置对象传入,方便模型在不同场景下复用。

调试方面,Eloquent 提供了 toSql 和 getBindings 方法,能够查看最终 SQL 和参数绑定。例如:

$sql = Article::published()->toSql();
$bindings = Article::published()->getBindings();
dump($sql, $bindings);

这段代码会输出实际执行的 SQL 和绑定的参数,帮助判断作用域是否被正确应用。性能问题往往不是作用域本身导致的,而是缺少索引或查询数据量过大。建议在数据库层查看执行计划,必要时为过滤字段添加索引。作用域的优势在于结构清晰,但最终性能仍要结合数据库设计和查询习惯来保障。

掌握了局部作用域和全局作用域的适用场景,就能让 Eloquent 查询层保持整洁。局部作用域适合按需组合,全局作用域适合默认规则,动态参数则让作用域更加灵活。实际开发中应控制作用域的复杂度,保持单一职责,遇到复杂查询时可以组合多个小作用域,而不是把所有条件塞进一个巨型方法。

广告位 · 文章底部(后台-模块管理替换此区域)