Laravel Eloquent如何使用查询作用域_可复用的查询逻辑封装

28次阅读

Laravel Eloquent查询作用域通过本地和全局作用域封装复用查询逻辑,提升代码可读性、维护性和安全性,支持链式调用、条件组合及关联查询,是构建清晰、高效数据访问层的核心工具。

Laravel Eloquent如何使用查询作用域_可复用的查询逻辑封装

Laravel Eloquent 查询作用域提供了一种极其优雅且高效的方式,将模型中常用的查询约束封装起来,实现逻辑的复用和代码的清晰化。它本质上就是把那些反复出现的 whereorWhere 等条件,抽象成一个可调用的方法,让你的查询语句读起来更像自然语言,大大提升了代码的可读性和可维护性。在我看来,掌握查询作用域是写出高质量 Laravel 应用的关键一步。

解决方案

使用 Eloquent 查询作用域主要分为两种:本地作用域(Local Scopes)和全局作用域(Global Scopes)。

本地作用域 (Local Scopes)

本地作用域是最常用的一种。它允许你为模型定义一系列可复用的查询约束,并在需要时显式地调用它们。

定义本地作用域: 在 Eloquent 模型中,定义一个本地作用域的方法名必须以 scope 开头,后面跟着作用域的名称(驼峰命名)。这个方法会接收一个 IlluminateDatabaseEloquentBuilder 实例作为第一个参数。

// app/Models/User.php  namespace AppModels;  use IlluminateDatabaseEloquentFactoriesHasFactory; use IlluminateFoundationAuthUser as Authenticatable; use IlluminateNotificationsNotifiable;  class User extends Authenticatable {     use HasFactory, Notifiable;      // ... 其他属性和方法 ...      /**      * 查询所有活跃用户。      */     public function scopeActive($query)     {         return $query->where('is_active', true);     }      /**      * 查询指定角色的用户。      */     public function scopeRole($query, $role)     {         return $query->where('role', $role);     }      /**      * 查询指定创建时间范围内的用户。      */     public function scopeCreatedBetween($query, $from, $to)     {         return $query->whereBetween('created_at', [$from, $to]);     } }

使用本地作用域: 一旦定义了本地作用域,你就可以像调用模型上的普通方法一样调用它,但要省略 scope 前缀,并且作用域方法会自动接收 Builder 实例,你只需传递你定义的额外参数。

// 获取所有活跃用户 $activeUsers = User::active()->get();  // 获取所有管理员用户 $admins = User::role('admin')->get();  // 获取在特定日期范围内创建的活跃编辑 $editors = User::active()                 ->role('editor')                 ->createdBetween('2023-01-01', '2023-12-31')                 ->get();  // 作用域可以链式调用,非常灵活 $recentActiveAdmins = User::active()->role('admin')->latest()->get();

全局作用域 (Global Scopes)

全局作用域允许你为模型添加一个始终适用的查询约束。这意味着,无论你对该模型执行任何查询,这个约束都会自动应用,除非你明确地将其移除。这对于像“软删除”或多租户应用中过滤租户ID等场景非常有用。

定义全局作用域: 全局作用域通常定义为一个单独的类,该类实现 IlluminateDatabaseEloquentScope 接口。这个接口要求你实现一个 apply 方法。

// app/Scopes/ActiveUserScope.php  namespace AppScopes;  use IlluminateDatabaseEloquentBuilder; use IlluminateDatabaseEloquentModel; use IlluminateDatabaseEloquentScope;  class ActiveUserScope implements Scope {     /**      * 将作用域应用于给定的 Eloquent 查询构建器。      *      * @param  IlluminateDatabaseEloquentBuilder  $builder      * @param  IlluminateDatabaseEloquentModel  $model      * @return void      */     public function apply(Builder $builder, Model $model)     {         $builder->where('is_active', true);     } }

应用全局作用域: 在模型的 boot 方法中注册全局作用域。

// app/Models/User.php  namespace AppModels;  use AppScopesActiveUserScope; // 引入全局作用域类 use IlluminateDatabaseEloquentFactoriesHasFactory; use IlluminateFoundationAuthUser as Authenticatable; use IlluminateNotificationsNotifiable;  class User extends Authenticatable {     use HasFactory, Notifiable;      // ... 其他属性和方法 ...      /**      * 模型“启动”时执行的任何引导逻辑。      *      * @return void      */     protected static function boot()     {         parent::boot();          static::addGlobalScope(new ActiveUserScope);     } }

现在,任何对 User 模型的查询都会自动包含 orWhere0 条件:

// 这会获取所有 is_active 为 true 的用户 $users = User::all();  // 这也会获取所有 is_active 为 true 的用户,即使你没有显式调用 active() $admins = User::where('role', 'admin')->get();

移除全局作用域: 如果你需要临时移除一个或所有全局作用域,可以使用 orWhere1 或 orWhere2 方法。

