laravel 入门(9)Eloquent 模型

概述#

一个 Eloquent 模型类映射一张数据表,通过模型类提供的方法,你可以获取其映射的数据表的所有记录,也可以获取单条记录,还可以创建、更新和删除对应数据表记录,而这一切都不需要你编写任何 SQL 语句、或者构建查询构建器即可完成。

Eloquent 专注于简单,并且和其他框架一样遵循「约定优于配置」,从而允许你通过最少的代码构建功能强大的模型类。

模型类定义#

使用模型类之前,需要在数据库有对应的数据表,因为模型类就是数据表在面向对象编程语言中的映射。
要创建一个模型类,需要使用 make:model 命令:

Copy Highlighter-hljs
php artisan make:model Post

注:如果对应的数据表尚未创建,你还可以在创建模型类的同时创建对应的数据库迁移文件,通过 php artisan make:model Post -m 即可。如果你想将模型类创建到 app/Models 目录下,可以这么运行上述命令 php artisan make:model Models/Post。

接下来我们就是 posts 表映射的 Post 模型为例,来看看默认都有哪些约定。新生成的 Post 模型类如下:

Copy Highlighter-hljs
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { // }

里面什么东西都没有,但是我们就可以通过它完成数据表记录的增删改查操作了,怎么做到的?这就是「约定优于配置」的功劳了。下面我们就来看看这些默认的约定。

表名

Eloquent 约定模型类映射表名是将类名由驼峰格式转化为小写+下划线(含多个单词的话),最后将其转化为复数形式,比如 Post 对应表名是 posts、PostTag 对应表名是 post_tags 等等。当然,如果你不想遵循这个系统约定的规则,也可以通过手动设置模型类属性的方式进行自定义,例如:

Copy Highlighter-hljs
protected $table = 'articles';

主键

Eloquent 默认假设每张数据表都有一个整型的自增主键,其字段名为 id,如果你的数据表主键名不是 id,可以通过 $primaryKey 属性来指定:

Copy Highlighter-hljs
protected $primaryKey = 'post_id';

如果主键不是自增的,还可以设置 $incrementing 属性为 false:

Copy Highlighter-hljs
public $incrementing = false;

如果主键不是整型,还可以设置 $keyType 属性为 string:

Copy Highlighter-hljs
protected $keyType = 'string';

时间戳

Eloquent 默认约定每张表都有 created_at 和 updated_at 字段(迁移类中 $table->timestamps() 会生成这两个字段),并且在保存模型类时会自动维护这两个字段。如果你的数据表里面不包含这两个字段,或者只包含一个,都需要设置 $timestamps 属性为 false:

Copy Highlighter-hljs
public $timestamps = false;

或者通过 CREATED_AT 和 UPDATED_AT 常量来设置自定义的创建和更新时间字段:

Copy Highlighter-hljs
public const CREATED_AT = 'create_time'; public const UPDATED_AT = 'update_time';

此外,默认时间的存储格式是 Y-m-d H:i:s,你还可以通过 $dateFormat 属性来自定义时间戳的格式,该属性值通过 PHP 的 date() 函数进行解析,所以原则上支持 date 函数支持的所有语法格式,比如将时间设置为 Unix 时间戳:

Copy Highlighter-hljs
protected $dateFormat = 'U';

这样,保存到数据库的时间格式就是 Unix 时间戳了,前提是你的 created_at 和 updated_at 字段是整型,否则会报格式错误。

数据库连接

Eloquent 模型类默认约定的数据库连接是 config/database.php 中配置的默认连接,正如我们在连接配置教程中所说的那样,如果应用配置了多个数据库连接,可以通过 $connection 属性为模型类指定使用哪个连接:

Copy Highlighter-hljs
protected $connection = 'connection_name';

查询数据#

日常开发中,大部分操作都是数据库中查询数据,Eloquent 模型了为我们提供了很多方法帮助我们从数据库中获取数据。

获取所有记录

我们可以通过模型类提供的 all 方法获取一张表的所有记录:

Copy Highlighter-hljs
$posts = Post::all();

和查询构建器一样,该方法返回的也是集合,只不过是模型类集合:

要获取指定模型类的字段属性,遍历该集合即可:

Copy Highlighter-hljs
foreach ($posts as $post) { dump($post->title); }

和查询构建器一样,如果结果集很大的话,模型类也支持通过 chunk 方法分块获取查询结果:

Copy Highlighter-hljs
Post::chunk(10, function ($posts) { foreach ($posts as $post) { if ($post->views == 0) { continue; } else { dump($post->title . ':' . $post->views); } } });

除此之外,在 Eloquent 模型中还可以通过 cursor 方法每次只获取一条查询结果,从而最大限度减少内存消耗:

Copy Highlighter-hljs
foreach (Post::cursor() as $post) { dump($post->title . ':' . $post->content); }

获取指定查询结果
如果想要指定查询条件和查询字段,可以通过 where 方法和 select 方法来实现:

Copy Highlighter-hljs
$posts = Post::where('views', '>', 0)->select('id', 'title', 'content')->get();

实际上,Eloquent 模型类底层的查询也是基于查询构建器来实现的,你可以在模型类上调用所有查询构建器的 Where 查询方法,同样是以流接口的模式构建方法链调用即可。前面提到的 chunk 和 cursor 方法也适用于这种指定查询条件的查询操作。

因为是查询构建器,所以我们还可以在模型查询操作中对查询结果进行排序和分页:

Copy Highlighter-hljs
$posts = Post::where('views', '>', 0)->orderBy('id', 'desc')->offset(10)->limit(5)->get();

获取单条记录
当然,你也可以通过查询构建器的方式在模型类查询中获取单条记录:

Copy Highlighter-hljs
$user = User::where('name', '学院君')->first();

返回的结果是一个模型类实例

你可以直接通过 $user->name 这样的方式访问模型类实例的属性。

此外,如果查询的条件是主键 ID 的话,还可以将上述调用简化为通过 find 方法来实现:

Copy Highlighter-hljs
$user = User::find(1);

模型类查询结果为空会返回 null。如果你想要在单条记录返回结果为空时返回 404 响应(在控制器方法中可能需要用到类似操作),可以通过 firstOrFail 或者 findOrFail 方法在找不到对应记录时抛出 404 异常,从而简化代码编写:

Copy Highlighter-hljs
$user = User::findOrFail(111);

获取聚合结果

Eloquent 模型类同样支持 count、sum、avg、max、min 等聚合函数查询:

Copy Highlighter-hljs
$num = User::whereNotNull('email_verified_at')->count(); # 计数 $sum = User::whereNotNull('email_verified_at')->sum('id'); # 求和 $avg = User::whereNotNull('email_verified_at')->avg('id'); # 平均值 $min = User::whereNotNull('email_verified_at')->min('id'); # 最小值 $max = User::whereNotNull('email_verified_at')->max('id'); # 最大值

你会发现,如果你掌握了查询构建器,就等同于掌握了 Laravel 中的所有数据库查询操作。只不过将 DB::table 换成对应的模型类而已。

注:除获取单条记录之外,ELoquent 模型类查询返回的结果都是集合类,因此你可以在查询结果上调用集合类的所有方法,还可以自定义模型对应集合类,详情请查看对应官方文档。

插入数据#

通过 Eloquent 模型类插入记录到数据库也比较简单:

Copy Highlighter-hljs
$post = new App\Post; $post->title = '测试文章标题'; $post->content = '测试文章内容'; $post->user_id = 1; $post->save();

创建时间和更新时间字段由 Eloquent 底层自动帮我们维护(遵循默认约定的话)。执行上面的代码就会在数据库新增一条记录。

更新数据#

通过模型类更新数据表记录也很简单:

Copy Highlighter-hljs
$post = Post::find(31); $post->title = '测试文章标题更新'; $post->save();

更新时间 Eloquent 底层会自动帮我们维护,执行上面的代码即可完成该 $post 模型对应数据表记录的更新。

删除数据#

通过模型类删除对应数据表记录和更新记录类似,都要先获取对应操作模型实例,删除对应记录更简单,获取到模型实例后,直接调用其删除方法即可:

Copy Highlighter-hljs
$post = Post::find(31); $post->delete();

这样,就完成了 id = 31 对应数据表记录的删除,你还可以通过 Eloquent 提供的 destroy 方法一次删除多条记录,通过数组传递多个主键 ID 即可:

Copy Highlighter-hljs
Post::destroy([1,2,3]);

当然,你也可以通过查询构建器的方式删除指定记录:

Copy Highlighter-hljs
$user = User::where('name', '学院君')->fisrt(); $user->delete();

批量赋值#

批量赋值主要用于快速设置模型属性。

在介绍批量赋值之前,我们先看一个例子,之前我们新增或者修改 Eloquent 模型时都是通过依次设置每个属性来实现的:

Copy Highlighter-hljs
$post = new App\Post; $post->title = '测试文章标题'; $post->content = '测试文章内容'; $post->user_id = 1; $post->save();

如果模型类就那么三五个属性还好,如果是十几个甚至几十个呢?每次这么做得崩溃掉,到时候我们的控制器类里面可能会遍布这种设置代码,Laravel 号称的优雅就是打脸了。所以这个时候,批量赋值就粉墨登场了,批量赋值就是为我们解决这个问题的。

创建模型#

以创建模型实例为例,批量赋值允许我们以数组的方式将待设置属性以关联数组的方式传递构造函数:

Copy Highlighter-hljs
$post = new Post([ 'title' => '测试文章标题', 'content' => '测试文章内容' ]);

仅这么看的话,好像跟之前的写法没有什么大的优势,还是需要指定每个属性,但是这为我们提供了一个很好的基础,如果和用户请求数据结合起来使用,就能焕发它的光彩了。比如,如果我们的请求数据是一个文章发布表单提交过来的数据,包含 title、content 等字段信息,就可以通过下面这种方式进行批量赋值了:

Copy Highlighter-hljs
$post = new Post($request->all());

这样一来,不管多少字段,一条语句就搞定了全部属性的赋值。但是,细心的同学可能会发现,这里有一个安全隐患,如果用户发布的时候,包含了用户字段 user_id,并且设置的不是自己的用户 ID,而是其它用户的 ID,发布出来的文章就变成其他人发布的了;又或者文章需要审核后才能发布,但用户在表单中传递了状态字段将文章状态设置为审核通过,这样文章保存后就直接是已发布状态了。诸如此类的问题还有很多,总而言之,批量赋值给我们带来便利的同时,也给我们带来了烦恼。

作为一个成熟的 ORM 框架,Eloquent 在设计之初肯定不会没有考虑到这样的问题,实际上,我们可以借助模型类中的白名单属性或黑名单属性来解决这个困扰。

所谓白名单属性就是该属性中指定的字段才能应用批量赋值,不在白名单中的属性会被忽略;与之相对的,黑名单属性指定的字段不会应用批量赋值,不在黑名单中的属性则会应用批量赋值。可以看到,这两个属性是互斥的,只要设置一个属性就可以解决所有问题了,不要同时设置两个属性。

Eloquent 模型类默认白名单属性为空,黑名单属性为 *,即所有字段都不会应用批量赋值:

Copy Highlighter-hljs
/** * 使用批量赋值的属性(白名单) * * @var array */ protected $fillable = []; /** * 不使用批量赋值的字段(黑名单) * * @var array */ protected $guarded = ['*'];

我们在实际开发中,对于频繁变动的数据表,建议使用白名单,这样安全性更好,因为哪些字段应用批量赋值始终是可控的,黑名单则会在后续新增字段的时候容易遗漏。而对于相对稳定或者字段很多的数据表,建议使用黑名单,免去设置字段之苦,但是对于这样的模型类,每次修改数据表结构的时候都要记得维护这个黑名单,看看是否需要变动。

所以,以 Post 模型为例,我们需要为其设置一个黑名单字段:

Copy Highlighter-hljs
protected $guarded = ['user_id'];

白名单和黑名单都是以数组属性,支持设置多个字段。这样设置就代表除了 user_id 字段之外,所有其它字段都支持批量赋值。

那排除在批量赋值之外的字段怎么设置呢?只能通过模型属性来设置了:

Copy Highlighter-hljs
$post = new Post($request->all()); $post->user_id = 0; $post->save();

是不是既安全又方便了?尤其是实际开发过程中,文章表可能有十几个字段的时候,效果更加明显。

更新模型#

如果是更新模型类,也可以通过批量赋值的方式实现,只需在获取模型类后使用 fill 方法批量填充属性即可:

Copy Highlighter-hljs
$post = Post::findOrFail(11); $post->fill($request->all()); $post->save();

软删除#

我们在日常开发过程中,删除数据库记录在所难免,但是我们多数时候并不想从数据库中物理删除记录,而只是想从业务角度逻辑删除。

注:所谓物理删除就是彻底删除该记录,逻辑删除只是给这条记录打上一个「已删除」的标记,不再出现在查询结果中,但是并没有真正删除这条记录。

逻辑删除删除好处多多,既保证了不出现在查询结果中的实际需求,又满足了统计或查看历史数据的隐形需求。通常,我们也把逻辑删除称作「软删除」,那对应的物理删除就可以称作「硬删除」了。

实现原理#

Eloquent 模型类为我们提供了「软删除」功能的支持。这就意味着,在 Laravel 中,我们不需要编写任何额外代码就可以实现对数据库记录的「软删除」。其底层实现原理是在支持软删除的数据表中添加一个 deleted_at 字段,这可以通过数据库迁移来实现。比如我们想要让 posts 表支持软删除,需要为其创建一个数据库迁移:

Copy Highlighter-hljs
php artisan make:migration alter_posts_add_deleted_at --table=posts

然后在新生成的迁移文件中编写代码如下:

Copy Highlighter-hljs
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AlterPostsAddDeletedAt extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('posts', function (Blueprint $table) { $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropColumn('deleted_at'); }); } }

