跳至内容

Eloquent:关系

介绍

数据库表通常彼此关联。例如,一篇博客文章可能包含多条评论,或者一个订单可能与下单的用户相关。Eloquent 简化了这些关系的管理和使用,并支持多种常见的关系:

定义关系

Eloquent 关系在 Eloquent 模型类中定义为方法。由于关系本身也是强大的查询构建器,因此将关系定义为方法可以提供强大的方法链和查询功能。例如,我们可以在此posts关系上链式添加其他查询约束:

1$user->posts()->where('active', 1)->get();

但是,在深入研究使用关系之前,让我们先学习如何定义 Eloquent 支持的每种关系类型。

一对一/有一个

A one-to-one relationship is a very basic type of database relationship. For example, a User model might be associated with one Phone model. To define this relationship, we will place a phone method on the User model. The phone method should call the hasOne method and return its result. The hasOne method is available to your model via the model's Illuminate\Database\Eloquent\Model base class:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasOne;
7 
8class User extends Model
9{
10 /**
11 * Get the phone associated with the user.
12 */
13 public function phone(): HasOne
14 {
15 return $this->hasOne(Phone::class);
16 }
17}

The first argument passed to the hasOne method is the name of the related model class. Once the relationship is defined, we may retrieve the related record using Eloquent's dynamic properties. Dynamic properties allow you to access relationship methods as if they were properties defined on the model:

1$phone = User::find(1)->phone;

Eloquent determines the foreign key of the relationship based on the parent model name. In this case, the Phone model is automatically assumed to have a user_id foreign key. If you wish to override this convention, you may pass a second argument to the hasOne method:

1return $this->hasOne(Phone::class, 'foreign_key');

Additionally, Eloquent assumes that the foreign key should have a value matching the primary key column of the parent. In other words, Eloquent will look for the value of the user's id column in the user_id column of the Phone record. If you would like the relationship to use a primary key value other than id or your model's $primaryKey property, you may pass a third argument to the hasOne method:

1return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

Defining the Inverse of the Relationship

So, we can access the Phone model from our User model. Next, let's define a relationship on the Phone model that will let us access the user that owns the phone. We can define the inverse of a hasOne relationship using the belongsTo method:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Phone extends Model
9{
10 /**
11 * Get the user that owns the phone.
12 */
13 public function user(): BelongsTo
14 {
15 return $this->belongsTo(User::class);
16 }
17}

When invoking the user method, Eloquent will attempt to find a User model that has an id which matches the user_id column on the Phone model.

Eloquent determines the foreign key name by examining the name of the relationship method and suffixing the method name with _id. So, in this case, Eloquent assumes that the Phone model has a user_id column. However, if the foreign key on the Phone model is not user_id, you may pass a custom key name as the second argument to the belongsTo method:

1/**
2 * Get the user that owns the phone.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class, 'foreign_key');
7}

If the parent model does not use id as its primary key, or you wish to find the associated model using a different column, you may pass a third argument to the belongsTo method specifying the parent table's custom key:

1/**
2 * Get the user that owns the phone.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
7}

One to Many / Has Many

A one-to-many relationship is used to define relationships where a single model is the parent to one or more child models. For example, a blog post may have an infinite number of comments. Like all other Eloquent relationships, one-to-many relationships are defined by defining a method on your Eloquent model:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class Post extends Model
9{
10 /**
11 * Get the comments for the blog post.
12 */
13 public function comments(): HasMany
14 {
15 return $this->hasMany(Comment::class);
16 }
17}

请记住,Eloquent 会自动为模型确定合适的外键列Comment。按照惯例,Eloquent 会采用父模型的“蛇形命名法”名称,并在其后添加。因此,在本例中,Eloquent 会假设模型_id的外键列为Commentpost_id

一旦定义了关系方法,我们就可以通过访问属性来访问相关评论的集合comments。记住,由于 Eloquent 提供了“动态关系属性”,我们可以像访问模型上的属性一样访问关系方法:

1use App\Models\Post;
2 
3$comments = Post::find(1)->comments;
4 
5foreach ($comments as $comment) {
6 // ...
7}

由于所有关系也充当查询构建器,因此您可以通过调用comments方法并继续将条件链接到查询上来向关系查询添加进一步的约束:

1$comment = Post::find(1)->comments()
2 ->where('title', 'foo')
3 ->first();

与方法类似hasOne,您也可以通过向方法传递附加参数来覆盖外键和本地键hasMany

1return $this->hasMany(Comment::class, 'foreign_key');
2 
3return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

自动为子级添加父级模型

即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题:

1$posts = Post::with('comments')->get();
2 
3foreach ($posts as $post) {
4 foreach ($post->comments as $comment) {
5 echo $comment->post->title;
6 }
7}

在上面的例子中,引入了一个“N + 1”查询问题,因为尽管每个Post模型都预先加载了评论,但 Eloquent 不会自动Post在每个子Comment模型上补充父模型的评论。

如果您希望 Eloquent 自动将父模型绑定到其子模型上,您可以chaperone在定义hasMany关系时调用该方法:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class Post extends Model
9{
10 /**
11 * Get the comments for the blog post.
12 */
13 public function comments(): HasMany
14 {
15 return $this->hasMany(Comment::class)->chaperone();
16 }
17}

或者,如果您希望在运行时选择自动父级水化,则可以chaperone在急切加载关系时调用该模型:

1use App\Models\Post;
2 
3$posts = Post::with([
4 'comments' => fn ($comments) => $comments->chaperone(),
5])->get();

一对多(逆)/属于

现在我们可以访问一篇博文的所有评论了,接下来让我们定义一个关系,允许评论访问其父博文​​。要定义逆关系hasMany,请在子模型上定义一个调用该belongsTo方法的关系方法:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Comment extends Model
9{
10 /**
11 * Get the post that owns the comment.
12 */
13 public function post(): BelongsTo
14 {
15 return $this->belongsTo(Post::class);
16 }
17}

一旦定义了关系,我们就可以通过访问post“动态关系属性”来检索评论的父帖子:

1use App\Models\Comment;
2 
3$comment = Comment::find(1);
4 
5return $comment->post->title;

在上面的例子中,Eloquent 将尝试找到一个与模型上的列匹配Post的模型idpost_idComment

Eloquent 通过检查关联方法的名称,并在方法名称后添加 后缀,_后跟父模型主键列的名称来确定默认外键名称。因此,在本例中,Eloquent 将假定该Post模型在表上的外键commentspost_id

但是,如果您的关系的外键不遵循这些约定,您可以将自定义外键名称作为第二个参数传递给该belongsTo方法:

1/**
2 * Get the post that owns the comment.
3 */
4public function post(): BelongsTo
5{
6 return $this->belongsTo(Post::class, 'foreign_key');
7}

如果您的父模型未使用id作为其主键,或者您希望使用不同的列查找关联模型,则可以将第三个参数传递给belongsTo指定父表的自定义键的方法:

1/**
2 * Get the post that owns the comment.
3 */
4public function post(): BelongsTo
5{
6 return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
7}

默认模型

belongsTo关系允许您定义一个默认模型,当给定关系为 时将返回该模型hasOne此模式通常被称为空对象模式,可以帮助您移除代码中的条件检查。在以下示例中,如果没有用户连接到该模型,则该关系将返回一个空模型hasOneThroughmorphOnenulluserApp\Models\UserPost

1/**
2 * Get the author of the post.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class)->withDefault();
7}

要用属性填充默认模型,您可以将数组或闭包传递给withDefault方法:

1/**
2 * Get the author of the post.
3 */
4public function user(): BelongsTo
5{
6 return $this->belongsTo(User::class)->withDefault([
7 'name' => 'Guest Author',
8 ]);
9}
10 
11/**
12 * Get the author of the post.
13 */
14public function user(): BelongsTo
15{
16 return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
17 $user->name = 'Guest Author';
18 });
19}

查询属于关系

当查询“属于”关系的子项时,您可以手动构建where子句来检索相应的 Eloquent 模型:

1use App\Models\Post;
2 
3$posts = Post::where('user_id', $user->id)->get();

但是,您可能会发现使用该whereBelongsTo方法更方便,它将自动确定给定模型的适当关系和外键:

1$posts = Post::whereBelongsTo($user)->get();

你也可以为该方法提供一个集合whereBelongsTo实例。这样,Laravel 就会检索属于该集合中任意父模型的模型:

1$users = User::where('vip', true)->get();
2 
3$posts = Post::whereBelongsTo($users)->get();