// 获取所有用户,包括 is_active 为 false 的用户 $allUsers = User::withoutGlobalScope(ActiveUserScope::class)->get();  // 移除所有全局作用域 $allUsersIncludingInactiveAndDeleted = User::withoutGlobalScopes()->get();

为什么我们需要查询作用域?(以及它解决了哪些痛点)

说实话,我个人觉得查询作用域是 Laravel 在处理数据查询方面最亮眼的设计之一。刚开始写项目的时候,我常常会在控制器或者服务层里堆砌一长串 where 条件,比如 orWhere4。代码一多,这种重复简直让人抓狂,而且非常难以阅读和维护。

查询作用域完美地解决了这些痛点:

  • 避免代码重复 (DRY原则): 最直接的好处就是消除了重复的查询逻辑。比如,你可能在应用的多个地方都需要获取“活跃用户”,如果没有作用域,你每次都要写 orWhere5。有了作用域,只需 orWhere6,简洁明了。
  • 提升可读性: 想象一下,orWhere7 这样的代码,是不是比一堆 where 条件清晰得多?它让查询语句更接近业务逻辑的描述,提高了代码的“自文档化”能力。
  • 简化维护: 如果某个通用查询条件需要修改(比如“活跃”的定义从 orWhere9 变成了 scope0),你只需要修改作用域定义中的一处,而不是散落在各处的几十行代码。这大大降低了维护成本和引入bug的风险。
  • 集中业务逻辑: 它将与模型相关的查询逻辑封装在模型内部,符合“单一职责原则”。模型不再仅仅是数据容器,它也包含了如何查询自身数据的智能。这让你的控制器或服务层更专注于业务流程,而不是数据获取的细节。
  • 更强大的组合能力: 作用域可以像乐高积木一样自由组合和链式调用。你可以根据不同的业务需求,灵活地拼接出复杂的查询,而无需重新编写底层条件。

我记得有一次,一个老项目需要紧急调整“产品上架”的逻辑,涉及到多个模块。如果不是因为之前使用了查询作用域封装了 scope1,那次改动肯定会是一场灾难,需要逐个文件去查找和修改。所以,我认为它不仅仅是“方便”,更是一种“风险管理”的工具

全局作用域与本地作用域:我该如何选择?

在选择使用全局作用域还是本地作用域时,我通常会问自己一个核心问题:这个查询条件是不是几乎所有对这个模型的查询都需要?

  • 本地作用域:适用大多数场景,明确的、有选择性的约束。

    • 何时使用: 当一个查询约束只在特定场景下需要应用时。这是最常见的情况。例如,获取“已发布”的文章、获取“某用户”的订单、获取“特定类型”的产品等。
    • 特点:
      • 显式调用: 你必须手动调用它,例如 scope2。
      • 可传参数: 可以接受参数,实现更灵活的过滤,例如 scope3。
      • 链式调用: 可以与其他作用域或查询方法自由组合。
    • 我的思考: 我倾向于尽可能多地使用本地作用域。它让查询行为更加透明和可控。当你看到 scope4 时,你就知道它会过滤掉未发布的文章。这种明确性在代码阅读和调试时非常重要。如果我不确定某个条件是否总是需要,我通常会先定义成本地作用域。
  • 全局作用域:适用于模型层面的、普遍性的、默认的约束。

    • 何时使用: 当一个查询约束是该模型的默认行为,并且几乎所有对该模型的查询都应该包含它时。最典型的例子就是 Laravel 自带的 scope5 特性,它会自动过滤掉已软删除的记录。另一个常见场景是多租户应用,你可能希望所有对 scope6 模型的查询都默认加上 scope7。
    • 特点:
      • 自动应用: 注册后,它会自动应用到所有对该模型的查询上,无需手动调用。
      • 隐式行为: 它的存在可能不那么显眼,需要开发者了解模型的内部机制。
      • 可移除: 可以使用 scope8 或 scope9 临时移除。
    • 我的思考: 使用全局作用域要非常谨慎。虽然它能带来极大的便利,但其隐式性也可能导致一些“惊喜”。比如,如果你忘记了某个模型有全局作用域,可能会在某些特殊查询中得到不完整的数据。因此,我只会在那些“除非明确要求,否则永远不应该看到”的数据场景中使用它。例如,在一个多租户系统中,如果用户能看到其他租户的数据是一个严重的安全漏洞,那么全局作用域就是非常合适的。但在其他场景,我更喜欢本地作用域带来的清晰和可控。

总的来说,本地作用域是你的首选,它提供了灵活性和透明度。全局作用域则是一个强大的工具,用于强制执行模型层面的默认行为,但使用时需要权衡其隐式性带来的潜在影响。

查询作用域在复杂业务逻辑中的进阶应用

在面对复杂的业务逻辑时,查询作用域的威力远不止于简单的 where 条件封装。它能够与其他 Eloquent 特性以及一些编程技巧结合,构建出非常强大且可维护的查询系统。

1. 链式调用与组合:构建复杂的动态查询