这样,运行 php artisan migrate 命令即可在 posts 表中新增一个 deleted_at 字段。该字段默认值为 NULL,表示没有被软删除。如果要在模型类中支持软删除,需要在对应模型类(在本例中是 Post 模型)中添加支持软删除的 Trait:

Copy Highlighter-hljs
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Post extends Model { use SoftDeletes; protected $guarded = ['user_id']; }

SoftDeletes Trait 提供了一系列与软删除相关的方法,下面我们会介绍到。

这样我们在模型类上做所有常规查询操作的时候就会过滤掉被软删除的记录(这些常规查询在上一篇教程中已经给出)。

注:你也可以修改这个默认约定的 deleted_at 字段,但何必费这个劲呢,除非你是从其它系统迁移过来的,原来的表结构已经存在了,这时候可以通过再模型类中设置静态属性 DELETED_AT 来自定义软删除字段。

要软删除一条记录,在对应模型类实例上调用 delete 方法即可,底层会自动将数据表的 deleted_at 字段设置为当前时间,表示该记录已经被「删除」。

相关方法#

要判断一条记录是否被软删除,可以通过 trashed 方法:

Copy Highlighter-hljs
$post = Post::findOrFail(32); $post->delete(); if ($post->trashed()) { dump('该记录已删除'); }