默认情况下,Laravel 将根据模型的类名确定与给定模型关联的关系;但是,您可以通过将关系名称作为whereBelongsTo方法的第二个参数来手动指定关系名称:

1$posts = Post::whereBelongsTo($user, 'author')->get();

拥有多个中的一个

有时,一个模型可能包含多个相关模型,但您希望能够轻松地检索该关系中“最新”或“最旧”的相关模型。例如,一个User模型可能与多个模型相关Order,但您希望定义一种便捷的方式来与用户最近下达的订单进行交互。您可以使用hasOne关系类型结合以下ofMany方法来实现此目的:

1/**
2 * Get the user's most recent order.
3 */
4public function latestOrder(): HasOne
5{
6 return $this->hasOne(Order::class)->latestOfMany();
7}

同样,您可以定义一种方法来检索关系的“最旧”或第一个相关模型:

1/**
2 * Get the user's oldest order.
3 */
4public function oldestOrder(): HasOne
5{
6 return $this->hasOne(Order::class)->oldestOfMany();
7}

默认情况下,latestOfManyoldestOfMany方法会根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时您可能希望使用不同的排序条件从更大的关系中检索单个模型。

例如,使用 该ofMany方法可以检索用户最昂贵的订单。该ofMany方法接受可排序列作为其第一个参数,以及查询关联模型时要应用的聚合函数(min或):max

1/**
2 * Get the user's largest order.
3 */
4public function largestOrder(): HasOne
5{
6 return $this->hasOne(Order::class)->ofMany('price', 'max');
7}

由于 PostgreSQL 不支持MAX针对 UUID 列执行该函数,因此目前无法将多对一关系与 PostgreSQL UUID 列结合使用。

将“多”关系转换为“有一个”关系

latestOfMany通常,当使用、oldestOfMany或方法检索单个模型时ofMany,您已经为同一模型定义了“包含多个”关系。为了方便起见,Laravel 允许您通过one在关系上调用 方法来轻松地将此关系转换为“包含一个”关系:

1/**
2 * Get the user's orders.
3 */
4public function orders(): HasMany
5{
6 return $this->hasMany(Order::class);
7}
8 
9/**
10 * Get the user's largest order.
11 */
12public function largestOrder(): HasOne
13{
14 return $this->orders()->one()->ofMany('price', 'max');
15}

您还可以使用该one方法将HasManyThrough关系转换为HasOneThrough关系:

1public function latestDeployment(): HasOneThrough
2{
3 return $this->deployments()->one()->latestOfMany();
4}

高级有多种关系之一

可以构建更高级的“多选一”关系。例如,一个Product模型可能关联多个Price模型,即使新定价发布后,这些模型仍会保留在系统中。此外,产品的新定价数据可以提前发布,以便在未来某个日期通过列生效published_at

总而言之,我们需要检索最新发布的价格,且发布日期不在未来。此外,如果两个价格的发布日期相同,我们将优先选择 ID 最大的价格。为此,我们必须将一个数组传递给该ofMany方法,其中包含用于确定最新价格的可排序列。此外,该ofMany方法的第二个参数将是一个闭包。此闭包负责为关系查询添加额外的发布日期约束:

1/**
2 * Get the current pricing for the product.
3 */
4public function currentPricing(): HasOne
5{
6 return $this->hasOne(Price::class)->ofMany([
7 'published_at' => 'max',
8 'id' => 'max',
9 ], function (Builder $query) {
10 $query->where('published_at', '<', now());
11 });
12}

有一个通过

“has-one-through” 关系定义了与另一个模型的一对一关系。然而,这种关系表明声明模型可以通过第三个模型与另一个模型的一个实例匹配

例如,在汽车修理厂应用程序中,每个Mechanic模型可能与一个模型关联Car,每个Car模型也可能与一个模型关联。虽然技工和车主在数据库中没有直接关系,但技工可以通过模型Owner访问车主。让我们看一下定义这种关系所需的表:Car

1mechanics
2 id - integer
3 name - string
4
5cars
6 id - integer
7 model - string
8 mechanic_id - integer
9
10owners
11 id - integer
12 name - string
13 car_id - integer

现在我们已经检查了关系的表结构,让我们在Mechanic模型上定义关系:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasOneThrough;
7 
8class Mechanic extends Model
9{
10 /**
11 * Get the car's owner.
12 */
13 public function carOwner(): HasOneThrough
14 {
15 return $this->hasOneThrough(Owner::class, Car::class);
16 }
17}

传递给该hasOneThrough方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。

或者,如果所有涉及该关系的模型都已定义好相关关系,您可以通过调用该through方法并提供这些关系的名称,流畅地定义一个“has-one-through”关系。例如,如果Mechanic模型之间存在一个cars关系,并且Car模型也存在一个owner关系,您可以像这样定义一个连接技工和车主的“has-one-through”关系:

1// String based syntax...
2return $this->through('cars')->has('owner');
3 
4// Dynamic syntax...
5return $this->throughCars()->hasOwner();

关键约定

执行关系查询时,将使用典型的 Eloquent 外键约定。如果您想自定义关系的键,可以将它们作为该hasOneThrough方法的第三和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:

1class Mechanic extends Model
2{
3 /**
4 * Get the car's owner.
5 */
6 public function carOwner(): HasOneThrough
7 {
8 return $this->hasOneThrough(
9 Owner::class,
10 Car::class,
11 'mechanic_id', // Foreign key on the cars table...
12 'car_id', // Foreign key on the owners table...
13 'id', // Local key on the mechanics table...
14 'id' // Local key on the cars table...
15 );
16 }
17}

或者,如前所述,如果所有涉及该关系的模型上都已定义相关关系,则可以通过调用该through方法并提供这些关系的名称来流畅地定义“has-one-through”关系。这种方法的优势在于可以重用现有关系中已定义的关键约定:

1// String based syntax...
2return $this->through('cars')->has('owner');
3 
4// Dynamic syntax...
5return $this->throughCars()->hasOwner();

有许多通过

“has-many-through” 关系提供了一种通过中间关系访问远距离关系的便捷方法。例如,假设我们正在构建一个像Laravel Cloud这样的部署平台。一个模型可能通过一个中间模型Application访问多个模型。使用此示例,您可以轻松收集给定应用程序的所有部署。让我们看一下定义此关系所需的表:DeploymentEnvironment

1applications
2 id - integer
3 name - string
4
5environments
6 id - integer
7 application_id - integer
8 name - string
9
10deployments
11 id - integer
12 environment_id - integer
13 commit_hash - string

现在我们已经检查了关系的表结构,让我们在Application模型上定义关系:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasManyThrough;
7 
8class Application extends Model
9{
10 /**
11 * Get all of the deployments for the application.
12 */
13 public function deployments(): HasManyThrough
14 {
15 return $this->hasManyThrough(Deployment::class, Environment::class);
16 }
17}

传递给该hasManyThrough方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。

或者,如果所有涉及该关系的模型都已定义相关关系,则可以通过调用该through方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果Application模型之间存在一个关系environments,并且Environment模型之间存在一个deployments关系,则可以像这样定义一个连接应用程序和部署的“has-many-through”关系:

1// String based syntax...
2return $this->through('environments')->has('deployments');
3 
4// Dynamic syntax...
5return $this->throughEnvironments()->hasDeployments();

虽然Deployment模型表不包含任何application_id列,但该hasManyThrough关系可以通过 访问应用程序的部署$application->deployments。为了检索这些模型,Eloquent 会检查application_id中间Environment模型表上的列。找到相关的环境 ID 后,即可使用它们来查询模型Deployment表。

关键约定

执行关系查询时,将使用典型的 Eloquent 外键约定。如果您想自定义关系的键,可以将它们作为该hasManyThrough方法的第三和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:

1class Application extends Model
2{
3 public function deployments(): HasManyThrough
4 {
5 return $this->hasManyThrough(
6 Deployment::class,
7 Environment::class,
8 'application_id', // Foreign key on the environments table...
9 'environment_id', // Foreign key on the deployments table...
10 'id', // Local key on the applications table...
11 'id' // Local key on the environments table...
12 );
13 }
14}

或者,如前所述,如果所有涉及该关系的模型上都已定义相关关系,则可以通过调用该through方法并提供这些关系的名称来流畅地定义“多对多”关系。这种方法的优势在于可以重用现有关系中已定义的关键约定:

1// String based syntax...
2return $this->through('environments')->has('deployments');
3 
4// Dynamic syntax...
5return $this->throughEnvironments()->hasDeployments();