Laravel Eloquent如何使用查询作用域_可复用的查询逻辑封装

蓝心千询

蓝心千询是vivo推出的一个多功能AI智能助手

Laravel Eloquent如何使用查询作用域_可复用的查询逻辑封装34

查看详情 Laravel Eloquent如何使用查询作用域_可复用的查询逻辑封装

查询作用域最大的魅力之一就是它的链式调用能力。你可以根据请求参数或业务状态,动态地组合多个作用域。

// 假设有一个商品列表,需要根据多种条件过滤 public function index(Request $request) {     $products = Product::query();      // 根据分类ID过滤 (本地作用域)     if ($request->has('category_id')) {         $products->byCategory($request->category_id);     }      // 根据状态过滤 (本地作用域)     if ($request->has('status')) {         $products->status($request->status);     }      // 根据价格范围过滤 (本地作用域)     if ($request->has('min_price') && $request->has('max_price')) {         $products->priceRange($request->min_price, $request->max_price);     }      // 默认只显示有库存的 (全局作用域,或另一个本地作用域)     // 假设 Product 模型有一个 scopeInStock()     $products->inStock();      // 排序     $products->orderBy('created_at', 'desc');      return $products->paginate(15); }

通过这种方式,控制器逻辑变得非常清晰,它只负责判断哪些条件需要应用,具体的过滤逻辑则封装在模型的作用域中。

2. 结合 IlluminateDatabaseEloquentBuilder1 方法实现条件性作用域

Laravel 的 IlluminateDatabaseEloquentBuilder1 方法与查询作用域简直是天作之合,它允许你根据一个布尔条件来应用查询构建器上的操作。这对于处理可选的过滤条件非常有用,避免了大量的 IlluminateDatabaseEloquentBuilder3 语句。

// app/Models/Post.php class Post extends Model {     // ...     public function scopePublished($query) { return $query->where('is_published', true); }     public function scopeFeatured($query) { return $query->where('is_featured', true); }     public function scopeOfType($query, $type) { return $query->where('type', $type); } }  // 在控制器或服务层中 public function getFilteredPosts(Request $request) {     $posts = Post::query()         ->when($request->has('type'), function ($query) use ($request) {             // 如果请求中包含 'type' 参数,则应用 ofType 作用域             $query->ofType($request->type);         })         ->when($request->boolean('show_featured'), function ($query) {             // 如果请求中 'show_featured' 为 true,则应用 featured 作用域             $query->featured();         })         ->when($request->boolean('show_published', true), function ($query) {             // 默认显示已发布的,除非明确指定不显示             $query->published();         })         ->get();      return $posts; }

这种模式让动态查询的构建变得非常流畅和易读,我发现它能把很多原本需要复杂条件判断的逻辑简化成优雅的链式调用。

3. 作用域与关联关系 (Relationships) 的结合

查询作用域不仅可以应用于模型本身,也可以在查询关联模型时发挥作用。

  • 查询拥有特定关联特征的父模型:

    // 获取所有拥有至少一篇已发布文章的用户 $usersWithPublishedPosts = User::whereHas('posts', function ($query) {     $query->published(); // 在关联查询中应用 Post 模型的 published 作用域 })->get();
  • 加载关联模型时应用作用域:

    // 获取所有用户,并加载他们已发布的文章 $users = User::with(['posts' => function ($query) {     $query->published(); // 在加载关联时过滤文章 }])->get();

    这在需要有条件地预加载关联数据时非常有用,避免加载不必要的数据。

4. 结合仓库模式 (Repository Pattern) 或服务层 (Service Layer)

在大型应用中,我们经常使用仓库模式或服务层来抽象数据访问。查询作用域在这里可以作为构建查询的“积木”,让仓库方法更加简洁和专注于业务意图。

// app/Repositories/PostRepository.php class PostRepository {     public function getPublishedPostsByType(string $type)     {         return Post::published()->ofType($type)->get();     }      public function getFeaturedPostsForAdmin()     {         // 假设有一个全局作用域用于管理员可见的帖子,或者在这里显式调用一个本地作用域         return Post::featured()->get();     } }  // 在服务层或控制器中 $posts = $postRepository->getPublishedPostsByType('article');

通过这种方式,查询作用域将底层的 where 逻辑隐藏在模型内部,而仓库或服务层则通过调用这些语义化的作用域来构建查询,进一步提升了整个应用架构的清晰度。

在我看来,查询作用域是 Eloquent 提供的一个核心工具,它鼓励我们以更声明式、更具表现力的方式来编写数据查询。掌握这些进阶用法,能够显著提升代码质量和开发效率。

以上就是Laravel Eloquent如何使用查询php laravel go app 工具 数据访问 作用域 代码可读性 为什么 red laravel 架构 if 封装 接口 作用域 database bug

php laravel go app 工具 数据访问 作用域 代码可读性 为什么 red laravel 架构 if 封装 接口 作用域 database bug

text=ZqhQzanResources