Eloquent:关系
介绍
数据库表通常彼此相关。例如,一篇博客文章可能有很多评论,或者一个订单可能与下订单的用户相关。Eloquent 使管理和使用这些关系变得容易,并支持各种常见的关系
定义关系
Eloquent 关系被定义为 Eloquent 模型类上的方法。由于关系也可以作为强大的查询构建器,因此将关系定义为方法可以提供强大的方法链式调用和查询能力。例如,我们可以在此 posts
关系上链接其他查询约束
$user->posts()->where('active', 1)->get();
但是,在深入研究使用关系之前,让我们先学习如何定义 Eloquent 支持的每种关系类型。
一对一 / Has One
一对一关系是一种非常基本的数据库关系类型。例如,User
模型可能与一个 Phone
模型关联。要定义此关系,我们将在 User
模型上放置一个 phone
方法。phone
方法应调用 hasOne
方法并返回其结果。可以通过模型的 Illuminate\Database\Eloquent\Model
基类访问模型的 hasOne
方法
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasOne; class User extends Model{ /** * Get the phone associated with the user. */ public function phone(): HasOne { return $this->hasOne(Phone::class); }}
传递给 hasOne
方法的第一个参数是相关模型类的名称。定义关系后,我们可以使用 Eloquent 的动态属性检索相关记录。动态属性允许您访问关系方法,就像它们是模型上定义的属性一样
$phone = User::find(1)->phone;
Eloquent 根据父模型名称确定关系的外键。在本例中,自动假定 Phone
模型具有 user_id
外键。如果您想覆盖此约定,可以向 hasOne
方法传递第二个参数
return $this->hasOne(Phone::class, 'foreign_key');
此外,Eloquent 假定外键应具有与父级主键列匹配的值。换句话说,Eloquent 将在 Phone
记录的 user_id
列中查找用户的 id
列的值。如果您希望关系使用除 id
或模型的 $primaryKey
属性之外的主键值,可以向 hasOne
方法传递第三个参数
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定义关系的逆关系
因此,我们可以从 User
模型访问 Phone
模型。接下来,让我们在 Phone
模型上定义一个关系,该关系将使我们能够访问拥有该电话的用户。我们可以使用 belongsTo
方法定义 hasOne
关系的逆关系
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Phone extends Model{ /** * Get the user that owns the phone. */ public function user(): BelongsTo { return $this->belongsTo(User::class); }}
当调用 user
方法时,Eloquent 将尝试查找一个 User
模型,该模型的 id
与 Phone
模型上的 user_id
列匹配。
Eloquent 通过检查关系方法的名称并在方法名称后附加 _id
来确定外键名称。因此,在本例中,Eloquent 假定 Phone
模型具有 user_id
列。但是,如果 Phone
模型上的外键不是 user_id
,您可以将自定义键名作为第二个参数传递给 belongsTo
方法
/** * Get the user that owns the phone. */public function user(): BelongsTo{ return $this->belongsTo(User::class, 'foreign_key');}
如果父模型不使用 id
作为其主键,或者您希望使用不同的列查找关联的模型,您可以向 belongsTo
方法传递第三个参数,以指定父表的自定义键
/** * Get the user that owns the phone. */public function user(): BelongsTo{ return $this->belongsTo(User::class, 'foreign_key', 'owner_key');}
一对多 / Has Many
一对多关系用于定义单个模型是父级对一个或多个子模型的关系。例如,一篇博客文章可能包含无限数量的评论。与所有其他 Eloquent 关系一样,通过在 Eloquent 模型上定义方法来定义一对多关系
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasMany; class Post extends Model{ /** * Get the comments for the blog post. */ public function comments(): HasMany { return $this->hasMany(Comment::class); }}
请记住,Eloquent 将自动确定 Comment
模型的正确外键列。按照惯例,Eloquent 将采用父模型的“蛇形命名法”名称并在其后附加 _id
。因此,在本示例中,Eloquent 将假定 Comment
模型上的外键列为 post_id
。
定义关系方法后,我们可以通过访问 comments
属性来访问相关评论的集合。请记住,由于 Eloquent 提供了“动态关系属性”,因此我们可以访问关系方法,就像它们在模型上定义为属性一样
use App\Models\Post; $comments = Post::find(1)->comments; foreach ($comments as $comment) { // ...}
由于所有关系也充当查询构建器,因此您可以通过调用 comments
方法并继续将条件链接到查询,来向关系查询添加进一步的约束
$comment = Post::find(1)->comments() ->where('title', 'foo') ->first();
与 hasOne
方法类似,您还可以通过向 hasMany
方法传递其他参数来覆盖外键和本地键
return $this->hasMany(Comment::class, 'foreign_key'); return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
自动在子级上水合父模型
即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题
$posts = Post::with('comments')->get(); foreach ($posts as $post) { foreach ($post->comments as $comment) { echo $comment->post->title; }}
在上面的示例中,引入了“N + 1”查询问题,因为即使为每个 Post
模型预加载了注释,Eloquent 也不会在每个子 Comment
模型上自动水合父 Post
。
如果你希望 Eloquent 自动将父模型的数据填充到其子模型上,可以在定义 hasMany
关系时调用 chaperone
方法。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasMany; class Post extends Model{ /** * Get the comments for the blog post. */ public function comments(): HasMany { return $this->hasMany(Comment::class)->chaperone(); }}
或者,如果你希望在运行时选择加入自动父级数据填充,可以在预加载关系时调用 chaperone
模型。
use App\Models\Post; $posts = Post::with([ 'comments' => fn ($comments) => $comments->chaperone(),])->get();
一对多(反向) / Belongs To
现在我们可以访问文章的所有评论,让我们定义一个关系,允许评论访问其父文章。要定义 hasMany
关系的逆关系,请在子模型上定义一个关系方法,该方法调用 belongsTo
方法。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Comment extends Model{ /** * Get the post that owns the comment. */ public function post(): BelongsTo { return $this->belongsTo(Post::class); }}
一旦关系被定义,我们可以通过访问 post
“动态关系属性”来检索评论的父文章。
use App\Models\Comment; $comment = Comment::find(1); return $comment->post->title;
在上面的例子中,Eloquent 将尝试查找一个 Post
模型,其 id
与 Comment
模型上的 post_id
列相匹配。
Eloquent 通过检查关系方法的名称,并在方法名称后添加 _
,然后是父模型的主键列的名称,来确定默认的外键名称。因此,在本例中,Eloquent 将假定 Post
模型在 comments
表上的外键是 post_id
。
但是,如果你的关系的外键不遵循这些约定,你可以将自定义外键名称作为第二个参数传递给 belongsTo
方法。
/** * Get the post that owns the comment. */public function post(): BelongsTo{ return $this->belongsTo(Post::class, 'foreign_key');}
如果你的父模型不使用 id
作为其主键,或者你希望使用不同的列来查找关联的模型,你可以将第三个参数传递给 belongsTo
方法,指定你的父表的自定义键。
/** * Get the post that owns the comment. */public function post(): BelongsTo{ return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');}
默认模型
belongsTo
、hasOne
、hasOneThrough
和 morphOne
关系允许你定义一个默认模型,如果给定的关系为 null
,则将返回该模型。这种模式通常被称为 Null Object 模式,可以帮助你消除代码中的条件检查。在下面的示例中,如果 Post
模型没有附加用户,则 user
关系将返回一个空的 App\Models\User
模型。
/** * Get the author of the post. */public function user(): BelongsTo{ return $this->belongsTo(User::class)->withDefault();}
要使用属性填充默认模型,你可以将数组或闭包传递给 withDefault
方法。
/** * Get the author of the post. */public function user(): BelongsTo{ return $this->belongsTo(User::class)->withDefault([ 'name' => 'Guest Author', ]);} /** * Get the author of the post. */public function user(): BelongsTo{ return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) { $user->name = 'Guest Author'; });}
查询属于的关系
当查询“属于”关系的子级时,你可以手动构建 where
子句来检索相应的 Eloquent 模型。
use App\Models\Post; $posts = Post::where('user_id', $user->id)->get();
但是,你可能会发现使用 whereBelongsTo
方法更方便,该方法会自动确定给定模型的正确关系和外键。
$posts = Post::whereBelongsTo($user)->get();
你还可以将一个 集合 实例提供给 whereBelongsTo
方法。这样做时,Laravel 将检索属于集合中任何父模型的模型。
$users = User::where('vip', true)->get(); $posts = Post::whereBelongsTo($users)->get();
默认情况下,Laravel 将根据模型的类名确定与给定模型关联的关系;但是,你可以通过将其作为 whereBelongsTo
方法的第二个参数提供来手动指定关系名称。
$posts = Post::whereBelongsTo($user, 'author')->get();
Has One of Many
有时,一个模型可能有很多相关的模型,但你希望轻松检索关系的“最新”或“最旧”相关模型。例如,一个 User
模型可能与许多 Order
模型相关,但你希望定义一种方便的方式来与用户下的最新订单进行交互。你可以使用 hasOne
关系类型和 ofMany
方法来实现此目的。
/** * Get the user's most recent order. */public function latestOrder(): HasOne{ return $this->hasOne(Order::class)->latestOfMany();}
同样,你可以定义一个方法来检索关系的“最旧”或第一个相关模型。
/** * Get the user's oldest order. */public function oldestOrder(): HasOne{ return $this->hasOne(Order::class)->oldestOfMany();}
默认情况下,latestOfMany
和 oldestOfMany
方法将根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时你可能希望使用不同的排序标准从更大的关系中检索单个模型。
例如,使用 ofMany
方法,你可以检索用户最昂贵的订单。ofMany
方法接受可排序的列作为其第一个参数,以及在查询相关模型时应用的聚合函数(min
或 max
)。
/** * Get the user's largest order. */public function largestOrder(): HasOne{ return $this->hasOne(Order::class)->ofMany('price', 'max');}
由于 PostgreSQL 不支持对 UUID 列执行 MAX
函数,因此目前无法将一对多关系与 PostgreSQL UUID 列结合使用。
将“多”关系转换为一对一关系
通常,当使用 latestOfMany
、oldestOfMany
或 ofMany
方法检索单个模型时,你已经为同一模型定义了“has many”关系。为了方便起见,Laravel 允许你通过在关系上调用 one
方法来轻松地将此关系转换为“has one”关系。
/** * Get the user's orders. */public function orders(): HasMany{ return $this->hasMany(Order::class);} /** * Get the user's largest order. */public function largestOrder(): HasOne{ return $this->orders()->one()->ofMany('price', 'max');}
高级一对多关系
可以构建更高级的“一对多”关系。例如,一个 Product
模型可能有很多相关的 Price
模型,即使在发布新定价后,这些模型也会保留在系统中。此外,产品的新定价数据可能会提前发布,以便在未来的日期通过 published_at
列生效。
因此,总而言之,我们需要检索最新的已发布定价,其中发布日期不是在未来。此外,如果两个价格具有相同的发布日期,我们将优先选择具有最大 ID 的价格。为了实现这一点,我们必须将包含确定最新价格的可排序列的数组传递给 ofMany
方法。此外,还将提供一个闭包作为 ofMany
方法的第二个参数。此闭包将负责向关系查询添加其他发布日期约束。
/** * Get the current pricing for the product. */public function currentPricing(): HasOne{ return $this->hasOne(Price::class)->ofMany([ 'published_at' => 'max', 'id' => 'max', ], function (Builder $query) { $query->where('published_at', '<', now()); });}
Has One Through
“has-one-through”关系定义了与另一个模型的一对一关系。但是,此关系表明声明模型可以通过通过第三个模型与另一个模型的一个实例匹配。
例如,在汽车维修店应用程序中,每个 Mechanic
模型可能与一个 Car
模型关联,并且每个 Car
模型可能与一个 Owner
模型关联。虽然技工和车主在数据库中没有直接关系,但技工可以通过 Car
模型访问车主。让我们看一下定义此关系所需的表。
mechanics id - integer name - string cars id - integer model - string mechanic_id - integer owners id - integer name - string car_id - integer
现在我们已经检查了关系表的结构,让我们在 Mechanic
模型上定义关系。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasOneThrough; class Mechanic extends Model{ /** * Get the car's owner. */ public function carOwner(): HasOneThrough { return $this->hasOneThrough(Owner::class, Car::class); }}
传递给 hasOneThrough
方法的第一个参数是我们要访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果相关关系已经在关系中涉及的所有模型上定义,你可以通过调用 through
方法并提供这些关系的名称来流畅地定义“has-one-through”关系。例如,如果 Mechanic
模型具有 cars
关系,并且 Car
模型具有 owner
关系,你可以像这样定义一个连接技工和车主的“has-one-through”关系。
// String based syntax...return $this->through('cars')->has('owner'); // Dynamic syntax...return $this->throughCars()->hasOwner();
键约定
在执行关系查询时,将使用典型的 Eloquent 外键约定。如果你想自定义关系键,你可以将它们作为 hasOneThrough
方法的第三个和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键。
class Mechanic extends Model{ /** * Get the car's owner. */ public function carOwner(): HasOneThrough { return $this->hasOneThrough( Owner::class, Car::class, 'mechanic_id', // Foreign key on the cars table... 'car_id', // Foreign key on the owners table... 'id', // Local key on the mechanics table... 'id' // Local key on the cars table... ); }}
或者,如前所述,如果相关关系已经在关系中涉及的所有模型上定义,你可以通过调用 through
方法并提供这些关系的名称来流畅地定义“has-one-through”关系。这种方法具有重用已在现有关系上定义的键约定的优势。
// String based syntax...return $this->through('cars')->has('owner'); // Dynamic syntax...return $this->throughCars()->hasOwner();
Has Many Through
“has-many-through”关系提供了一种通过中间关系访问远程关系的便捷方法。例如,假设我们正在构建一个像 Laravel Vapor 这样的部署平台。一个 Project
模型可以通过一个中间的 Environment
模型访问许多 Deployment
模型。使用此示例,你可以轻松收集给定项目的所有部署。让我们看一下定义此关系所需的表。
projects id - integer name - string environments id - integer project_id - integer name - string deployments id - integer environment_id - integer commit_hash - string
现在我们已经检查了关系表的结构,让我们在 Project
模型上定义关系。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Project extends Model{ /** * Get all of the deployments for the project. */ public function deployments(): HasManyThrough { return $this->hasManyThrough(Deployment::class, Environment::class); }}
传递给 hasManyThrough
方法的第一个参数是我们要访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果相关关系已经在关系中涉及的所有模型上定义,你可以通过调用 through
方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果 Project
模型具有 environments
关系,并且 Environment
模型具有 deployments
关系,你可以像这样定义一个连接项目和部署的“has-many-through”关系。
// String based syntax...return $this->through('environments')->has('deployments'); // Dynamic syntax...return $this->throughEnvironments()->hasDeployments();
虽然 Deployment
模型的表不包含 project_id
列,但 hasManyThrough
关系允许通过 $project->deployments
访问项目的部署。要检索这些模型,Eloquent 会检查中间 Environment
模型的表上的 project_id
列。在找到相关的环境 ID 后,它们用于查询 Deployment
模型的表。
键约定
在执行关系查询时,将使用典型的 Eloquent 外键约定。如果你想自定义关系键,你可以将它们作为 hasManyThrough
方法的第三个和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键。
class Project extends Model{ public function deployments(): HasManyThrough { return $this->hasManyThrough( Deployment::class, Environment::class, 'project_id', // Foreign key on the environments table... 'environment_id', // Foreign key on the deployments table... 'id', // Local key on the projects table... 'id' // Local key on the environments table... ); }}
或者,如前所述,如果相关关系已经在关系中涉及的所有模型上定义,你可以通过调用 through
方法并提供这些关系的名称来流畅地定义“has-many-through”关系。这种方法具有重用已在现有关系上定义的键约定的优势。
// String based syntax...return $this->through('environments')->has('deployments'); // Dynamic syntax...return $this->throughEnvironments()->hasDeployments();
多对多关系
多对多关系比 hasOne
和 hasMany
关系稍微复杂。多对多关系的一个例子是用户有多个角色,而这些角色也在应用程序中被其他用户共享。例如,用户可能被分配了“作者”和“编辑”的角色;但是,这些角色也可能被分配给其他用户。因此,一个用户有多个角色,一个角色有多个用户。
表结构
要定义此关系,需要三个数据库表:users
、roles
和 role_user
。role_user
表源自相关模型名称的字母顺序,包含 user_id
和 role_id
列。此表用作连接用户和角色的中间表。
请记住,由于一个角色可以属于多个用户,我们不能简单地在 roles
表上放置一个 user_id
列。这将意味着一个角色只能属于一个用户。为了支持将角色分配给多个用户,需要 role_user
表。我们可以像这样总结关系的表结构。
users id - integer name - string roles id - integer name - string role_user user_id - integer role_id - integer
模型结构
多对多关系是通过编写一个返回 belongsToMany
方法结果的方法来定义的。belongsToMany
方法由 Illuminate\Database\Eloquent\Model
基类提供,该基类由应用程序的所有 Eloquent 模型使用。例如,让我们在 User
模型上定义一个 roles
方法。传递给此方法的第一个参数是相关模型类的名称。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany; class User extends Model{ /** * The roles that belong to the user. */ public function roles(): BelongsToMany { return $this->belongsToMany(Role::class); }}
一旦定义了关系,您可以使用 roles
动态关系属性访问用户的角色。
use App\Models\User; $user = User::find(1); foreach ($user->roles as $role) { // ...}
由于所有关系也充当查询构建器,您可以通过调用 roles
方法并继续在查询上链式添加条件,来进一步约束关系查询。
$roles = User::find(1)->roles()->orderBy('name')->get();
为了确定关系中间表的表名,Eloquent 会按字母顺序连接两个相关模型的名称。但是,您可以自由地覆盖此约定。您可以通过向 belongsToMany
方法传递第二个参数来实现。
return $this->belongsToMany(Role::class, 'role_user');
除了自定义中间表的名称之外,您还可以通过向 belongsToMany
方法传递额外的参数来自定义表上键的列名。第三个参数是您正在定义关系的模型的外部键名称,而第四个参数是您正在连接到的模型的外部键名称。
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
定义关系的逆关系
要定义多对多关系的“反向”,您应该在相关模型上定义一个方法,该方法也返回 belongsToMany
方法的结果。为了完成我们的用户/角色示例,让我们在 Role
模型上定义 users
方法。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model{ /** * The users that belong to the role. */ public function users(): BelongsToMany { return $this->belongsToMany(User::class); }}
正如您所看到的,该关系的定义与其 User
模型对应关系完全相同,只是引用了 App\Models\User
模型。由于我们重用了 belongsToMany
方法,因此在定义多对多关系的“反向”时,所有常用的表和键自定义选项都是可用的。
检索中间表列
正如您已经了解到的,处理多对多关系需要中间表的存在。Eloquent 提供了一些非常有用的与此表交互的方式。例如,假设我们的 User
模型与许多 Role
模型相关联。在访问此关系后,我们可以使用模型上的 pivot
属性访问中间表。
use App\Models\User; $user = User::find(1); foreach ($user->roles as $role) { echo $role->pivot->created_at;}
请注意,我们检索的每个 Role
模型都会自动分配一个 pivot
属性。此属性包含一个表示中间表的模型。
默认情况下,只有模型键会出现在 pivot
模型上。如果您的中间表包含额外的属性,您必须在定义关系时指定它们。
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
如果您希望您的中间表具有 created_at
和 updated_at
时间戳,并且这些时间戳由 Eloquent 自动维护,请在定义关系时调用 withTimestamps
方法。
return $this->belongsToMany(Role::class)->withTimestamps();
利用 Eloquent 自动维护的时间戳的中间表需要同时具有 created_at
和 updated_at
时间戳列。
自定义 pivot
属性名称
如前所述,中间表的属性可以通过 pivot
属性在模型上访问。但是,您可以自由自定义此属性的名称,以更好地反映其在应用程序中的用途。
例如,如果您的应用程序包含可能订阅播客的用户,那么您可能在用户和播客之间存在多对多关系。在这种情况下,您可能希望将中间表属性重命名为 subscription
而不是 pivot
。这可以使用定义关系时的 as
方法来完成。
return $this->belongsToMany(Podcast::class) ->as('subscription') ->withTimestamps();
一旦指定了自定义的中间表属性,您可以使用自定义的名称访问中间表数据。
$users = User::with('podcasts')->get(); foreach ($users->flatMap->podcasts as $podcast) { echo $podcast->subscription->created_at;}
通过中间表列筛选查询
您还可以使用 wherePivot
、wherePivotIn
、wherePivotNotIn
、wherePivotBetween
、wherePivotNotBetween
、wherePivotNull
和 wherePivotNotNull
方法在定义关系时,过滤由 belongsToMany
关系查询返回的结果。
return $this->belongsToMany(Role::class) ->wherePivot('approved', 1); return $this->belongsToMany(Role::class) ->wherePivotIn('priority', [1, 2]); return $this->belongsToMany(Role::class) ->wherePivotNotIn('priority', [1, 2]); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNull('expired_at'); return $this->belongsToMany(Podcast::class) ->as('subscriptions') ->wherePivotNotNull('expired_at');
通过中间表列排序查询
您可以使用 orderByPivot
方法对 belongsToMany
关系查询返回的结果进行排序。在下面的示例中,我们将检索用户的所有最新徽章。
return $this->belongsToMany(Badge::class) ->where('rank', 'gold') ->orderByPivot('created_at', 'desc');
定义自定义中间表模型
如果您想定义一个自定义模型来表示多对多关系的中间表,您可以在定义关系时调用 using
方法。自定义枢轴模型使您有机会在枢轴模型上定义额外的行为,例如方法和类型转换。
自定义多对多枢轴模型应该扩展 Illuminate\Database\Eloquent\Relations\Pivot
类,而自定义多态多对多枢轴模型应该扩展 Illuminate\Database\Eloquent\Relations\MorphPivot
类。例如,我们可以定义一个使用自定义 RoleUser
枢轴模型的 Role
模型。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model{ /** * The users that belong to the role. */ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->using(RoleUser::class); }}
在定义 RoleUser
模型时,您应该扩展 Illuminate\Database\Eloquent\Relations\Pivot
类。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Relations\Pivot; class RoleUser extends Pivot{ // ...}
枢轴模型不能使用 SoftDeletes
特性。如果您需要软删除枢轴记录,请考虑将您的枢轴模型转换为实际的 Eloquent 模型。
自定义枢轴模型和自增 ID
如果您定义了使用自定义枢轴模型的多对多关系,并且该枢轴模型具有自动递增的主键,则应确保您的自定义枢轴模型类定义了一个设置为 true
的 incrementing
属性。
/** * Indicates if the IDs are auto-incrementing. * * @var bool */public $incrementing = true;
多态关系
多态关系允许子模型使用单个关联属于多种类型的模型。例如,假设您正在构建一个允许用户共享博客文章和视频的应用程序。在这样的应用程序中,一个 Comment
模型可能同时属于 Post
和 Video
模型。
一对一(多态)
表结构
一对一多态关系类似于典型的一对一关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,一个博客 Post
和一个 User
可以与一个 Image
模型共享多态关系。使用一对一多态关系允许您拥有一个唯一图像的单表,该表可以与文章和用户关联。首先,让我们检查一下表结构。
posts id - integer name - string users id - integer name - string images id - integer url - string imageable_id - integer imageable_type - string
请注意 images
表上的 imageable_id
和 imageable_type
列。imageable_id
列将包含文章或用户的 ID 值,而 imageable_type
列将包含父模型的类名。imageable_type
列由 Eloquent 用来确定在访问 imageable
关系时返回哪种“类型”的父模型。在这种情况下,该列将包含 App\Models\Post
或 App\Models\User
。
模型结构
接下来,让我们检查一下构建此关系所需的模型定义。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo; class Image extends Model{ /** * Get the parent imageable model (user or post). */ public function imageable(): MorphTo { return $this->morphTo(); }} use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphOne; class Post extends Model{ /** * Get the post's image. */ public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); }} use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphOne; class User extends Model{ /** * Get the user's image. */ public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); }}
检索关系
一旦定义了数据库表和模型,您可以通过模型访问关系。例如,要检索文章的图像,我们可以访问 image
动态关系属性。
use App\Models\Post; $post = Post::find(1); $image = $post->image;
您可以通过访问执行 morphTo
调用的方法的名称来检索多态模型的父级。在这种情况下,它是 Image
模型上的 imageable
方法。因此,我们将作为动态关系属性访问该方法。
use App\Models\Image; $image = Image::find(1); $imageable = $image->imageable;
Image
模型上的 imageable
关系将返回 Post
或 User
实例,具体取决于哪个类型的模型拥有该图像。
键约定
如有必要,您可以指定多态子模型使用的“id”和“type”列的名称。如果您这样做,请确保始终将关系的名称作为第一个参数传递给 morphTo
方法。通常,此值应与方法名称匹配,因此您可以使用 PHP 的 __FUNCTION__
常量。
/** * Get the model that the image belongs to. */public function imageable(): MorphTo{ return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');}
一对多(多态)
表结构
一对多多态关系类似于典型的一对多关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,假设您的应用程序的用户可以“评论”文章和视频。使用多态关系,您可以使用单个 comments
表来包含文章和视频的评论。首先,让我们检查一下构建此关系所需的表结构。
posts id - integer title - string body - text videos id - integer title - string url - string comments id - integer body - text commentable_id - integer commentable_type - string
模型结构
接下来,让我们检查一下构建此关系所需的模型定义。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo; class Comment extends Model{ /** * Get the parent commentable model (post or video). */ public function commentable(): MorphTo { return $this->morphTo(); }} use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphMany; class Post extends Model{ /** * Get all of the post's comments. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); }} use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphMany; class Video extends Model{ /** * Get all of the video's comments. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); }}
检索关系
一旦定义了数据库表和模型,您可以通过模型的动态关系属性访问关系。例如,要访问文章的所有评论,我们可以使用 comments
动态属性。
use App\Models\Post; $post = Post::find(1); foreach ($post->comments as $comment) { // ...}
您还可以通过访问执行 morphTo
调用的方法的名称来检索多态子模型的父级。在这种情况下,它是 Comment
模型上的 commentable
方法。因此,我们将作为动态关系属性访问该方法,以便访问评论的父模型。
use App\Models\Comment; $comment = Comment::find(1); $commentable = $comment->commentable;
Comment
模型上的 commentable
关系将返回 Post
或 Video
实例,具体取决于哪个类型的模型是评论的父级。
自动在子级上水合父模型
即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题
$posts = Post::with('comments')->get(); foreach ($posts as $post) { foreach ($post->comments as $comment) { echo $comment->commentable->title; }}
在上面的示例中,引入了“N + 1”查询问题,因为即使为每个 Post
模型预加载了注释,Eloquent 也不会在每个子 Comment
模型上自动水合父 Post
。
如果您希望 Eloquent 自动将父模型水合到其子模型上,您可以在定义 morphMany
关系时调用 chaperone
方法。
class Post extends Model{ /** * Get all of the post's comments. */ public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable')->chaperone(); }}
或者,如果你希望在运行时选择加入自动父级数据填充,可以在预加载关系时调用 chaperone
模型。
use App\Models\Post; $posts = Post::with([ 'comments' => fn ($comments) => $comments->chaperone(),])->get();
多对一(多态)
有时,一个模型可能有许多相关模型,但您想轻松检索关系的“最新”或“最旧”相关模型。例如,User
模型可能与许多 Image
模型相关,但您想定义一种方便的方式来与用户上传的最新图像进行交互。您可以使用 morphOne
关系类型结合 ofMany
方法来实现此目的。
/** * Get the user's most recent image. */public function latestImage(): MorphOne{ return $this->morphOne(Image::class, 'imageable')->latestOfMany();}
同样,你可以定义一个方法来检索关系的“最旧”或第一个相关模型。
/** * Get the user's oldest image. */public function oldestImage(): MorphOne{ return $this->morphOne(Image::class, 'imageable')->oldestOfMany();}
默认情况下,latestOfMany
和 oldestOfMany
方法将根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时你可能希望使用不同的排序标准从更大的关系中检索单个模型。
例如,使用 ofMany
方法,您可以检索用户最“喜欢”的图像。ofMany
方法接受可排序的列作为其第一个参数,以及在查询相关模型时要应用的聚合函数(min
或 max
)。
/** * Get the user's most popular image. */public function bestImage(): MorphOne{ return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');}
可以构建更高级的“多对一”关系。有关更多信息,请参阅多对一关系文档。
多对多(多态)
表结构
多对多多态关系比“morph one”和“morph many”关系略微复杂。例如,Post
模型和 Video
模型可以与 Tag
模型共享多态关系。在这种情况下使用多对多多态关系将允许您的应用程序拥有一个唯一标签的单表,该标签可以与文章或视频关联。首先,让我们检查一下构建此关系所需的表结构。
posts id - integer name - string videos id - integer name - string tags id - integer name - string taggables tag_id - integer taggable_id - integer taggable_type - string
在深入研究多态多对多关系之前,您可能会从阅读有关典型多对多关系的文档中获益。
模型结构
接下来,我们准备在模型上定义关系。Post
和 Video
模型都将包含一个调用基类 Eloquent 模型提供的 morphToMany
方法的 tags
方法。
morphToMany
方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称和它包含的键的名称,我们将把这种关系称为“taggable”。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphToMany; class Post extends Model{ /** * Get all of the tags for the post. */ public function tags(): MorphToMany { return $this->morphToMany(Tag::class, 'taggable'); }}
定义关系的逆关系
接下来,在 Tag
模型上,你应该为每个可能的父模型定义一个方法。因此,在这个例子中,我们将定义一个 posts
方法和一个 videos
方法。这两个方法都应该返回 morphedByMany
方法的结果。
morphedByMany
方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称和它包含的键的名称,我们将把这种关系称为“taggable”。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphToMany; class Tag extends Model{ /** * Get all of the posts that are assigned this tag. */ public function posts(): MorphToMany { return $this->morphedByMany(Post::class, 'taggable'); } /** * Get all of the videos that are assigned this tag. */ public function videos(): MorphToMany { return $this->morphedByMany(Video::class, 'taggable'); }}
检索关系
一旦你的数据库表和模型被定义好,你就可以通过你的模型访问这些关系。例如,要访问一个帖子的所有标签,你可以使用 tags
动态关系属性。
use App\Models\Post; $post = Post::find(1); foreach ($post->tags as $tag) { // ...}
你可以通过访问执行 morphedByMany
调用的方法名,从多态子模型中检索多态关系的父级。在本例中,它是 Tag
模型上的 posts
或 videos
方法。
use App\Models\Tag; $tag = Tag::find(1); foreach ($tag->posts as $post) { // ...} foreach ($tag->videos as $video) { // ...}
自定义多态类型
默认情况下,Laravel 将使用完全限定的类名来存储相关模型的“类型”。例如,对于上面的一对多关系示例,其中 Comment
模型可能属于 Post
或 Video
模型,默认的 commentable_type
将分别为 App\Models\Post
或 App\Models\Video
。但是,你可能希望将这些值与应用程序的内部结构解耦。
例如,我们可以使用简单的字符串(如 post
和 video
)而不是使用模型名称作为“类型”。通过这样做,即使模型被重命名,数据库中的多态“类型”列的值也将保持有效。
use Illuminate\Database\Eloquent\Relations\Relation; Relation::enforceMorphMap([ 'post' => 'App\Models\Post', 'video' => 'App\Models\Video',]);
如果你愿意,你可以在 App\Providers\AppServiceProvider
类的 boot
方法中调用 enforceMorphMap
方法,或者创建一个单独的服务提供者。
你可以在运行时使用模型的 getMorphClass
方法确定给定模型的 morph 别名。相反,你可以使用 Relation::getMorphedModel
方法确定与 morph 别名关联的完全限定的类名。
use Illuminate\Database\Eloquent\Relations\Relation; $alias = $post->getMorphClass(); $class = Relation::getMorphedModel($alias);
当向现有应用程序添加“morph 映射”时,数据库中每个仍然包含完全限定类的可 morph 的 *_type
列值都需要转换为其“映射”名称。
动态关系
你可以使用 resolveRelationUsing
方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议用于正常的应用程序开发,但在开发 Laravel 包时,这有时可能很有用。
resolveRelationUsing
方法接受所需的关系名称作为其第一个参数。传递给该方法的第二个参数应该是一个闭包,该闭包接受模型实例并返回一个有效的 Eloquent 关系定义。通常,你应该在 服务提供者的引导方法中配置动态关系。
use App\Models\Order;use App\Models\Customer; Order::resolveRelationUsing('customer', function (Order $orderModel) { return $orderModel->belongsTo(Customer::class, 'customer_id');});
在定义动态关系时,始终为 Eloquent 关系方法提供显式的键名参数。
查询关系
由于所有 Eloquent 关系都是通过方法定义的,你可以调用这些方法来获取关系实例,而无需实际执行查询来加载相关模型。此外,所有类型的 Eloquent 关系也可以作为查询构建器,允许你在最终对数据库执行 SQL 查询之前继续将约束链接到关系查询。
例如,想象一个博客应用程序,其中 User
模型有许多关联的 Post
模型。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model{ /** * Get all of the posts for the user. */ public function posts(): HasMany { return $this->hasMany(Post::class); }}
你可以查询 posts
关系并向关系添加额外的约束,如下所示:
use App\Models\User; $user = User::find(1); $user->posts()->where('active', 1)->get();
你可以在关系上使用任何 Laravel 查询构建器方法,因此请务必浏览查询构建器文档以了解所有可用的方法。
在关系之后链式使用 orWhere
子句
如上面的示例所示,在查询关系时,你可以自由地向关系添加额外的约束。但是,在将 orWhere
子句链接到关系时要小心,因为 orWhere
子句将在逻辑上与关系约束处于同一级别。
$user->posts() ->where('active', 1) ->orWhere('votes', '>=', 100) ->get();
上面的示例将生成以下 SQL。如你所见,or
子句指示查询返回 *任何* 投票数大于 100 的帖子。该查询不再受限于特定用户。
select *from postswhere user_id = ? and active = 1 or votes >= 100
在大多数情况下,你应该使用逻辑分组来对括号之间的条件检查进行分组。
use Illuminate\Database\Eloquent\Builder; $user->posts() ->where(function (Builder $query) { return $query->where('active', 1) ->orWhere('votes', '>=', 100); }) ->get();
上面的示例将生成以下 SQL。请注意,逻辑分组已正确地对约束进行了分组,并且查询仍然受限于特定用户。
select *from postswhere user_id = ? and (active = 1 or votes >= 100)
关系方法 vs. 动态属性
如果你不需要向 Eloquent 关系查询添加额外的约束,你可以像访问属性一样访问关系。例如,继续使用我们的 User
和 Post
示例模型,我们可以像这样访问用户的所有帖子:
use App\Models\User; $user = User::find(1); foreach ($user->posts as $post) { // ...}
动态关系属性执行“延迟加载”,这意味着它们仅在实际访问时才加载关系数据。因此,开发人员通常使用预加载来预加载他们知道在加载模型后将被访问的关系。预加载可以显著减少加载模型关系时必须执行的 SQL 查询。
查询关系是否存在
在检索模型记录时,你可能希望根据关系的存在来限制结果。例如,假设你想检索所有至少有一个评论的博客帖子。为此,你可以将关系的名称传递给 has
和 orHas
方法。
use App\Models\Post; // Retrieve all posts that have at least one comment...$posts = Post::has('comments')->get();
你还可以指定一个运算符和计数值来进一步自定义查询。
// Retrieve all posts that have three or more comments...$posts = Post::has('comments', '>=', 3)->get();
可以使用“点”表示法构造嵌套的 has
语句。例如,你可以检索所有至少有一个评论,且该评论至少有一张图片的帖子。
// Retrieve posts that have at least one comment with images...$posts = Post::has('comments.images')->get();
如果你需要更强大的功能,你可以使用 whereHas
和 orWhereHas
方法在你的 has
查询中定义额外的查询约束,例如检查评论的内容。
use Illuminate\Database\Eloquent\Builder; // Retrieve posts with at least one comment containing words like code%...$posts = Post::whereHas('comments', function (Builder $query) { $query->where('content', 'like', 'code%');})->get(); // Retrieve posts with at least ten comments containing words like code%...$posts = Post::whereHas('comments', function (Builder $query) { $query->where('content', 'like', 'code%');}, '>=', 10)->get();
Eloquent 目前不支持跨数据库查询关系存在。这些关系必须存在于同一个数据库中。
内联关系存在查询
如果你想使用附加到关系查询的单个简单 where 条件来查询关系的存在,你可能会发现使用 whereRelation
、orWhereRelation
、whereMorphRelation
和 orWhereMorphRelation
方法更方便。例如,我们可以查询所有有未批准评论的帖子。
use App\Models\Post; $posts = Post::whereRelation('comments', 'is_approved', false)->get();
当然,与调用查询构建器的 where
方法一样,你也可以指定一个运算符。
$posts = Post::whereRelation( 'comments', 'created_at', '>=', now()->subHour())->get();
查询关系是否不存在
在检索模型记录时,你可能希望根据关系的不存在来限制结果。例如,假设你想检索所有 *没有* 任何评论的博客帖子。为此,你可以将关系的名称传递给 doesntHave
和 orDoesntHave
方法。
use App\Models\Post; $posts = Post::doesntHave('comments')->get();
如果你需要更强大的功能,你可以使用 whereDoesntHave
和 orWhereDoesntHave
方法来向你的 doesntHave
查询添加额外的查询约束,例如检查评论的内容。
use Illuminate\Database\Eloquent\Builder; $posts = Post::whereDoesntHave('comments', function (Builder $query) { $query->where('content', 'like', 'code%');})->get();
你可以使用“点”表示法对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子;但是,来自未被禁止的作者的评论的帖子将包含在结果中。
use Illuminate\Database\Eloquent\Builder; $posts = Post::whereDoesntHave('comments.author', function (Builder $query) { $query->where('banned', 0);})->get();
查询 Morph To 关系
要查询“morph to”关系的存在,你可以使用 whereHasMorph
和 whereDoesntHaveMorph
方法。这些方法接受关系的名称作为它们的第一个参数。接下来,这些方法接受你希望包含在查询中的相关模型的名称。最后,你可以提供一个闭包来定制关系查询。
use App\Models\Comment;use App\Models\Post;use App\Models\Video;use Illuminate\Database\Eloquent\Builder; // Retrieve comments associated to posts or videos with a title like code%...$comments = Comment::whereHasMorph( 'commentable', [Post::class, Video::class], function (Builder $query) { $query->where('title', 'like', 'code%'); })->get(); // Retrieve comments associated to posts with a title not like code%...$comments = Comment::whereDoesntHaveMorph( 'commentable', Post::class, function (Builder $query) { $query->where('title', 'like', 'code%'); })->get();
有时,你可能需要根据相关多态模型的“类型”添加查询约束。传递给 whereHasMorph
方法的闭包可以接收 $type
值作为其第二个参数。此参数允许你检查正在构建的查询的“类型”。
use Illuminate\Database\Eloquent\Builder; $comments = Comment::whereHasMorph( 'commentable', [Post::class, Video::class], function (Builder $query, string $type) { $column = $type === Post::class ? 'content' : 'title'; $query->where($column, 'like', 'code%'); })->get();
有时你可能想查询“morph to”关系父级的子级。你可以使用 whereMorphedTo
和 whereNotMorphedTo
方法来实现此目的,这些方法将自动确定给定模型的正确 morph 类型映射。这些方法接受 morphTo
关系的名称作为它们的第一个参数,并接受相关的父模型作为它们的第二个参数。
$comments = Comment::whereMorphedTo('commentable', $post) ->orWhereMorphedTo('commentable', $video) ->get();
查询所有相关模型
你可以提供 *
作为通配符值,而不是传递可能的多个多态模型的数组。这将指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将执行额外的查询以执行此操作。
use Illuminate\Database\Eloquent\Builder; $comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) { $query->where('title', 'like', 'foo%');})->get();
聚合相关模型
计算相关模型数量
有时,你可能想计算给定关系的关联模型的数量,而无需实际加载这些模型。要实现此目的,你可以使用 withCount
方法。withCount
方法将在结果模型上放置一个 {关系}_count
属性。
use App\Models\Post; $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count;}
通过将一个数组传递给 withCount
方法,你可以添加多个关系的“计数”,并向查询添加额外的约束。
use Illuminate\Database\Eloquent\Builder; $posts = Post::withCount(['votes', 'comments' => function (Builder $query) { $query->where('content', 'like', 'code%');}])->get(); echo $posts[0]->votes_count;echo $posts[0]->comments_count;
你还可以为关系计数结果设置别名,从而允许在同一关系上进行多次计数。
use Illuminate\Database\Eloquent\Builder; $posts = Post::withCount([ 'comments', 'comments as pending_comments_count' => function (Builder $query) { $query->where('approved', false); },])->get(); echo $posts[0]->comments_count;echo $posts[0]->pending_comments_count;
延迟计数加载
使用 loadCount
方法,你可以在父模型已被检索后加载关系计数。
$book = Book::first(); $book->loadCount('genres');
如果需要在计数查询上设置额外的查询约束,你可以传递一个以你希望计数的关系为键的数组。数组值应该是接收查询构建器实例的闭包。
$book->loadCount(['reviews' => function (Builder $query) { $query->where('rating', 5);}])
关系计数和自定义 Select 语句
如果你将 withCount
与 select
语句组合使用,请确保在 select
方法之后调用 withCount
。
$posts = Post::select(['title', 'body']) ->withCount('comments') ->get();
其他聚合函数
除了 withCount
方法外,Eloquent 还提供了 withMin
、withMax
、withAvg
、withSum
和 withExists
方法。这些方法将在你的结果模型上放置一个 {关系}_{函数}_{列}
属性。
use App\Models\Post; $posts = Post::withSum('comments', 'votes')->get(); foreach ($posts as $post) { echo $post->comments_sum_votes;}
如果你希望使用其他名称访问聚合函数的结果,你可以指定自己的别名。
$posts = Post::withSum('comments as total_comments', 'votes')->get(); foreach ($posts as $post) { echo $post->total_comments;}
与 loadCount
方法类似,这些方法的延迟版本也可用。这些额外的聚合操作可以在已检索的 Eloquent 模型上执行。
$post = Post::first(); $post->loadSum('comments', 'votes');
如果你将这些聚合方法与 select
语句组合使用,请确保在 select
方法之后调用聚合方法。
$posts = Post::select(['title', 'body']) ->withExists('comments') ->get();
计算 Morph To 关系的相关模型数量
如果你想预加载一个“morph to”关系,以及该关系可能返回的各种实体的相关模型计数,你可以结合使用 with
方法和 morphTo
关系的 morphWithCount
方法。
在这个例子中,假设 Photo
和 Post
模型可以创建 ActivityFeed
模型。我们假设 ActivityFeed
模型定义了一个名为 parentable
的“多态关联”关系,它允许我们检索给定 ActivityFeed
实例的父级 Photo
或 Post
模型。此外,假设 Photo
模型“拥有多个” Tag
模型,而 Post
模型“拥有多个” Comment
模型。
现在,假设我们要检索 ActivityFeed
实例,并为每个 ActivityFeed
实例急切加载 parentable
父模型。此外,我们还想检索与每个父级照片关联的标签数量以及与每个父级帖子关联的评论数量。
use Illuminate\Database\Eloquent\Relations\MorphTo; $activities = ActivityFeed::with([ 'parentable' => function (MorphTo $morphTo) { $morphTo->morphWithCount([ Photo::class => ['tags'], Post::class => ['comments'], ]); }])->get();
延迟计数加载
假设我们已经检索了一组 ActivityFeed
模型,现在我们想加载与活动提要关联的各种 parentable
模型的嵌套关系计数。您可以使用 loadMorphCount
方法来实现此目的。
$activities = ActivityFeed::with('parentable')->get(); $activities->loadMorphCount('parentable', [ Photo::class => ['tags'], Post::class => ['comments'],]);
预加载
当将 Eloquent 关系作为属性访问时,相关模型是“延迟加载”的。这意味着关系数据实际上直到您首次访问该属性时才会被加载。但是,Eloquent 可以在您查询父模型时“急切加载”关系。“急切加载”缓解了“N + 1”查询问题。为了说明 N + 1 查询问题,请考虑一个“属于” Author
模型的 Book
模型。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Book extends Model{ /** * Get the author that wrote the book. */ public function author(): BelongsTo { return $this->belongsTo(Author::class); }}
现在,让我们检索所有书籍及其作者。
use App\Models\Book; $books = Book::all(); foreach ($books as $book) { echo $book->author->name;}
此循环将执行一个查询来检索数据库表中的所有书籍,然后为每本书执行另一个查询以检索该书的作者。因此,如果我们有 25 本书,上面的代码将运行 26 个查询:一个用于原始书籍,另外 25 个用于检索每本书的作者。
值得庆幸的是,我们可以使用急切加载将此操作减少到仅两个查询。在构建查询时,您可以使用 with
方法指定应急切加载的关系。
$books = Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name;}
对于此操作,只会执行两个查询 - 一个查询检索所有书籍,另一个查询检索所有书籍的所有作者。
select * from books select * from authors where id in (1, 2, 3, 4, 5, ...)
急切加载多个关系
有时您可能需要急切加载多个不同的关系。为此,只需将关系数组传递给 with
方法。
$books = Book::with(['author', 'publisher'])->get();
嵌套急切加载
要急切加载某个关系的关系,可以使用“点”语法。例如,让我们急切加载所有书的作者以及所有作者的个人联系方式。
$books = Book::with('author.contacts')->get();
或者,您可以通过向 with
方法提供嵌套数组来指定嵌套急切加载的关系,这在急切加载多个嵌套关系时会很方便。
$books = Book::with([ 'author' => [ 'contacts', 'publisher', ],])->get();
嵌套急切加载 morphTo
关系
如果您想急切加载 morphTo
关系,以及该关系可能返回的各种实体上的嵌套关系,则可以将 with
方法与 morphTo
关系的 morphWith
方法结合使用。为了帮助说明此方法,让我们考虑以下模型
<?php use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo; class ActivityFeed extends Model{ /** * Get the parent of the activity feed record. */ public function parentable(): MorphTo { return $this->morphTo(); }}
在此示例中,假设 Event
、Photo
和 Post
模型可以创建 ActivityFeed
模型。此外,假设 Event
模型属于 Calendar
模型,Photo
模型与 Tag
模型相关联,而 Post
模型属于 Author
模型。
使用这些模型定义和关系,我们可以检索 ActivityFeed
模型实例,并急切加载所有 parentable
模型及其各自的嵌套关系
use Illuminate\Database\Eloquent\Relations\MorphTo; $activities = ActivityFeed::query() ->with(['parentable' => function (MorphTo $morphTo) { $morphTo->morphWith([ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]); }])->get();
急切加载特定列
您可能并不总是需要检索到的关系中的每一列。因此,Eloquent 允许您指定要检索的关系的哪些列
$books = Book::with('author:id,name,book_id')->get();
使用此功能时,您应始终在要检索的列列表中包含 id
列和任何相关的外键列。
默认急切加载
有时您可能希望在检索模型时始终加载某些关系。要实现此目的,您可以在模型上定义一个 $with
属性。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Book extends Model{ /** * The relationships that should always be loaded. * * @var array */ protected $with = ['author']; /** * Get the author that wrote the book. */ public function author(): BelongsTo { return $this->belongsTo(Author::class); } /** * Get the genre of the book. */ public function genre(): BelongsTo { return $this->belongsTo(Genre::class); }}
如果您想从单个查询的 $with
属性中删除一项,可以使用 without
方法。
$books = Book::without('author')->get();
如果您想为单个查询覆盖 $with
属性中的所有项,可以使用 withOnly
方法。
$books = Book::withOnly('genre')->get();
约束预加载
有时您可能希望急切加载关系,但还要为急切加载查询指定其他查询条件。您可以通过将关系数组传递给 with
方法来实现此目的,其中数组键是关系名称,数组值是一个闭包,该闭包将其他约束添加到急切加载查询中。
use App\Models\User;use Illuminate\Contracts\Database\Eloquent\Builder; $users = User::with(['posts' => function (Builder $query) { $query->where('title', 'like', '%code%');}])->get();
在此示例中,Eloquent 将仅急切加载帖子,其中帖子的 title
列包含单词 code
。您可以调用其他 查询构建器 方法来进一步自定义急切加载操作
$users = User::with(['posts' => function (Builder $query) { $query->orderBy('created_at', 'desc');}])->get();
约束 morphTo
关系的急切加载
如果您正在急切加载 morphTo
关系,Eloquent 将运行多个查询来获取每种类型的相关模型。您可以使用 MorphTo
关系的 constrain
方法向这些查询中的每一个添加其他约束。
use Illuminate\Database\Eloquent\Relations\MorphTo; $comments = Comment::with(['commentable' => function (MorphTo $morphTo) { $morphTo->constrain([ Post::class => function ($query) { $query->whereNull('hidden_at'); }, Video::class => function ($query) { $query->where('type', 'educational'); }, ]);}])->get();
在此示例中,Eloquent 将仅急切加载未隐藏的帖子和 type
值为“educational”的视频。
使用关系存在来约束急切加载
有时您可能会发现自己需要检查关系的存在,同时根据相同的条件加载关系。例如,您可能希望仅检索具有与给定查询条件匹配的子 Post
模型的 User
模型,同时还急切加载匹配的帖子。您可以使用 withWhereHas
方法来实现此目的。
use App\Models\User; $users = User::withWhereHas('posts', function ($query) { $query->where('featured', true);})->get();
延迟预加载
有时您可能需要在检索父模型后急切加载关系。例如,如果您需要动态决定是否加载相关模型,这可能很有用。
use App\Models\Book; $books = Book::all(); if ($someCondition) { $books->load('author', 'publisher');}
如果您需要在急切加载查询中设置其他查询约束,则可以传递一个以要加载的关系为键的数组。数组值应为接收查询实例的闭包实例。
$author->load(['books' => function (Builder $query) { $query->orderBy('published_date', 'asc');}]);
要仅在尚未加载关系时加载关系,请使用 loadMissing
方法。
$book->loadMissing('author');
嵌套延迟急切加载和 morphTo
如果您想急切加载 morphTo
关系,以及该关系可能返回的各种实体上的嵌套关系,则可以使用 loadMorph
方法。
此方法将 morphTo
关系的名称作为其第一个参数,并将模型/关系对的数组作为其第二个参数。为了帮助说明此方法,让我们考虑以下模型
<?php use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo; class ActivityFeed extends Model{ /** * Get the parent of the activity feed record. */ public function parentable(): MorphTo { return $this->morphTo(); }}
在此示例中,假设 Event
、Photo
和 Post
模型可以创建 ActivityFeed
模型。此外,假设 Event
模型属于 Calendar
模型,Photo
模型与 Tag
模型相关联,而 Post
模型属于 Author
模型。
使用这些模型定义和关系,我们可以检索 ActivityFeed
模型实例,并急切加载所有 parentable
模型及其各自的嵌套关系
$activities = ActivityFeed::with('parentable') ->get() ->loadMorph('parentable', [ Event::class => ['calendar'], Photo::class => ['tags'], Post::class => ['author'], ]);
防止延迟加载
如前所述,急切加载关系通常可以为您的应用程序带来显着的性能优势。因此,如果您愿意,可以指示 Laravel 始终阻止关系的延迟加载。要实现此目的,您可以调用基本 Eloquent 模型类提供的 preventLazyLoading
方法。通常,您应该在应用程序的 AppServiceProvider
类的 boot
方法中调用此方法。
preventLazyLoading
方法接受一个可选的布尔参数,该参数指示是否应阻止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,以便即使生产代码中意外存在延迟加载关系,您的生产环境也能继续正常运行
use Illuminate\Database\Eloquent\Model; /** * Bootstrap any application services. */public function boot(): void{ Model::preventLazyLoading(! $this->app->isProduction());}
阻止延迟加载后,当您的应用程序尝试延迟加载任何 Eloquent 关系时,Eloquent 将引发 Illuminate\Database\LazyLoadingViolationException
异常。
您可以使用 handleLazyLoadingViolationsUsing
方法自定义延迟加载违规的行为。例如,使用此方法,您可以指示延迟加载违规仅被记录,而不是通过异常中断应用程序的执行。
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) { $class = $model::class; info("Attempted to lazy load [{$relation}] on model [{$class}].");});
插入和更新相关模型
save
方法
Eloquent 提供了方便的方法来向关系中添加新模型。例如,也许您需要向帖子添加新评论。您可以使用关系的 save
方法插入评论,而不是手动设置 Comment
模型上的 post_id
属性
use App\Models\Comment;use App\Models\Post; $comment = new Comment(['message' => 'A new comment.']); $post = Post::find(1); $post->comments()->save($comment);
请注意,我们没有将 comments
关系作为动态属性访问。相反,我们调用了 comments
方法来获取关系的实例。save
方法将自动将适当的 post_id
值添加到新的 Comment
模型。
如果您需要保存多个相关模型,可以使用 saveMany
方法。
$post = Post::find(1); $post->comments()->saveMany([ new Comment(['message' => 'A new comment.']), new Comment(['message' => 'Another new comment.']),]);
save
和 saveMany
方法将持久化给定的模型实例,但不会将新持久化的模型添加到已加载到父模型上的任何内存中关系。如果您计划在使用 save
或 saveMany
方法后访问关系,则可能希望使用 refresh
方法重新加载模型及其关系。
$post->comments()->save($comment); $post->refresh(); // All comments, including the newly saved comment...$post->comments;
递归保存模型和关系
如果您想 save
您的模型及其所有关联的关系,可以使用 push
方法。在此示例中,将保存 Post
模型以及其评论和评论的作者
$post = Post::find(1); $post->comments[0]->message = 'Message';$post->comments[0]->author->name = 'Author Name'; $post->push();
可以使用 pushQuietly
方法来保存模型及其关联的关系,而不会引发任何事件
$post->pushQuietly();
create
方法
除了 save
和 saveMany
方法之外,您还可以使用 create
方法,该方法接受属性数组,创建模型并将其插入数据库。save
和 create
之间的区别在于 save
接受完整的 Eloquent 模型实例,而 create
接受纯 PHP array
。新创建的模型将由 create
方法返回。
use App\Models\Post; $post = Post::find(1); $comment = $post->comments()->create([ 'message' => 'A new comment.',]);
您可以使用 createMany
方法创建多个相关模型。
$post = Post::find(1); $post->comments()->createMany([ ['message' => 'A new comment.'], ['message' => 'Another new comment.'],]);
可以使用 createQuietly
和 createManyQuietly
方法创建模型,而无需分派任何事件。
$user = User::find(1); $user->posts()->createQuietly([ 'title' => 'Post title.',]); $user->posts()->createManyQuietly([ ['title' => 'First post.'], ['title' => 'Second post.'],]);
您还可以使用 findOrNew
、firstOrNew
、firstOrCreate
和 updateOrCreate
方法来 在关系上创建和更新模型。
在使用 create
方法之前,请务必查看 批量赋值 文档。
Belongs To 关系
如果您想将子模型分配给新的父模型,可以使用 associate
方法。在此示例中,User
模型定义与 Account
模型的 belongsTo
关系。此 associate
方法将在子模型上设置外键
use App\Models\Account; $account = Account::find(10); $user->account()->associate($account); $user->save();
要从子模型中删除父模型,可以使用 dissociate
方法。此方法会将关系的外键设置为 null
$user->account()->dissociate(); $user->save();
多对多关系
附加/分离
Eloquent 还提供了使处理多对多关系更方便的方法。例如,假设一个用户可以拥有多个角色,一个角色可以拥有多个用户。您可以使用 attach
方法通过在关系的中间表中插入记录来将角色附加到用户
use App\Models\User; $user = User::find(1); $user->roles()->attach($roleId);
将关系附加到模型时,您还可以传递其他数据的数组以插入到中间表中。
$user->roles()->attach($roleId, ['expires' => $expires]);
有时可能需要从用户中删除角色。要删除多对多关系记录,请使用 detach
方法。detach
方法将从中间表中删除相应的记录;但是,两个模型都将保留在数据库中。
// Detach a single role from the user...$user->roles()->detach($roleId); // Detach all roles from the user...$user->roles()->detach();
为方便起见,attach
和 detach
还接受 ID 数组作为输入。
$user = User::find(1); $user->roles()->detach([1, 2, 3]); $user->roles()->attach([ 1 => ['expires' => $expires], 2 => ['expires' => $expires],]);
同步关联
您还可以使用 sync
方法来构建多对多关联。sync
方法接受要放置在中间表上的 ID 数组。给定数组中不存在的任何 ID 将从中间表中删除。因此,在此操作完成后,中间表中仅存在给定数组中的 ID
$user->roles()->sync([1, 2, 3]);
您也可以通过 ID 传递额外的中间表值。
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果您想为每个同步的模型 ID 插入相同的中间表值,您可以使用 syncWithPivotValues
方法。
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
如果您不想分离给定数组中不存在的现有 ID,您可以使用 syncWithoutDetaching
方法。
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切换关联
多对多关系还提供了一个 toggle
方法,该方法可以“切换”给定相关模型 ID 的附加状态。如果给定的 ID 当前已附加,则会被分离。同样,如果当前已分离,则会被附加。
$user->roles()->toggle([1, 2, 3]);
您也可以通过 ID 传递额外的中间表值。
$user->roles()->toggle([ 1 => ['expires' => true], 2 => ['expires' => true],]);
更新中间表中的记录
如果您需要更新关系中间表中的现有行,可以使用 updateExistingPivot
方法。此方法接受中间记录外键和一个要更新的属性数组。
$user = User::find(1); $user->roles()->updateExistingPivot($roleId, [ 'active' => false,]);
更新父级时间戳
当一个模型定义与另一个模型(例如属于 Post
的 Comment
)的 belongsTo
或 belongsToMany
关系时,有时在子模型更新时更新父模型的时间戳会很有帮助。
例如,当 Comment
模型更新时,您可能希望自动“触摸”拥有它的 Post
的 updated_at
时间戳,以便将其设置为当前日期和时间。要实现这一点,您可以将一个 touches
属性添加到您的子模型,其中包含在子模型更新时应更新其 updated_at
时间戳的关系的名称。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Comment extends Model{ /** * All of the relationships to be touched. * * @var array */ protected $touches = ['post']; /** * Get the post that the comment belongs to. */ public function post(): BelongsTo { return $this->belongsTo(Post::class); }}
只有在使用 Eloquent 的 save
方法更新子模型时,父模型的时间戳才会被更新。