范围关系

向模型中添加额外的方法来约束关系是很常见的。例如,你可以featuredPosts向模型添加一个方法,通过附加约束User来约束更广泛的关系postswhere

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class User extends Model
9{
10 /**
11 * Get the user's posts.
12 */
13 public function posts(): HasMany
14 {
15 return $this->hasMany(Post::class)->latest();
16 }
17 
18 /**
19 * Get the user's featured posts.
20 */
21 public function featuredPosts(): HasMany
22 {
23 return $this->posts()->where('featured', true);
24 }
25}

但是,如果您尝试通过 方法创建模型featuredPosts,则其featured属性将不会设置为true。如果您想通过关系方法创建模型,并指定应添加到通过该关系创建的所有模型的属性,则可以withAttributes在构建关系查询时使用 方法:

1/**
2 * Get the user's featured posts.
3 */
4public function featuredPosts(): HasMany
5{
6 return $this->posts()->withAttributes(['featured' => true]);
7}

withAttributes方法将where使用给定的属性向查询添加条件,并且还将给定的属性添加到通过关系方法创建的任何模型中:

1$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
2 
3$post->featured; // true

要指示withAttributes方法不where向查询添加条件,您可以将asConditions参数设置为false

1return $this->posts()->withAttributes(['featured' => true], asConditions: false);

多对多关系

多对多关系比hasOne“与”hasMany关系稍微复杂一些。多对多关系的一个例子是,一个用户拥有多个角色,并且这些角色也由应用程序中的其他用户共享。例如,一个用户可能被分配了“作者”和“编辑”的角色;然而,这些角色也可能被分配给其他用户。因此,一个用户拥有多个角色,一个角色也拥有多个用户。

表结构

要定义这种关系,需要三个数据库表:usersrolesrole_user。 该role_user表根据相关模型名称的字母顺序派生,包含user_idrole_id列。该表用作连接用户和角色的中间表。

请记住,由于一个角色可以属于多个用户,我们不能简单地user_id在表中添加一列roles。这意味着一个角色只能属于一个用户。为了支持将角色分配给多个用户,role_user需要创建该表。我们可以总结如下关系的表结构:

1users
2 id - integer
3 name - string
4
5roles
6 id - integer
7 name - string
8
9role_user
10 user_id - integer
11 role_id - integer

模型结构

多对多关系的定义方式是编写一个返回该belongsToMany方法结果的方法。该方法由应用程序所有 Eloquent 模型所使用的基类belongsToMany提供。例如,让我们在模型上定义一个方法。传递给该方法的第一个参数是相关模型类的名称:Illuminate\Database\Eloquent\ModelrolesUser

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class User extends Model
9{
10 /**
11 * The roles that belong to the user.
12 */
13 public function roles(): BelongsToMany
14 {
15 return $this->belongsToMany(Role::class);
16 }
17}

一旦定义了关系,您就可以使用roles动态关系属性访问用户的角色:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5foreach ($user->roles as $role) {
6 // ...
7}

由于所有关系也充当查询构建器,因此您可以通过调用roles方法并继续将条件链接到查询上来向关系查询添加进一步的约束:

1$roles = User::find(1)->roles()->orderBy('name')->get();

为了确定关系中间表的表名,Eloquent 会按字母顺序连接两个相关的模型名称。但是,您可以自由地覆盖此约定。您可以通过向该belongsToMany方法传递第二个参数来实现:

1return $this->belongsToMany(Role::class, 'role_user');

除了自定义中间表的名称外,你还可以通过向该belongsToMany方法传递附加参数来自定义表中键的列名。第三个参数是要定义关系的模型的外键名称,而第四个参数是要连接到的模型的外键名称:

1return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

定义关系的逆

要定义多对多关系的“逆”,你应该在关联模型上定义一个方法,该方法也返回该belongsToMany方法的结果。为了完成我们的用户/角色示例,让我们在模型users上定义这个方法Role

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class Role extends Model
9{
10 /**
11 * The users that belong to the role.
12 */
13 public function users(): BelongsToMany
14 {
15 return $this->belongsToMany(User::class);
16 }
17}

User如您所见,除了引用模型之外,该关系的定义与其对应的模型完全相同App\Models\User。由于我们重用了该belongsToMany方法,因此在定义多对多关系的“逆”关系时,所有常用的表和键自定义选项均可用。

检索中间表列

正如您已经了解的,处理多对多关系需要中间表。Eloquent 提供了一些非常有用的方法与此表交互。例如,假设我们的User模型与多个Role模型相关联。访问此关系后,我们可以使用pivot模型上的属性访问中间表:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5foreach ($user->roles as $role) {
6 echo $role->pivot->created_at;
7}

请注意,我们检索的每个Role模型都会自动分配一个pivot属性。该属性包含一个表示中间表的模型。

默认情况下,模型上只存在模型键pivot。如果中间表包含额外的属性,则必须在定义关系时指定它们:

1return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

如果您希望中间表具有由 Eloquent 自动维护的时间戳,请在定义关系时调用该created_at方法updated_atwithTimestamps

1return $this->belongsToMany(Role::class)->withTimestamps();

使用 Eloquent 自动维护的时间戳的中间表需要同时具有created_atupdated_at时间戳列。

自定义pivot属性名称

如前所述,可以通过pivot属性在模型上访问中间表中的属性。但是,您可以自由自定义此属性的名称,以更好地反映其在应用程序中的用途。

例如,如果您的应用程序包含可能订阅播客的用户,则用户和播客之间可能存在多对多关系。在这种情况下,您可能希望将中间表属性重命名为 ,subscription而不是pivot。这可以as在定义关系时使用方法来完成:

1return $this->belongsToMany(Podcast::class)
2 ->as('subscription')
3 ->withTimestamps();

一旦指定了自定义中间表属性,您就可以使用自定义名称访问中间表数据:

1$users = User::with('podcasts')->get();
2 
3foreach ($users->flatMap->podcasts as $podcast) {
4 echo $podcast->subscription->created_at;
5}

通过中间表列过滤查询

您还可以在定义关系时belongsToMany使用wherePivot、、、、、wherePivotIn方法过滤关系查询返回的结果wherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull

1return $this->belongsToMany(Role::class)
2 ->wherePivot('approved', 1);
3 
4return $this->belongsToMany(Role::class)
5 ->wherePivotIn('priority', [1, 2]);
6 
7return $this->belongsToMany(Role::class)
8 ->wherePivotNotIn('priority', [1, 2]);
9 
10return $this->belongsToMany(Podcast::class)
11 ->as('subscriptions')
12 ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
13 
14return $this->belongsToMany(Podcast::class)
15 ->as('subscriptions')
16 ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
17 
18return $this->belongsToMany(Podcast::class)
19 ->as('subscriptions')
20 ->wherePivotNull('expired_at');
21 
22return $this->belongsToMany(Podcast::class)
23 ->as('subscriptions')
24 ->wherePivotNotNull('expired_at');

该方法wherePivot会在查询中添加 where 子句约束,但在通过定义的关系创建新模型时不会添加指定的值。如果您需要查询并创建具有特定枢轴值的关系,则可以使用该withPivotValue方法:

1return $this->belongsToMany(Role::class)
2 ->withPivotValue('approved', 1);

通过中间表列排序查询

belongsToMany您可以使用该方法对关系查询返回的结果进行排序orderByPivot。在以下示例中,我们将检索用户的所有最新徽章:

1return $this->belongsToMany(Badge::class)
2 ->where('rank', 'gold')
3 ->orderByPivot('created_at', 'desc');

定义自定义中间表模型

如果您想定义一个自定义模型来表示多对多关系的中间表,可以using在定义关系时调用该方法。自定义数据透视模型允许您在数据透视模型上定义其他行为,例如方法和强制类型转换。

自定义多对多数据透视模型应该扩展该类,Illuminate\Database\Eloquent\Relations\Pivot而自定义多态多对多数据透视模型也应该扩展该类Illuminate\Database\Eloquent\Relations\MorphPivot。例如,我们可以定义一个Role使用自定义数据RoleUser透视模型的模型:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7 
8class Role extends Model
9{
10 /**
11 * The users that belong to the role.
12 */
13 public function users(): BelongsToMany
14 {
15 return $this->belongsToMany(User::class)->using(RoleUser::class);
16 }
17}

定义RoleUser模型时,应该扩展该类Illuminate\Database\Eloquent\Relations\Pivot

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Relations\Pivot;
6 
7class RoleUser extends Pivot
8{
9 // ...
10}