此时再查询 id=32 的记录,已经不存在了,报 404 异常。如果想要在查询结果中出现软删除记录,可以通过在查询的时候调用 withTrashed 方法实现:

Copy Highlighter-hljs
$post = Post::withTrashed()->find(32);

在某些场景下,你可能只需要获取被软删除的记录,这可以通过 onlyTrashed 方法来实现:

Copy Highlighter-hljs
$post = Post::onlyTrashed()->where('views', 0)->get();

如果是误删除的话,你可以 restore 方法来恢复软删除记录:

Copy Highlighter-hljs
$post->restore(); // 恢复单条记录 Post::onlyTrashed()->where('views', 0)->restore(); // 恢复多条记录

最后,如果你确实是想物理删除数据表记录,通过 forceDelete 方法删除即可:

Copy Highlighter-hljs
$post->forceDelete();

访问器和修改器#

访问器#

访问器用于从数据库获取对应字段值后进行一定处理满足指定需求再返回给调用方。

要定义访问器很简单,在相应模型类中设置对应方法即可。以$user->display_name 为例,我们可以在 User 模型类中添加相应的方法 getDisplayNameAttribute(注意这里的转化方式,将小写字母+短划线格式属性转化为驼峰格式方法,后面的修改器也是这样):

Copy Highlighter-hljs
public function getDisplayNameAttribute() { return $this->nickname ? $this->nickname : $this->name; }