数据透视模型可能无法使用该SoftDeletes特性。如果您需要软删除数据透视记录,请考虑将数据透视模型转换为真正的 Eloquent 模型。

自定义数据透视模型和递增 ID

如果您已经定义了使用自定义枢轴模型的多对多关系,并且该枢轴模型具有自动递增的主键,则应确保您的自定义枢轴模型类定义设置incrementing为的属性true

1/**
2 * Indicates if the IDs are auto-incrementing.
3 *
4 * @var bool
5 */
6public $incrementing = true;

多态关系

多态关系允许子模型使用单个关联从属于多种类型的模型。例如,假设您正在构建一个允许用户分享博客文章和视频的应用程序。在这样的应用程序中,一个Comment模型可能同时属于PostVideo模型。

一对一(多态)

表结构

一对一多态关系类似于典型的一对一关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,博客Post和模型User可以共享一个多态关系Image。使用一对一多态关系,您可以拥有一个包含唯一图像的表,这些图像可能与帖子和用户相关联。首先,让我们检查一下表结构:

1posts
2 id - integer
3 name - string
4
5users
6 id - integer
7 name - string
8
9images
10 id - integer
11 url - string
12 imageable_id - integer
13 imageable_type - string

注意表中的imageable_id和列列将包含帖子或用户的 ID 值,而列将包含父模型的类名。Eloquent使用此列来确定在访问关系时返回哪种“类型”的父模型。在本例中, 列将包含imageable_typeimagesimageable_idimageable_typeimageable_typeimageableApp\Models\PostApp\Models\User

模型结构

接下来,让我们检查一下建立这种关系所需的模型定义:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphTo;
7 
8class Image extends Model
9{
10 /**
11 * Get the parent imageable model (user or post).
12 */
13 public function imageable(): MorphTo
14 {
15 return $this->morphTo();
16 }
17}
18 
19use Illuminate\Database\Eloquent\Model;
20use Illuminate\Database\Eloquent\Relations\MorphOne;
21 
22class Post extends Model
23{
24 /**
25 * Get the post's image.
26 */
27 public function image(): MorphOne
28 {
29 return $this->morphOne(Image::class, 'imageable');
30 }
31}
32 
33use Illuminate\Database\Eloquent\Model;
34use Illuminate\Database\Eloquent\Relations\MorphOne;
35 
36class User extends Model
37{
38 /**
39 * Get the user's image.
40 */
41 public function image(): MorphOne
42 {
43 return $this->morphOne(Image::class, 'imageable');
44 }
45}

检索关系

定义好数据库表和模型后,你就可以通过模型访问它们之间的关系了。例如,为了检索帖子中的图片,我们可以访问image动态关系属性:

1use App\Models\Post;
2 
3$post = Post::find(1);
4 
5$image = $post->image;

您可以通过访问执行调用的方法名称来检索多态模型的父级morphTo。在本例中,该方法是模型imageable上的方法Image。因此,我们将该方法作为动态关系属性来访问:

1use App\Models\Image;
2 
3$image = Image::find(1);
4 
5$imageable = $image->imageable;

imageable模型上的关系Image返回PostUser实例,具体取决于哪种类型的模型拥有图像。

关键约定

如果需要,您可以指定多态子模型所使用的“id”和“type”列的名称。如果这样做,请确保始终将关系名称作为该morphTo方法的第一个参数传递。通常,此值应与方法名称匹配,因此您可以使用 PHP 的__FUNCTION__常量:

1/**
2 * Get the model that the image belongs to.
3 */
4public function imageable(): MorphTo
5{
6 return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
7}

一对多(多态)

表结构

一对多多态关系类似于典型的一对多关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,假设您的应用程序的用户可以对帖子和视频进行“评论”。使用多态关系,您可以使用单个comments表来包含帖子和视频的评论。首先,让我们检查一下构建此关系所需的表结构:

1posts
2 id - integer
3 title - string
4 body - text
5
6videos
7 id - integer
8 title - string
9 url - string
10
11comments
12 id - integer
13 body - text
14 commentable_id - integer
15 commentable_type - string

模型结构

接下来,让我们检查一下建立这种关系所需的模型定义:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphTo;
7 
8class Comment extends Model
9{
10 /**
11 * Get the parent commentable model (post or video).
12 */
13 public function commentable(): MorphTo
14 {
15 return $this->morphTo();
16 }
17}
18 
19use Illuminate\Database\Eloquent\Model;
20use Illuminate\Database\Eloquent\Relations\MorphMany;
21 
22class Post extends Model
23{
24 /**
25 * Get all of the post's comments.
26 */
27 public function comments(): MorphMany
28 {
29 return $this->morphMany(Comment::class, 'commentable');
30 }
31}
32 
33use Illuminate\Database\Eloquent\Model;
34use Illuminate\Database\Eloquent\Relations\MorphMany;
35 
36class Video extends Model
37{
38 /**
39 * Get all of the video's comments.
40 */
41 public function comments(): MorphMany
42 {
43 return $this->morphMany(Comment::class, 'commentable');
44 }
45}

检索关系

定义好数据库表和模型后,你就可以通过模型的动态关系属性来访问它们之间的关系了。例如,要访问某篇文章的所有评论,我们可以使用comments动态属性:

1use App\Models\Post;
2 
3$post = Post::find(1);
4 
5foreach ($post->comments as $comment) {
6 // ...
7}

您还可以通过访问执行调用的方法名称来检索多态子模型的父模型morphTo。在本例中,该方法是模型commentable上的方法Comment。因此,我们将该方法作为动态关系属性来访问,以便访问评论的父模型:

1use App\Models\Comment;
2 
3$comment = Comment::find(1);
4 
5$commentable = $comment->commentable;

commentable模型上的关系Comment返回一个PostVideo实例,具体取决于评论的父模型类型。

自动为子级添加父级模型

即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题:

1$posts = Post::with('comments')->get();
2 
3foreach ($posts as $post) {
4 foreach ($post->comments as $comment) {
5 echo $comment->commentable->title;
6 }
7}

在上面的例子中,引入了一个“N + 1”查询问题,因为尽管每个Post模型都预先加载了评论,但 Eloquent 不会自动Post在每个子Comment模型上补充父模型的评论。

如果您希望 Eloquent 自动将父模型绑定到其子模型上,您可以chaperone在定义morphMany关系时调用该方法:

1class Post extends Model
2{
3 /**
4 * Get all of the post's comments.
5 */
6 public function comments(): MorphMany
7 {
8 return $this->morphMany(Comment::class, 'commentable')->chaperone();
9 }
10}

或者,如果您希望在运行时选择自动父级水化,则可以chaperone在急切加载关系时调用该模型:

1use App\Models\Post;
2 
3$posts = Post::with([
4 'comments' => fn ($comments) => $comments->chaperone(),
5])->get();

多之一(多态)

有时,一个模型可能包含多个相关模型,但您希望轻松检索该关系中“最新”或“最旧”的相关模型。例如,一个User模型可能与多个模型相关Image,但您希望定义一种便捷的方式来与用户上传的最新图片进行交互。您可以使用morphOne关系类型结合以下ofMany方法来实现此目的:

1/**
2 * Get the user's most recent image.
3 */
4public function latestImage(): MorphOne
5{
6 return $this->morphOne(Image::class, 'imageable')->latestOfMany();
7}

同样,您可以定义一种方法来检索关系的“最旧”或第一个相关模型:

1/**
2 * Get the user's oldest image.
3 */
4public function oldestImage(): MorphOne
5{
6 return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
7}

默认情况下,latestOfManyoldestOfMany方法会根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时您可能希望使用不同的排序条件从更大的关系中检索单个模型。

例如,使用 该ofMany方法,你可以检索用户最“喜欢”的图片。该ofMany方法接受可排序列作为其第一个参数,并指定在查询关联模型时应用哪个聚合函数(min或):max

1/**
2 * Get the user's most popular image.
3 */
4public function bestImage(): MorphOne
5{
6 return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
7}

可以构建更高级的“多对一”关系。更多信息,请参阅“多对一”文档

多对多(多态)

表结构

多对多多态关系比“一变一”和“多变一”关系稍微复杂一些。例如,一个Post模型与Video另一个模型之间可以共享一个与另一个模型的多态关系Tag。在这种情况下使用多对多多态关系,可以让你的应用程序拥有一个包含唯一标签的表,这些标签可能与帖子或视频相关联。首先,让我们来看看构建这种关系所需的表结构:

1posts
2 id - integer
3 name - string
4
5videos
6 id - integer
7 name - string
8
9tags
10 id - integer
11 name - string
12
13taggables
14 tag_id - integer
15 taggable_id - integer
16 taggable_type - string

在深入研究多态多对多关系之前,阅读有关典型多对多关系的文档可能会对您有所帮助。

模型结构

接下来,我们准备定义模型之间的关系。PostVideo模型都将包含一个tags方法,该方法调用morphToMany基础 Eloquent 模型类提供的方法。

morphToMany方法接受关联模型的名称以及“关系名称”。根据我们分配给中间表的名称及其包含的键,我们将该关系称为“可标记的”:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphToMany;
7 
8class Post extends Model
9{
10 /**
11 * Get all of the tags for the post.
12 */
13 public function tags(): MorphToMany
14 {
15 return $this->morphToMany(Tag::class, 'taggable');
16 }
17}

定义关系的逆

接下来,在Tag模型上,你应该为其每个可能的父模型定义一个方法。因此,在本例中,我们将定义一个posts方法和一个videos方法。这两个方法都应该返回该方法的结果morphedByMany

morphedByMany方法接受关联模型的名称以及“关系名称”。根据我们分配给中间表的名称及其包含的键,我们将该关系称为“可标记的”:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\MorphToMany;
7 
8class Tag extends Model
9{
10 /**
11 * Get all of the posts that are assigned this tag.
12 */
13 public function posts(): MorphToMany
14 {
15 return $this->morphedByMany(Post::class, 'taggable');
16 }
17 
18 /**
19 * Get all of the videos that are assigned this tag.
20 */
21 public function videos(): MorphToMany
22 {
23 return $this->morphedByMany(Video::class, 'taggable');
24 }
25}

检索关系

定义好数据库表和模型后,你就可以通过模型访问它们之间的关系了。例如,要访问某篇文章的所有标签,你可以使用tags动态关系属性:

1use App\Models\Post;
2 
3$post = Post::find(1);
4 
5foreach ($post->tags as $tag) {
6 // ...
7}

你可以通过访问执行调用的方法名,从多态子模型中检索多态关系的父级morphedByMany。在本例中,该方法是模型上的posts或方法videosTag

1use App\Models\Tag;
2 
3$tag = Tag::find(1);
4 
5foreach ($tag->posts as $post) {
6 // ...
7}
8 
9foreach ($tag->videos as $video) {
10 // ...
11}

自定义多态类型

默认情况下,Laravel 将使用完全限定类名来存储相关模型的“类型”。例如,在上面的一对多关系示例中,一个Comment模型可能属于PostVideo,则默认值commentable_type分别为App\Models\PostApp\Models\Video。但是,您可能希望将这些值与应用程序的内部结构分离。

例如,我们可以使用简单的字符串(例如postvideo)来代替使​​用模型名称作为“类型”。这样,即使模型重命名,数据库中多态的“类型”列值仍然有效:

1use Illuminate\Database\Eloquent\Relations\Relation;
2 
3Relation::enforceMorphMap([
4 'post' => 'App\Models\Post',
5 'video' => 'App\Models\Video',
6]);

如果您愿意,您可以在类的方法enforceMorphMap调用该方法,或者创建单独的服务提供商。bootApp\Providers\AppServiceProvider

你可以在运行时使用模型的方法确定给定模型的变形别名getMorphClass。相反,你也可以使用如下方法确定与变形别名关联的完全限定类名Relation::getMorphedModel

1use Illuminate\Database\Eloquent\Relations\Relation;
2 
3$alias = $post->getMorphClass();
4 
5$class = Relation::getMorphedModel($alias);

当向现有应用程序添加“变形图”时,*_type数据库中每个仍包含完全限定类的可变形列值都需要转换为其“图”名称。

动态关系

您可以使用该resolveRelationUsing方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议在常规应用程序开发中使用,但在开发 Laravel 软件包时偶尔会很有用。

resolveRelationUsing方法接受所需的关系名称作为其第一个参数。传递给该方法的第二个参数应该是一个闭包,它接受模型实例并返回有效的 Eloquent 关系定义。通常,你应该在服务提供者的 boot 方法中配置动态关系:

1use App\Models\Order;
2use App\Models\Customer;
3 
4Order::resolveRelationUsing('customer', function (Order $orderModel) {
5 return $orderModel->belongsTo(Customer::class, 'customer_id');
6});

定义动态关系时,始终向 Eloquent 关系方法提供明确的关键名称参数。

查询关系

由于所有 Eloquent 关系都是通过方法定义的,因此您可以调用这些方法来获取关系实例,而无需实际执行查询来加载相关模型。此外,所有类型的 Eloquent 关系都可以用作查询构建器,允许您在最终对数据库执行 SQL 查询之前继续将约束链接到关系查询上。

例如,想象一个博客应用程序,其中一个User模型有许多关联Post模型:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\HasMany;
7 
8class User extends Model
9{
10 /**
11 * Get all of the posts for the user.
12 */
13 public function posts(): HasMany
14 {
15 return $this->hasMany(Post::class);
16 }
17}

您可以查询posts关系并向关系添加其他约束,如下所示:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->posts()->where('active', 1)->get();

您可以在关系中使用 Laravel查询构建器的任何方法,因此请务必浏览查询构建器文档以了解所有可用的方法。

关系后的链接orWhere子句

如上例所示,您可以在查询关系时自由添加其他约束。但是,将orWhere子句链接到关系时请务必小心,因为这些orWhere子句在逻辑上将与关系约束分组在同一级别:

1$user->posts()
2 ->where('active', 1)
3 ->orWhere('votes', '>=', 100)
4 ->get();

上面的示例将生成以下 SQL。如您所见,该or子句指示查询返回任何投票数大于 100 的帖子。查询不再局限于特定用户:

1select *
2from posts
3where user_id = ? and active = 1 or votes >= 100

在大多数情况下,您应该使用逻辑组将条件检查分组在括号之间:

1use Illuminate\Database\Eloquent\Builder;
2 
3$user->posts()
4 ->where(function (Builder $query) {
5 return $query->where('active', 1)
6 ->orWhere('votes', '>=', 100);
7 })
8 ->get();

上面的示例将生成以下 SQL。请注意,逻辑分组已正确对约束进行了分组,并且查询仍然约束于特定用户:

1select *
2from posts
3where user_id = ? and (active = 1 or votes >= 100)

关系方法与动态属性

如果您不需要在 Eloquent 关系查询中添加其他约束,则可以像访问属性一样访问该关系。例如,继续使用我们的UserPost示例模型,我们可以像这样访问用户的所有帖子:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5foreach ($user->posts as $post) {
6 // ...
7}

动态关系属性执行“延迟加载”,这意味着它们仅在您实际访问时才会加载其关系数据。因此,开发人员经常使用预先加载来预加载他们知道在模型加载后才会访问的关系。预先加载可以显著减少加载模型关系时必须执行的 SQL 查询。

查询关系存在

检索模型记录时,您可能希望根据关系的存在来限制结果。例如,假设您想检索所有至少有一条评论的博客文章。为此,您可以将关系的名称传递给hasandorHas方法:

1use App\Models\Post;
2 
3// Retrieve all posts that have at least one comment...
4$posts = Post::has('comments')->get();

您还可以指定运算符和计数值来进一步自定义查询:

1// Retrieve all posts that have three or more comments...
2$posts = Post::has('comments', '>=', 3)->get();

嵌套has语句可以使用“点”符号构造。例如,你可以检索所有包含至少一条评论且评论中包含至少一张图片的帖子:

1// Retrieve posts that have at least one comment with images...
2$posts = Post::has('comments.images')->get();

如果您需要更多功能,您可以使用whereHasorWhereHas方法在查询中定义额外的查询约束has,例如检查评论的内容:

1use Illuminate\Database\Eloquent\Builder;
2 
3// Retrieve posts with at least one comment containing words like code%...
4$posts = Post::whereHas('comments', function (Builder $query) {
5 $query->where('content', 'like', 'code%');
6})->get();
7 
8// Retrieve posts with at least ten comments containing words like code%...
9$posts = Post::whereHas('comments', function (Builder $query) {
10 $query->where('content', 'like', 'code%');
11}, '>=', 10)->get();

Eloquent 目前不支持跨数据库查询关系的存在。关系必须存在于同一个数据库中。

多对多关系存在查询

whereAttachedTo方法可用于查询与模型或模型集合有多对多附件的模型:

1$users = User::whereAttachedTo($role)->get();

你也可以为该方法提供一个集合whereAttachedTo实例。这样,Laravel 就会检索附加到该集合中任意模型的模型:

1$tags = Tag::whereLike('name', '%laravel%')->get();
2 
3$posts = Post::whereAttachedTo($tags)->get();

内联关系存在查询

如果您想通过在关系查询中附加一个简单的 where 条件来查询关系的存在,您可能会发现使用whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation方法会更方便。例如,我们可以查询所有包含未批准评论的帖子:

1use App\Models\Post;
2 
3$posts = Post::whereRelation('comments', 'is_approved', false)->get();

当然,就像调用查询生成器的where方法一样,您也可以指定一个运算符:

1$posts = Post::whereRelation(
2 'comments', 'created_at', '>=', now()->subHour()
3)->get();

查询关系缺失

检索模型记录时,您可能希望根据是否存在关系来限制结果。例如,假设您想检索所有没有任何评论的博客文章。为此,您可以将关系的名称传递给doesntHaveandorDoesntHave方法:

1use App\Models\Post;
2 
3$posts = Post::doesntHave('comments')->get();

如果您需要更多功能,您可以使用whereDoesntHaveorWhereDoesntHave方法为查询添加额外的查询约束doesntHave,例如检查评论的内容:

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::whereDoesntHave('comments', function (Builder $query) {
4 $query->where('content', 'like', 'code%');
5})->get();

您可以使用“点”符号来针对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子,以及所有有评论且评论均非来自被禁用户的帖子:

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
4 $query->where('banned', 1);
5})->get();

查询 Morph To 关系

要查询“变形为”关系的存在,可以使用whereHasMorphandwhereDoesntHaveMorph方法。这些方法接受关系名称作为第一个参数。接下来,这些方法接受您希望包含在查询中的相关模型的名称。最后,您可以提供一个闭包来自定义关系查询:

1use App\Models\Comment;
2use App\Models\Post;
3use App\Models\Video;
4use Illuminate\Database\Eloquent\Builder;
5 
6// Retrieve comments associated to posts or videos with a title like code%...
7$comments = Comment::whereHasMorph(
8 'commentable',
9 [Post::class, Video::class],
10 function (Builder $query) {
11 $query->where('title', 'like', 'code%');
12 }
13)->get();
14 
15// Retrieve comments associated to posts with a title not like code%...
16$comments = Comment::whereDoesntHaveMorph(
17 'commentable',
18 Post::class,
19 function (Builder $query) {
20 $query->where('title', 'like', 'code%');
21 }
22)->get();

您可能偶尔需要根据相关多态模型的“类型”添加查询约束。传递给该whereHasMorph方法的闭包可能会接收一个$type值作为其第二个参数。此参数允许您检查正在构建的查询的“类型”:

1use Illuminate\Database\Eloquent\Builder;
2 
3$comments = Comment::whereHasMorph(
4 'commentable',
5 [Post::class, Video::class],
6 function (Builder $query, string $type) {
7 $column = $type === Post::class ? 'content' : 'title';
8 
9 $query->where($column, 'like', 'code%');
10 }
11)->get();

有时,您可能想要查询“变形为”关系父级的子级。您可以使用whereMorphedTowhereNotMorphedTo方法来实现此目的,它们将自动确定给定模型的正确变形类型映射。这些方法接受关系名称morphTo作为其第一个参数,并将相关的父模型作为其第二个参数:

1$comments = Comment::whereMorphedTo('commentable', $post)
2 ->orWhereMorphedTo('commentable', $video)
3 ->get();

除了传递可能的多态模型数组之外,您还可以提供*通配符值。这将指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将执行额外的查询以执行此操作:

1use Illuminate\Database\Eloquent\Builder;
2 
3$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
4 $query->where('title', 'like', 'foo%');
5})->get();

有时,你可能希望在不实际加载模型的情况下,统计给定关系的相关模型数量。为此,你可以使用该withCount方法。该方法会在结果模型上withCount添加一个属性:{relation}_count

1use App\Models\Post;
2 
3$posts = Post::withCount('comments')->get();
4 
5foreach ($posts as $post) {
6 echo $post->comments_count;
7}

通过将数组传递给withCount方法,您可以为多个关系添加“计数”,并为查询添加额外的约束:

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
4 $query->where('content', 'like', 'code%');
5}])->get();
6 
7echo $posts[0]->votes_count;
8echo $posts[0]->comments_count;

您还可以为关系计数结果添加别名,允许对同一关系进行多次计数:

1use Illuminate\Database\Eloquent\Builder;
2 
3$posts = Post::withCount([
4 'comments',
5 'comments as pending_comments_count' => function (Builder $query) {
6 $query->where('approved', false);
7 },
8])->get();
9 
10echo $posts[0]->comments_count;
11echo $posts[0]->pending_comments_count;

延迟计数加载

使用该loadCount方法,您可以在检索父模型之后加载关系计数:

1$book = Book::first();
2 
3$book->loadCount('genres');

如果需要在计数查询中设置额外的查询约束,可以传递一个以需要计数的关系为键的数组。数组值应该是接收查询构建器实例的闭包:

1$book->loadCount(['reviews' => function (Builder $query) {
2 $query->where('rating', 5);
3}])

关系计数和自定义选择语句

如果您要withCountselect语句结合,请确保在方法withCount之后调用select

1$posts = Post::select(['title', 'body'])
2 ->withCount('comments')
3 ->get();

其他聚合函数

除了withCount方法之外,Eloquent 还提供了withMinwithMaxwithAvgwithSum和方法。这些方法会在生成的模型上withExists添加属性:{relation}_{function}_{column}

1use App\Models\Post;
2 
3$posts = Post::withSum('comments', 'votes')->get();
4 
5foreach ($posts as $post) {
6 echo $post->comments_sum_votes;
7}

如果您希望使用其他名称访问聚合函数的结果,您可以指定自己的别名:

1$posts = Post::withSum('comments as total_comments', 'votes')->get();
2 
3foreach ($posts as $post) {
4 echo $post->total_comments;
5}

与方法类似loadCount,这些方法也有延迟版本。这些额外的聚合操作可以在已经检索到的 Eloquent 模型上执行:

1$post = Post::first();
2 
3$post->loadSum('comments', 'votes');

如果将这些聚合方法与select语句组合,请确保在该方法之后调用聚合方法select

1$posts = Post::select(['title', 'body'])
2 ->withExists('comments')
3 ->get();

如果您希望急切加载“变形为”关系以及该关系可能返回的各种实体的相关模型计数,则可以将该方法与关系的方法with结合使用morphTomorphWithCount

在此示例中,我们假设Photo模型Post可以创建ActivityFeed模型。我们假设ActivityFeed模型定义了一个名为 的“变形为”关系,parentable该关系允许我们检索给定实例的父级PhotoPost模型ActivityFeed。此外,我们假设Photo模型“拥有多个”Tag模型,并且Post模型“拥有多个”Comment模型。

现在,假设我们需要检索ActivityFeed实例,并立即加载parentable每个ActivityFeed实例的父模型。此外,我们还想检索与每张父照片关联的标签数量,以及与每篇父帖子关联的评论数量:

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$activities = ActivityFeed::with([
4 'parentable' => function (MorphTo $morphTo) {
5 $morphTo->morphWithCount([
6 Photo::class => ['tags'],
7 Post::class => ['comments'],
8 ]);
9 }])->get();

延迟计数加载

假设我们已经检索了一组ActivityFeed模型,现在我们想要加载parentable与Events源关联的各种模型的嵌套关系计数。您可以使用以下loadMorphCount方法来实现此目的:

1$activities = ActivityFeed::with('parentable')->get();
2 
3$activities->loadMorphCount('parentable', [
4 Photo::class => ['tags'],
5 Post::class => ['comments'],
6]);

预先加载

当将 Eloquent 关系作为属性访问时,相关模型会被“延迟加载”。这意味着关系数据直到您首次访问该属性时才会真正加载。但是,Eloquent 可以在您查询父模型时“预先加载”关系。预先加载可以缓解“N + 1”查询问题。为了说明 N + 1 查询问题,请考虑一个Book“属于”其他Author模型的模型:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Book extends Model
9{
10 /**
11 * Get the author that wrote the book.
12 */
13 public function author(): BelongsTo
14 {
15 return $this->belongsTo(Author::class);
16 }
17}

现在,让我们检索所有书籍及其作者:

1use App\Models\Book;
2 
3$books = Book::all();
4 
5foreach ($books as $book) {
6 echo $book->author->name;
7}

此循环将执行一个查询来检索数据库表中的所有书籍,然后对每本书执行另一个查询以检索书籍的作者。因此,如果我们有 25 本书,则上面的代码将运行 26 个查询:一个查询原始书籍,另外 25 个查询用于检索每本书的作者。

值得庆幸的是,我们可以使用预先加载将此操作简化为两个查询。构建查询时,可以使用以下with命令指定哪些关系需要预先加载:

1$books = Book::with('author')->get();
2 
3foreach ($books as $book) {
4 echo $book->author->name;
5}

对于此操作,将仅执行两个查询 - 一个查询检索所有书籍,另一个查询检索所有书籍的所有作者:

1select * from books
2 
3select * from authors where id in (1, 2, 3, 4, 5, ...)

预加载多个关系

有时你可能需要预先加载几种不同的关系。为此,只需将关系数组传递给该with方法:

1$books = Book::with(['author', 'publisher'])->get();

嵌套预加载

要预先加载某个关联的关系,可以使用“点”语法。例如,让我们预先加载这本书的所有作者及其所有个人联系人:

1$books = Book::with('author.contacts')->get();

或者,您可以通过向方法提供嵌套数组来指定嵌套的预加载关系with,这在预加载多个嵌套关系时非常方便:

1$books = Book::with([
2 'author' => [
3 'contacts',
4 'publisher',
5 ],
6])->get();

嵌套预加载morphTo关系

如果您希望预先加载某个morphTo关系,以及该关系可能返回的各种实体上的嵌套关系,则可以将该方法与该关系的方法with结合使用。为了帮助说明此方法,我们考虑以下模型:morphTomorphWith

1<?php
2 
3use Illuminate\Database\Eloquent\Model;
4use Illuminate\Database\Eloquent\Relations\MorphTo;
5 
6class ActivityFeed extends Model
7{
8 /**
9 * Get the parent of the activity feed record.
10 */
11 public function parentable(): MorphTo
12 {
13 return $this->morphTo();
14 }
15}

在此示例中,我们假设EventPhotoPost模型可以创建ActivityFeed模型。此外,我们假设Event模型属于某个Calendar模型,Photo模型与模型相关联Tag,并且Post模型属于某个Author模型。

使用这些模型定义和关系,我们可以检索ActivityFeed模型实例并急切加载所有parentable模型及其各自的嵌套关系:

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$activities = ActivityFeed::query()
4 ->with(['parentable' => function (MorphTo $morphTo) {
5 $morphTo->morphWith([
6 Event::class => ['calendar'],
7 Photo::class => ['tags'],
8 Post::class => ['author'],
9 ]);
10 }])->get();

预先加载特定列

您可能并不总是需要检索关系中的每一列。因此,Eloquent 允许您指定要检索关系中的哪些列:

1$books = Book::with('author:id,name,book_id')->get();

使用此功能时,您应该始终将该id列和任何相关的外键列包含在您希望检索的列的列表中。

默认预加载

有时你可能希望在检索模型时始终加载某些关系。为此,你可以$with在模型上定义一个属性:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Book extends Model
9{
10 /**
11 * The relationships that should always be loaded.
12 *
13 * @var array
14 */
15 protected $with = ['author'];
16 
17 /**
18 * Get the author that wrote the book.
19 */
20 public function author(): BelongsTo
21 {
22 return $this->belongsTo(Author::class);
23 }
24 
25 /**
26 * Get the genre of the book.
27 */
28 public function genre(): BelongsTo
29 {
30 return $this->belongsTo(Genre::class);
31 }
32}

如果您想要$with从单个查询的属性中删除某个项目,您可以使用该without方法:

1$books = Book::without('author')->get();

如果您想要覆盖$with单个查询的属性中的所有项目,您可以使用该withOnly方法:

1$books = Book::withOnly('genre')->get();

限制预加载

有时你可能希望预先加载某个关联,但同时又希望为预先加载查询指定额外的查询条件。你可以将一个关联数组传递给该with方法来实现这一点,其中数组的键是关联名称,数组值是一个闭包,用于为预先加载查询添加额外的约束:

1use App\Models\User;
2use Illuminate\Contracts\Database\Eloquent\Builder;
3 
4$users = User::with(['posts' => function (Builder $query) {
5 $query->where('title', 'like', '%code%');
6}])->get();

在此示例中,Eloquent 只会预先加载帖子列中title包含单词 的帖子code。您可以调用其他查询构建器方法来进一步自定义预先加载操作:

1$users = User::with(['posts' => function (Builder $query) {
2 $query->orderBy('created_at', 'desc');
3}])->get();

限制morphTo关系的预加载

如果您急切地加载关联,Eloquent 将运行多个查询来获取每种类型的关联模型。您可以使用关联的方法morphTo为每个查询添加额外的约束MorphToconstrain

1use Illuminate\Database\Eloquent\Relations\MorphTo;
2 
3$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
4 $morphTo->constrain([
5 Post::class => function ($query) {
6 $query->whereNull('hidden_at');
7 },
8 Video::class => function ($query) {
9 $query->where('type', 'educational');
10 },
11 ]);
12}])->get();

在这个例子中,Eloquent 只会急切加载未被隐藏的帖子和值为type“educational”的视频。

通过关系存在来约束预加载

有时,您可能需要在基于相同条件加载关系的同时检查关系是否存在。例如,您可能希望只检索具有符合给定查询条件User的子模型的模型Post,同时还预先加载匹配的帖子。您可以使用以下withWhereHas方法实现此目的:

1use App\Models\User;
2 
3$users = User::withWhereHas('posts', function ($query) {
4 $query->where('featured', true);
5})->get();

延迟预加载

有时,您可能需要在父模型已检索后立即加载关联模型。例如,如果您需要动态决定是否加载关联模型,这可能会很有用:

1use App\Models\Book;
2 
3$books = Book::all();
4 
5if ($someCondition) {
6 $books->load('author', 'publisher');
7}

如果你需要在预先加载查询中设置额外的查询约束,可以传递一个以你想要加载的关系为键的数组。数组值应该是接收查询实例的闭包实例:

1$author->load(['books' => function (Builder $query) {
2 $query->orderBy('published_date', 'asc');
3}]);

要仅在尚未加载关系时加载关系,请使用该loadMissing方法:

1$book->loadMissing('author');

嵌套延迟预加载和morphTo

如果您想要急切加载一种morphTo关系以及该关系可能返回的各种实体上的嵌套关系,则可以使用该loadMorph方法。

此方法接受关系名称morphTo作为其第一个参数,并接受一个包含模型/关系对的数组作为其第二个参数。为了帮助说明此方法,我们考虑以下模型:

1<?php
2 
3use Illuminate\Database\Eloquent\Model;
4use Illuminate\Database\Eloquent\Relations\MorphTo;
5 
6class ActivityFeed extends Model
7{
8 /**
9 * Get the parent of the activity feed record.
10 */
11 public function parentable(): MorphTo
12 {
13 return $this->morphTo();
14 }
15}

在此示例中,我们假设EventPhotoPost模型可以创建ActivityFeed模型。此外,我们假设Event模型属于某个Calendar模型,Photo模型与模型相关联Tag,并且Post模型属于某个Author模型。

使用这些模型定义和关系,我们可以检索ActivityFeed模型实例并急切加载所有parentable模型及其各自的嵌套关系:

1$activities = ActivityFeed::with('parentable')
2 ->get()
3 ->loadMorph('parentable', [
4 Event::class => ['calendar'],
5 Photo::class => ['tags'],
6 Post::class => ['author'],
7 ]);

自动预加载

此功能目前处于测试阶段,旨在收集社区反馈。即使在补丁版本中,此功能的行为和功能也可能会发生变化。

在许多情况下,Laravel 可以自动预加载您访问的关系。要启用自动预加载,您应该在应用程序的 方法Model::automaticallyEagerLoadRelationships中调用该方法bootAppServiceProvider

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::automaticallyEagerLoadRelationships();
9}

启用此功能后,Laravel 将尝试自动加载您访问的任何之前未加载的关系。例如,考虑以下场景:

1use App\Models\User;
2 
3$users = User::all();
4 
5foreach ($users as $user) {
6 foreach ($user->posts as $post) {
7 foreach ($post->comments as $comment) {
8 echo $comment->content;
9 }
10 }
11}