这样,我们就可以在代码中直接通过 $user->display_name 访问期望的用户名了,以后如果你想要修改用户名显示逻辑,直接改这个方法里的代码就好了,非常方便。

注:访问器方法名中包含的字段尽量不要和数据库字段名同名,否则会覆盖数据库字段,导致通过模型属性将永远无法访问该数据库字段;另外,如果访问器内部访问了某个数据库字段,则不能将访问器和该数据库字段同名,否则会导致循环引用而报错。比如此例中,就不能将访问器方法名设置为 getNameAttribute 或 getNickNameAttribute。

修改器#

有了访问器,相对的,就有修改器,修改器用于在字段值保存到数据库之前进行一定处理满足需求后再存到数据库。比如做金融的同学可能比较熟悉,在保存用户银行卡号的时候需要加密后才能保存,显示时需要对银行卡号进行脱敏处理。

我们先定义一个加密银行卡号的修改器

Copy Highlighter-hljs
public function setCardNoAttribute($value) { $value = str_replace(' ', '', $value); // 将所有空格去掉 $this->attributes['card_no'] = encrypt($value); }

注:使用 $this->attributes['card_no'] 或者 $this->card_no没有本质上的区别,$this->card_no在框架底层还是会调用 $this->attributes['card_no]

注意修改器传入形参 $value 不能漏掉,否则无法正常设置属性值。下面,我们通过模型类保存一个银行卡号到数据库:

Copy Highlighter-hljs
$user = User::find(1); $user->card_no = '6222020903001483077'; $user->save();

这样,就会将银行卡加密后保存到数据库了。

但是这样的数据回显给用户肯定是不行的,所以我们还要定义一个访问器将加密数据解密,但是银行卡号一般都是脱敏后显示给用户(脱敏是为了安全考虑,避免银行卡号被爬取或劫持),比如支付宝「我的银行卡」页面看到的银行卡号都是脱敏后显示给用户的

这里我们以支付宝为参照,将银行卡后四位显示,其它数字隐藏,并将不同银行卡号位数统一为 16 位。所以我们为银行卡号字段编写访问器如下:

Copy Highlighter-hljs
public function getCardNumAttribute() { if (!$this->card_no) { return ''; } $cardNo = decrypt($this->card_no); $lastFour = mb_substr($cardNo, -4); return '**** **** **** ' . $lastFour; }

注:由于我们在访问器内部访问了 card_no 属性,所以需要将访问器方法名调整为 getCardNumAttribute。

这样,当我们查询并获取到对应模型实例后,访问 $user->card_num 属性,返回的就是脱敏后的银行卡号了:

Copy Highlighter-hljs
**** **** **** 3077

数组 & JSON 转化#

你有一定有过这种经历,数据以 JSON 格式在数据库中存取时,每次存储时都要通过 json_encode 对数据进行编码,读取时都要通过 json_decode 对数据进行解码。我们当然可以通过上述访问器和修改器完成这种操作,但是 Laravel 提供了更加快捷的方法,对于一个在数据库中类型为 JSON 或 TEXT 的字段,我们可以在模型类中将字段对应属性类型转化设置为数组,这样在保存字段到数据库时,会自动将数组数据转化为 JSON 格式,在从数据库读取该字段时,会自动将 JSON 数据转化为数组格式,方便操作。

还是以 users 表为例,我们为其新增一个类型为 JSON 格式的字段 settings,用于保存用户设置信息(MySQL 5.7 以下版本设置字段类型为 TEXT 格式)。然后在 users 表中设置 settings 类型转化格式为 array:

Copy Highlighter-hljs
protected $casts = [ 'settings' => 'array' ];

接下来,我们来测试下保存操作:

Copy Highlighter-hljs
$user = User::find(1); $user->settings = ['city' => '杭州', 'hobby' => ['读书','撸码']]; $user->save();

模型类上使用全局作用域和局部作用域进行查询#

问题引出#

在通过 Eloquent 模型实现增删改查这篇教程中,我们已经学习了如何在 Eloquent 模型类中进行各种查询,但是这些查询大多需要手动调用查询构建器提供的各种方法来实现。如果有一些查询需要在多个地方调用,那么在每个地方都要编写同样的代码,有没有什么办法对这种场景下的查询代码进行优化呢?

Eloquent 模型类提供的「Scope」功能就可以帮我们实现这种优化。「Scope」字面意义上翻译为「作用域」,有点不那么好理解,从功能上来说,把它看作预置的「过滤器」更合适。我们将那些需要在多处调用的查询条件编写过滤器,然后将调用查询代码的地方改为调用过滤器,调用过滤器比编写那些冗长而重复的查询方法更加便捷,可读性也更好。

从调用方式或者过滤器的作用范围来说,可以把「作用域」分为「全局作用域」和「局部作用域」。「作用域」都是围绕模型类展开的,不管是全局作用域还是局部作用域,都是作用到某个模型类上。接下来,我们就来演示如何在 Eloquent 模型类上使用「作用域」进行查询。

全局作用域#

所谓「全局作用域」,指的是预置过滤器在注册该「全局作用域」的模型类的所有查询中生效,不需要指定任何额外条件。

以 User 模型类为例,我们在系统中可能只想针对已经验证过邮箱的用户进行操作,在没有介绍「作用域」之前,可能你会在应用中到处编写这样的代码:

Copy Highlighter-hljs
$users = User::whereNotNull('email_verified_at')->...

接下来,我们通过「全局作用域」来优化上面的代码。

通过全局作用域类实现#

要实现「全局作用域」,首先需要编写一个实现 Illuminate\Database\Eloquent\Scope 接口的全局作用域类,这里我们将其命名为 EmailVerifiedAtScope,并将其放到 app/Scopes 目录下:

Copy Highlighter-hljs
<?php namespace App\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; class EmailVerifiedAtScope implements Scope { public function apply(Builder $builder, Model $model) { return $builder->whereNotNull('email_verified_at'); } }

在这个全局作用域类中,只需要实现 apply 方法即可,在该方法中,在查询构建器上应用过滤器方法并将其返回。

然后,我们需要将这个全局作用域类注册到 User 模型类上,这样,在 User 模型类上进行查询的时候才可以应用相应的过滤条件。这个工作可以通过在 User 模型类中重写父类的 boot 方法来完成:

Copy Highlighter-hljs
protected static function boot() { parent::boot(); static::addGlobalScope(new EmailVerifiedAtScope()); }

注:boot 方法会在模型类实例化的时候调用。你可以在这里进行一些模型类的初始化操作。

这样,就可以将刚刚编写的全局过滤器应用到 User 模型上。如果有多个全局作用域类,可以多次调用 static::addGlobalScope 方法来注册。

这样,当我们通过 User 模型类进行查询的时候,就会自动应用全局作用域指定的查询条件了,以 User::all() 为例,我们通过 Telescope 的 「Queries」 页面就能看到对应的 SQL:

Copy Highlighter-hljs
select * from `users` where `email_verified_at` is not null

包含了全局作用域的过滤条件。

这样,我们就可以把之前查询代码 User::whereNotNull('email_verified_at')->... 中对 email_verified_at 的过滤条件去掉了。

通过匿名函数实现#

如果你觉得编写一个「全局作用域」类很麻烦,过滤逻辑又很简单,还可以在模型类的 boot 方法中通过匿名函数实现全局作用域:

Copy Highlighter-hljs
protected static function boot() { parent::boot(); //static::addGlobalScope(new EmailVerifiedAtScope()); static::addGlobalScope('email_verified_at_scope', function (Builder $builder) { return $builder->whereNotNull('email_verified_at'); }); }

实现效果和上面通过全局作用域类完全一样。

移除全局作用域#

在某些特定场景下,我们可能需要移全局作用域,比如在后台用户管理页,我们需要将未验证邮箱的用户页显示出来,这个时候我们可以借助模型类的 withoutGlobalScope 方法来实现,该方法支持多种传参格式,移除多种全局作用域及其组合:

Copy Highlighter-hljs
User::withoutGlobalScope(EmailVerifiedAtScope::class)->get(); # 指定类 User::withoutGlobalScope('email_verified_at_scope')->get(); # 匿名函数 User::withoutGlobalScopes()->get(); # 移除所有全局作用域 User::withoutGlobalScopes([FirstScope::class, SecondScope::class])->get(); # 移除多个类/匿名函数

局部作用域#

「全局作用域」虽然强大,但不够灵活,有的时候我们的预置过滤器可能因不同场景而已,不同场景需要不同的预置过滤器,这个时候就不能使用「全局作用域」了,要改用「局部作用域」,在不同场景应用不同的局部作用域来完成查询功能。

所谓「局部作用域」,指的是预置过滤器在对应模型类的指定查询中生效,与「全局作用域」不同,「局部作用域」需要额外指定才能生效,但是相应的,也更加灵活,可以适用于不同场景。

「局部作用域」的实现也比较简单,在需要应用它的模型类中定义一个过滤器方法即可。该方法需要以 scope 开头,然后附加该过滤器的名称,以文章列表页显示最流行文章为例(按照浏览数逆序),可以在 Post 模型类中编写一个 scopePopular 方法:

Copy Highlighter-hljs
public function scopePopular(Builder $query) { return $query->where('views', '>', '0')->orderBy('views', 'desc'); }

而在文章详情页,我们希望展示的是已发布的文章详情,如果文章没有发布,返回 404,因此我们再定义一个「局部作用域」方法 scopeActive(没有 status 字段的话新增一个):

Copy Highlighter-hljs
public function scopeActive(Builder $query) { return $query->where('status', Post::ACTIVED); }

同时这个作用域也要应用到列表页,否则影响用户体验。

在模型类上调用「局部作用域」过滤器方法只需调用 scope 之后的过滤器名称即可,Eloquent 底层会通过魔术方法自动调用对应完整方法:

Copy Highlighter-hljs
$post = Post::active()->find(100);

对应的 SQL 语句如下:

Copy Highlighter-hljs
select * from `posts` where `status` = ? and `posts`.`id` = ? and `posts`.`deleted_at` is null limit 1

说明局部作用域已经生效了,通过这个例子你可能不觉得「局部作用域」的优势,我们来看列表页的查询。我们可以在模型类上通过方法链的方式应用多个「局部作用域」,所以对于按照浏览数逆序查询,可以通过下面这种方式实现:

Copy Highlighter-hljs
$post = Post::active()->popular()->get();

对应的 SQL 语句如下:

Copy Highlighter-hljs
select * from `posts` where `status` = 1 and `views` > "0" and `posts`.`deleted_at` is null order by `views` desc

如果我们要把这个 SQL 语句转化为查询构建器的话,显然需要编写多个查询方法,而且如果要在多个地方进行这种查询,一方面代码可读性很差,另一方面而且容易出错,可维护性不好,每次修改一个地方的参数,其它地方要同步修改,换成局部作用域来实现,既清晰又简洁。推荐使用这种方式来构建需要在多个场景调用的复杂 Eloquent 查询。

移除局部作用域很简单,不要在查询中指定对应的过滤器方法即可。

动态作用域#

此外,Eloquent 模型类还支持「动态作用域」,所谓动态作用域指的是在查询过程中动态设置预置过滤器的查询条件,动态作用域和局部作用域类似,过滤器方法名同样以 scope 开头,只不过可以通过额外参数指定查询条件,比如我要在文章中查询指定类型的文章,可以通过在 Post 模型类中定义如下方法:

Copy Highlighter-hljs
public function scopeOfType(Builder $query, $type) { return $query->where('type', $type); }

这样,在查询指定类型的文章时,就可以这么实现:

Copy Highlighter-hljs
$posts = Post::active()->ofType(Post::Article)->get();

对应的 SQL 语句如下:

Copy Highlighter-hljs
select * from `posts` where `status` = ? and `type` = ? and `posts`.`deleted_at` is null

动态作用域的调用和移除方式和局部作用域一样。

posted @   caibaotimes  阅读(1214)  评论(1编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示
CONTENTS