通常,上面的代码会针对每个用户执行查询以检索其帖子,并针对每个帖子执行查询以检索其评论。但是,automaticallyEagerLoadRelationships启用此功能后,当您尝试访问任何检索到的用户的帖子时,Laravel 将自动以惰性预加载方式加载用户集合中所有用户的帖子。同样,当您尝试访问任何检索到的帖子的评论时,所有最初检索到的帖子的评论都将以惰性预加载方式加载。

如果您不想全局启用自动预加载,您仍然可以通过调用withRelationshipAutoloading集合上的方法来为单个 Eloquent 集合实例启用此功能:

1$users = User::where('vip', true)->get();
2 
3return $users->withRelationshipAutoloading();

防止延迟加载

如前所述,预先加载关联关系通常可以显著提升应用程序的性能。因此,如果您愿意,可以指示 Laravel 始终阻止关联的延迟加载。为此,您可以调用preventLazyLoadingEloquent 基类模型提供的方法。通常,您应该在boot应用程序AppServiceProvider类的方法中调用此方法。

preventLazyLoading方法接受一个可选的布尔参数,用于指示是否应阻止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,这样即使在生产代码中意外存在延迟加载关系,您的生产环境仍能继续正常运行:

1use Illuminate\Database\Eloquent\Model;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Model::preventLazyLoading(! $this->app->isProduction());
9}

防止延迟加载后,Illuminate\Database\LazyLoadingViolationException当您的应用程序尝试延迟加载任何 Eloquent 关系时,Eloquent 将抛出异常。

您可以使用该方法自定义延迟加载违规的行为handleLazyLoadingViolationsUsing。例如,使用此方法,您可以指示仅记录延迟加载违规,而不是以异常中断应用程序的执行:

1Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
2 $class = $model::class;
3 
4 info("Attempted to lazy load [{$relation}] on model [{$class}].");
5});

方法save

Eloquent 提供了便捷的方法将新模型添加到关系中。例如,你可能需要为帖子添加一条新评论。无需手动设置模型post_id上的属性,Comment你只需使用关系的方法插入评论即可save

1use App\Models\Comment;
2use App\Models\Post;
3 
4$comment = new Comment(['message' => 'A new comment.']);
5 
6$post = Post::find(1);
7 
8$post->comments()->save($comment);

请注意,我们没有将comments关系作为动态属性进行访问。相反,我们调用了该comments方法来获取关系的实例。该save方法会自动将适当的post_id值添加到新Comment模型中。

如果需要保存多个关联模型,可以使用如下saveMany方法:

1$post = Post::find(1);
2 
3$post->comments()->saveMany([
4 new Comment(['message' => 'A new comment.']),
5 new Comment(['message' => 'Another new comment.']),
6]);

和方法会持久化指定的模型实例,但不会将新持久化的模型添加到任何已加载到父模型savesaveMany内存关系中。如果您计划在使用savesaveMany方法后访问关系,则可能需要使用refresh方法来重新加载模型及其关系:

1$post->comments()->save($comment);
2 
3$post->refresh();
4 
5// All comments, including the newly saved comment...
6$post->comments;

递归保存模型和关系

如果您想要保存save模型及其所有相关关系,可以使用该push方法。在本例中,Post模型及其评论和评论作者都将被保存:

1$post = Post::find(1);
2 
3$post->comments[0]->message = 'Message';
4$post->comments[0]->author->name = 'Author Name';
5 
6$post->push();

pushQuietly方法可用于保存模型及其相关关系,而不会引发任何事件:

1$post->pushQuietly();

方法create

除了savesaveMany方法之外,你还可以使用create方法,它接受一个属性数组作为参数,创建一个模型并将其插入数据库。save和的区别create在于save接受一个完整的 Eloquent 模型实例,而create接受一个普通的 PHP 对象array。 该方法将返回新创建的模型create

1use App\Models\Post;
2 
3$post = Post::find(1);
4 
5$comment = $post->comments()->create([
6 'message' => 'A new comment.',
7]);

您可以使用该createMany方法创建多个相关模型:

1$post = Post::find(1);
2 
3$post->comments()->createMany([
4 ['message' => 'A new comment.'],
5 ['message' => 'Another new comment.'],
6]);

方法可用于创建模型而不分派任何事件createQuietlycreateManyQuietly

1$user = User::find(1);
2 
3$user->posts()->createQuietly([
4 'title' => 'Post title.',
5]);
6 
7$user->posts()->createManyQuietly([
8 ['title' => 'First post.'],
9 ['title' => 'Second post.'],
10]);

您还可以使用findOrNew、、方法来创建和更新关系模型firstOrNewfirstOrCreateupdateOrCreate

在使用此create方法之前,请务必查看批量分配文档。

属于关系

如果您想将子模型分配给新的父模型,可以使用该associate方法。在本例中,该User模型定义了belongsTo与父Account模型的关系。此associate方法将在子模型上设置外键:

1use App\Models\Account;
2 
3$account = Account::find(10);
4 
5$user->account()->associate($account);
6 
7$user->save();

要从子模型中删除父模型,可以使用dissociate方法。此方法将关系的外键设置为null

1$user->account()->dissociate();
2 
3$user->save();

多对多关系

连接/拆卸

Eloquent 还提供了一些方法,使处理多对多关系更加便捷。例如,假设一个用户可以拥有多个角色,一个角色也可以拥有多个用户。您可以使用该attach方法通过在关系的中间表中插入记录来将角色附加到用户:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->roles()->attach($roleId);

当将关系附加到模型时,您还可以传递要插入到中间表中的附加数据数组:

1$user->roles()->attach($roleId, ['expires' => $expires]);

有时可能需要从用户中删除角色。要删除多对多关系记录,请使用该detach方法。该detach方法将从中间表中删除相应的记录;但是,两个模型仍将保留在数据库中:

1// Detach a single role from the user...
2$user->roles()->detach($roleId);
3 
4// Detach all roles from the user...
5$user->roles()->detach();

为了方便起见,attachdetach接受 ID 数组作为输入:

1$user = User::find(1);
2 
3$user->roles()->detach([1, 2, 3]);
4 
5$user->roles()->attach([
6 1 => ['expires' => $expires],
7 2 => ['expires' => $expires],
8]);

同步关联

您还可以使用sync方法来构建多对多关联。该sync方法接受一个 ID 数组作为参数,并将其放入中间表中。任何不在给定数组中的 ID 都将从中间表中移除。因此,此操作完成后,中间表中将只保留给定数组中的 ID:

1$user->roles()->sync([1, 2, 3]);

您还可以通过 ID 传递其他中间表值:

1$user->roles()->sync([1 => ['expires' => true], 2, 3]);

如果您想要为每个同步的模型 ID 插入相同的中间表值,则可以使用该syncWithPivotValues方法:

1$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

如果您不想从给定数组中分离缺失的现有 ID,则可以使用该syncWithoutDetaching方法:

1$user->roles()->syncWithoutDetaching([1, 2, 3]);

切换关联

多对多关系还提供了一种toggle方法来“切换”给定相关模型 ID 的连接状态。如果给定的 ID 当前已连接,则将其断开连接。同样,如果当前已断开连接,则将其连接:

1$user->roles()->toggle([1, 2, 3]);

您还可以通过 ID 传递其他中间表值:

1$user->roles()->toggle([
2 1 => ['expires' => true],
3 2 => ['expires' => true],
4]);

更新中间表上的记录

如果需要更新关系中间表中的现有行,可以使用该updateExistingPivot方法。该方法接受中间记录外键和要更新的属性数组:

1$user = User::find(1);
2 
3$user->roles()->updateExistingPivot($roleId, [
4 'active' => false,
5]);

接触父时间戳

当一个模型定义了与另一个模型的belongsTobelongsToMany关系时,例如Comment属于 的Post,在更新子模型时更新父模型的时间戳有时会有所帮助。

例如,当Comment模型更新时,你可能希望自动“修改”updated_at所属模型的时间戳Post,使其设置为当前日期和时间。为此,你可以在子模型中添加一个属性,该属性包含应在子模型更新时更新touches其时间戳的关系名称:updated_at

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Relations\BelongsTo;
7 
8class Comment extends Model
9{
10 /**
11 * All of the relationships to be touched.
12 *
13 * @var array
14 */
15 protected $touches = ['post'];
16 
17 /**
18 * Get the post that the comment belongs to.
19 */
20 public function post(): BelongsTo
21 {
22 return $this->belongsTo(Post::class);
23 }
24}

仅当使用 Eloquent 的方法更新子模型时,父模型时间戳才会更新save