Eloquent: 关联
简介
数据库表之间经常存在关联。例如,一篇博客文章可能有很多评论,或者一个订单可能与下订单的用户相关联。Eloquent 使管理和处理这些关联变得简单,并支持多种常见的关联类型
定义关联
Eloquent 关联被定义为 Eloquent 模型类中的方法。由于关联同时也作为强大的查询构建器,将关联定义为方法可以提供强大的方法链式调用和查询能力。例如,我们可以对这个 posts 关联链式添加额外的查询约束
1$user->posts()->where('active', 1)->get();
但是,在深入使用关联之前,让我们先学习如何定义 Eloquent 支持的每种关联类型。
一对一 / Has One
一对一关联是一种非常基础的数据库关联类型。例如,一个 User 模型可能与一个 Phone 模型关联。要定义此关联,我们将 phone 方法放在 User 模型中。phone 方法应该调用 hasOne 方法并返回其结果。hasOne 方法通过模型的 Illuminate\Database\Eloquent\Model 基类提供给您的模型
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(): HasOne14 {15 return $this->hasOne(Phone::class);16 }17}
传递给 hasOne 方法的第一个参数是关联模型类的名称。一旦定义了关联,我们就可以使用 Eloquent 的动态属性来检索关联记录。动态属性允许您像访问模型上定义的属性一样访问关联方法
1$phone = User::find(1)->phone;
Eloquent 根据父模型名称确定关联的外键。在这种情况下,Phone 模型被自动假定拥有一个 user_id 外键。如果您希望覆盖此约定,可以向 hasOne 方法传递第二个参数
1return $this->hasOne(Phone::class, 'foreign_key');
此外,Eloquent 假定外键的值应该与父模型的主键列匹配。换句话说,Eloquent 将在 Phone 记录的 user_id 列中查找用户 id 列的值。如果您希望关联使用除 id 或您模型主键之外的主键值,您可以向 hasOne 方法传递第三个参数
1return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定义关联的反向
这样,我们就可以从 User 模型访问 Phone 模型了。接下来,让我们在 Phone 模型上定义一个关联,使我们能够访问拥有该手机的用户。我们可以使用 belongsTo 方法定义 hasOne 关联的反向
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(): BelongsTo14 {15 return $this->belongsTo(User::class);16 }17}
调用 user 方法时,Eloquent 将尝试查找一个 id 与 Phone 模型上的 user_id 列匹配的 User 模型。
Eloquent 通过检查关联方法的名称并将方法名称后缀 _id 来确定外键名称。因此,在这种情况下,Eloquent 假定 Phone 模型具有 user_id 列。但是,如果 Phone 模型上的外键不是 user_id,您可以将自定义键名作为第二个参数传递给 belongsTo 方法
1/**2 * Get the user that owns the phone.3 */4public function user(): BelongsTo5{6 return $this->belongsTo(User::class, 'foreign_key');7}
如果父模型不使用 id 作为其主键,或者您希望使用不同的列来查找关联模型,您可以向 belongsTo 方法传递第三个参数,指定父表的自定义键
1/**2 * Get the user that owns the phone.3 */4public function user(): BelongsTo5{6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');7}
一对多 / Has Many
一对多关联用于定义单个模型作为零个或多个子模型的父模型的关联。例如,一篇博客文章可以有无数条评论。像所有其他 Eloquent 关联一样,一对多关联是通过在您的 Eloquent 模型中定义一个方法来定义的
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(): HasMany14 {15 return $this->hasMany(Comment::class);16 }17}
请记住,Eloquent 会自动为 Comment 模型确定正确的外键列。按照惯例,Eloquent 会取父模型的“蛇形命名法”名称并加上 _id 后缀。因此,在此示例中,Eloquent 将假定 Comment 模型上的外键列是 post_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 也不会自动在每个子 Comment 模型上填充父 Post。
如果您希望 Eloquent 自动在子模型上填充父模型,您可以在定义 hasMany 关联时调用 chaperone 方法
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(): HasMany14 {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();
一对多(反向) / Belongs To
现在我们可以访问帖子的所有评论了,让我们定义一个关联,允许评论访问其父帖子。要定义 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(): BelongsTo14 {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 将尝试查找一个 id 与 Comment 模型上的 post_id 列匹配的 Post 模型。
Eloquent 通过检查关联方法的名称并加上 _ 后跟父模型主键列名称的后缀来确定默认外键名称。因此,在此示例中,Eloquent 将假定 comments 表上的 Post 模型的外键是 post_id。
但是,如果您的关联外键不遵循这些约定,您可以将自定义外键名称作为第二个参数传递给 belongsTo 方法
1/**2 * Get the post that owns the comment.3 */4public function post(): BelongsTo5{6 return $this->belongsTo(Post::class, 'foreign_key');7}
如果您的父模型不使用 id 作为其主键,或者您希望使用不同的列来查找关联模型,您可以向 belongsTo 方法传递第三个参数,指定父表的自定义键
1/**2 * Get the post that owns the comment.3 */4public function post(): BelongsTo5{6 return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');7}
默认模型
belongsTo、hasOne、hasOneThrough 和 morphOne 关联允许您定义一个默认模型,如果给定的关联为 null,则返回该模型。这种模式通常被称为 空对象模式,可以帮助消除代码中的条件检查。在下面的示例中,如果 Post 模型没有附加用户,user 关联将返回一个空的 App\Models\User 模型
1/**2 * Get the author of the post.3 */4public function user(): BelongsTo5{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(): BelongsTo15{16 return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {17 $user->name = 'Guest Author';18 });19}
查询 Belongs To 关联
在查询“belongs to”关联的子项时,您可以手动构建 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();
Has One of Many
有时一个模型可能有许多关联模型,但您想轻松检索关联的“最新”或“最旧”的关联模型。例如,User 模型可能与许多 Order 模型相关联,但您想定义一种便捷的方式来与用户下的最新订单进行交互。您可以使用 hasOne 关联类型结合 ofMany 方法来实现这一点
1/**2 * Get the user's most recent order.3 */4public function latestOrder(): HasOne5{6 return $this->hasOne(Order::class)->latestOfMany();7}
同样,您可以定义一个方法来检索关联的“最旧”或第一个关联模型
1/**2 * Get the user's oldest order.3 */4public function oldestOrder(): HasOne5{6 return $this->hasOne(Order::class)->oldestOfMany();7}
默认情况下,latestOfMany 和 oldestOfMany 方法将根据模型的主键(必须是可排序的)检索最新或最旧的关联模型。但是,有时您可能希望使用不同的排序标准从较大的关联中检索单个模型。
例如,使用 ofMany 方法,您可以检索用户最昂贵的订单。ofMany 方法接受可排序的列作为其第一个参数,以及在查询关联模型时应用的聚合函数(min 或 max)
1/**2 * Get the user's largest order.3 */4public function largestOrder(): HasOne5{6 return $this->hasOne(Order::class)->ofMany('price', 'max');7}
由于 PostgreSQL 不支持对 UUID 列执行 MAX 函数,目前无法将 one-of-many 关联与 PostgreSQL UUID 列结合使用。
将“多”关联转换为 Has One 关联
通常,在使用 latestOfMany、oldestOfMany 或 ofMany 方法检索单个模型时,您已经为同一个模型定义了一个“has many”关联。为了方便起见,Laravel 允许您通过对关联调用 one 方法轻松地将此关联转换为“has 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(): HasOne13{14 return $this->orders()->one()->ofMany('price', 'max');15}
您也可以使用 one 方法将 HasManyThrough 关联转换为 HasOneThrough 关联
1public function latestDeployment(): HasOneThrough2{3 return $this->deployments()->one()->latestOfMany();4}
高级 Has One of Many 关联
可以构建更高级的“has one of many”关联。例如,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
“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 910owners11 id - integer12 name - string13 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(): HasOneThrough14 {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
“has-many-through”关联提供了一种通过中间关联访问远端关联的便捷方式。例如,假设我们正在构建一个像 Laravel Cloud 这样的部署平台。Application 模型可以通过中间的 Environment 模型访问许多 Deployment 模型。使用此示例,您可以轻松地为给定的应用程序收集所有部署。让我们看看定义此关联所需的表
1applications 2 id - integer 3 name - string 4 5environments 6 id - integer 7 application_id - integer 8 name - string 910deployments11 id - integer12 environment_id - integer13 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(): HasManyThrough14 {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 会检查中间 Environment 模型表上的 application_id 列。找到相关的环境 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 方法并提供这些关联的名称来流畅地定义“has-many-through”关联。这种方法提供了重用现有关联上已定义的关键约定的优势
1// String based syntax...2return $this->through('environments')->has('deployments');3 4// Dynamic syntax...5return $this->throughEnvironments()->hasDeployments();
作用域关联
在模型中添加限制关联的额外方法是很常见的。例如,您可以在 User 模型中添加一个 featuredPosts 方法,该方法通过额外的 where 约束来限制更广泛的 posts 关联
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(): HasMany14 {15 return $this->hasMany(Post::class)->latest();16 }17 18 /**19 * Get the user's featured posts.20 */21 public function featuredPosts(): HasMany22 {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(): HasMany5{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 关联稍微复杂一些。多对多关联的一个例子是一个用户拥有多个角色,而这些角色也被应用程序中的其他用户共享。例如,一个用户可能被分配了“作者”和“编辑”角色;但是,这些角色也可能被分配给其他用户。因此,一个用户有多个角色,一个角色有多个用户。
表结构
要定义此关联,需要三个数据库表:users、roles 和 role_user。role_user 表派生自相关模型名称的字母顺序,并包含 user_id 和 role_id 列。该表用作连接用户和角色的中间表。
请记住,由于一个角色可以属于多个用户,我们不能简单地在 roles 表上放置一个 user_id 列。这意味着一个角色只能属于一个用户。为了支持将角色分配给多个用户,需要 role_user 表。我们可以这样总结关联的表结构
1users 2 id - integer 3 name - string 4 5roles 6 id - integer 7 name - string 8 9role_user10 user_id - integer11 role_id - integer
模型结构
多对多关联是通过编写返回 belongsToMany 方法结果的方法来定义的。belongsToMany 方法由您的应用程序中所有 Eloquent 模型使用的 Illuminate\Database\Eloquent\Model 基类提供。例如,让我们在 User 模型中定义一个 roles 方法。传递给此方法的第一个参数是关联模型类的名称
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(): BelongsToMany14 {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 方法的结果。为了完成我们的用户/角色示例,让我们在 Role 模型中定义 users 方法
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(): BelongsToMany14 {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_at 时间戳,请在定义关联时调用 withTimestamps 方法
1return $this->belongsToMany(Role::class)->withTimestamps();
使用 Eloquent 自动维护时间戳的中间表必须同时具有 created_at 和 updated_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}
通过中间表列过滤查询
您还可以在定义关联时使用 wherePivot、wherePivotIn、wherePivotNotIn、wherePivotBetween、wherePivotNotBetween、wherePivotNull 和 wherePivotNotNull 方法来过滤 belongsToMany 关联查询返回的结果
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 子句约束,但在通过定义的关联创建新模型时不会添加指定的值。如果您需要使用特定的 pivot 值来查询和创建关联,您可以使用 withPivotValue 方法
1return $this->belongsToMany(Role::class)2 ->withPivotValue('approved', 1);
通过中间表列排序查询
您可以使用 orderByPivot 和 orderByPivotDesc 方法对 belongsToMany 关联查询返回的结果进行排序。在下面的示例中,我们将检索用户的所有最新徽章
1return $this->belongsToMany(Badge::class)2 ->where('rank', 'gold')3 ->orderByPivotDesc('created_at');
定义自定义中间表模型
如果您想定义一个自定义模型来表示多对多关联的中间表,您可以在定义关联时调用 using 方法。自定义 pivot 模型使您有机会在 pivot 模型上定义额外的行为,例如方法和转换(casts)。
自定义多对多 pivot 模型应扩展 Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多态多对多 pivot 模型应扩展 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们可以定义一个使用自定义 RoleUser pivot 模型的 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(): BelongsToMany14 {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}
Pivot 模型不能使用 SoftDeletes 特性。如果您需要软删除 pivot 记录,请考虑将您的 pivot 模型转换为实际的 Eloquent 模型。
自定义 Pivot 模型和自增 ID
如果您定义了一个使用自定义 pivot 模型的多对多关联,并且该 pivot 模型具有自增主键,则应确保您的自定义 pivot 模型类使用 Table 属性并将 incrementing 设置为 true
1use Illuminate\Database\Eloquent\Attributes\Table;2use Illuminate\Database\Eloquent\Relations\Pivot;3 4#[Table(incrementing: true)]5class RoleUser extends Pivot6{7 // ...8}
多态关联
多态关联允许子模型使用单个关联属于不止一种类型的模型。例如,想象一下您正在构建一个允许用户分享博客文章和视频的应用程序。在这种应用程序中,Comment 模型可能同时属于 Post 和 Video 模型。
一对一(多态)
表结构
一对一多态关联类似于典型的一对一关联;但是,子模型可以使用单个关联属于不止一种类型的模型。例如,博客 Post 和 User 可以共享与 Image 模型的 多态 关联。使用一对一多态关联,您可以拥有一个唯一的图片表,该表可以与帖子和用户关联。首先,让我们检查表结构
1posts 2 id - integer 3 name - string 4 5users 6 id - integer 7 name - string 8 9images10 id - integer11 url - string12 imageable_id - integer13 imageable_type - string
注意 images 表上的 imageable_id 和 imageable_type 列。imageable_id 列将包含帖子或用户的 ID 值,而 imageable_type 列将包含父模型的类名。imageable_type 列被 Eloquent 用于确定在访问 imageable 关联时要返回哪种“类型”的父模型。在这种情况下,该列将包含 App\Models\Post 或 App\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(): MorphTo14 {15 return $this->morphTo();16 }17}18 19use Illuminate\Database\Eloquent\Model;20use Illuminate\Database\Eloquent\Relations\MorphOne;21 22class Post extends Model23{24 /**25 * Get the post's image.26 */27 public function image(): MorphOne28 {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 Model37{38 /**39 * Get the user's image.40 */41 public function image(): MorphOne42 {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 调用的方法的名称来检索多态模型的父级。在这种情况下,那是 Image 模型上的 imageable 方法。因此,我们将该方法作为动态关联属性进行访问
1use App\Models\Image;2 3$image = Image::find(1);4 5$imageable = $image->imageable;
Image 模型上的 imageable 关联将返回 Post 或 User 实例,具体取决于哪种类型的模型拥有该图片。
关键约定
如有必要,您可以指定多态子模型使用的“id”和“type”列的名称。如果这样做,请确保始终将关联的名称作为第一个参数传递给 morphTo 方法。通常,此值应与方法名称匹配,因此您可以使用 PHP 的 __FUNCTION__ 常量
1/**2 * Get the model that the image belongs to.3 */4public function imageable(): MorphTo5{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 - string1011comments12 id - integer13 body - text14 commentable_id - integer15 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(): MorphTo14 {15 return $this->morphTo();16 }17}18 19use Illuminate\Database\Eloquent\Model;20use Illuminate\Database\Eloquent\Relations\MorphMany;21 22class Post extends Model23{24 /**25 * Get all of the post's comments.26 */27 public function comments(): MorphMany28 {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 Model37{38 /**39 * Get all of the video's comments.40 */41 public function comments(): MorphMany42 {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 调用的方法的名称来检索多态子模型的父级。在这种情况下,那是 Comment 模型上的 commentable 方法。因此,我们将该方法作为动态关联属性进行访问,以便访问评论的父模型
1use App\Models\Comment;2 3$comment = Comment::find(1);4 5$commentable = $comment->commentable;
Comment 模型上的 commentable 关联将返回 Post 或 Video 实例,具体取决于哪种类型的模型是评论的父级。
自动在子模型上填充父模型
即使在使用 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 也不会自动在每个子 Comment 模型上填充父 Post。
如果您希望 Eloquent 自动在子模型上填充父模型,您可以在定义 morphMany 关联时调用 chaperone 方法
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();
One of Many(多态)
有时一个模型可能有许多关联模型,但您想轻松检索关联的“最新”或“最旧”的关联模型。例如,User 模型可能与许多 Image 模型相关联,但您想定义一种便捷的方式来与用户上传的最新图片进行交互。您可以使用 morphOne 关联类型结合 ofMany 方法来实现这一点
1/**2 * Get the user's most recent image.3 */4public function latestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->latestOfMany();7}
同样,您可以定义一个方法来检索关联的“最旧”或第一个关联模型
1/**2 * Get the user's oldest image.3 */4public function oldestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->oldestOfMany();7}
默认情况下,latestOfMany 和 oldestOfMany 方法将根据模型的主键(必须是可排序的)检索最新或最旧的关联模型。但是,有时您可能希望使用不同的排序标准从较大的关联中检索单个模型。
例如,使用 ofMany 方法,您可以检索用户最“喜欢”的图片。ofMany 方法接受可排序的列作为其第一个参数,以及在查询关联模型时应用的聚合函数(min 或 max)
1/**2 * Get the user's most popular image.3 */4public function bestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');7}
可以构建更高级的“one of many”关联。有关更多信息,请查阅 has one of many 文档。
多对多(多态)
表结构
多对多多态关联比“morph one”和“morph many”关联稍微复杂一些。例如,Post 模型和 Video 模型可以共享与 Tag 模型的 多态 关联。在这种情况下使用多对多多态关联将允许您的应用程序拥有一个唯一的标签表,该表可以与帖子或视频关联。首先,让我们检查构建此关联所需的表结构
1posts 2 id - integer 3 name - string 4 5videos 6 id - integer 7 name - string 8 9tags10 id - integer11 name - string1213taggables14 tag_id - integer15 taggable_id - integer16 taggable_type - string
在深入了解多态多对多关联之前,阅读有关典型 多对多关联 的文档可能会对您有所帮助。
模型结构
接下来,我们准备在模型上定义关联。Post 和 Video 模型都将包含一个调用基 Eloquent 模型类提供的 morphToMany 方法的 tags 方法。
morphToMany 方法接受关联模型的名称以及“关联名称”。基于我们分配给中间表名称及其包含的键,我们将此关联称为“taggable”
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(): MorphToMany14 {15 return $this->morphToMany(Tag::class, 'taggable');16 }17}
定义关联的反向
接下来,在 Tag 模型上,您应该为每个可能的父模型定义一个方法。因此,在此示例中,我们将定义一个 posts 方法和一个 videos 方法。这两个方法都应该返回 morphedByMany 方法的结果。
morphedByMany 方法接受关联模型的名称以及“关联名称”。基于我们分配给中间表名称及其包含的键,我们将此关联称为“taggable”
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(): MorphToMany14 {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(): MorphToMany22 {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 调用的方法的名称从多态子模型中检索多态关联的父级。在这种情况下,那是 Tag 模型上的 posts 或 videos 方法
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 模型可能属于 Post 或 Video 模型),默认的 commentable_type 将分别是 App\Models\Post 或 App\Models\Video。但是,您可能希望将这些值与应用程序的内部结构解耦。
例如,我们不使用模型名称作为“类型”,而是使用简单的字符串,例如 post 和 video。通过这样做,即使模型被重命名,我们数据库中的多态“类型”列值也将保持有效
1use Illuminate\Database\Eloquent\Relations\Relation;2 3Relation::enforceMorphMap([4 'post' => 'App\Models\Post',5 'video' => 'App\Models\Video',6]);
您可以在 App\Providers\AppServiceProvider 类的 boot 方法中调用 enforceMorphMap 方法,或者根据需要创建一个单独的服务提供者。
您可以在运行时使用模型的 getMorphClass 方法确定给定模型的多态别名。相反,您可以使用 Relation::getMorphedModel 方法确定与多态别名关联的完全限定类名
1use Illuminate\Database\Eloquent\Relations\Relation;2 3$alias = $post->getMorphClass();4 5$class = Relation::getMorphedModel($alias);
向现有应用程序添加“morph map”时,数据库中每个仍包含完全限定类名的多态 *_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(): HasMany14 {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 posts3where 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 posts3where user_id = ? and (active = 1 or votes >= 100)
关联方法 vs 动态属性
如果您不需要向 Eloquent 关联查询添加额外的约束,您可以像访问属性一样访问关联。例如,继续使用我们的 User 和 Post 示例模型,我们可以这样访问用户的所有帖子
1use App\Models\User;2 3$user = User::find(1);4 5foreach ($user->posts as $post) {6 // ...7}
动态关联属性执行“延迟加载”,这意味着它们只会在您实际访问它们时加载其关联数据。因此,开发人员经常使用 渴求式加载 来预加载他们知道在加载模型后会被访问的关联。渴求式加载显着减少了加载模型关联必须执行的 SQL 查询数量。
查询关联是否存在
检索模型记录时,您可能希望根据关联的存在与否来限制结果。例如,想象您想要检索所有至少有一条评论的博客文章。为此,您可以将关联名称传递给 has 和 orHas 方法
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();
如果您需要更强大的功能,可以使用 whereHas 和 orWhereHas 方法在 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 条件,您可能会发现使用 whereRelation、orWhereRelation、whereMorphRelation 和 orWhereMorphRelation 方法更方便。例如,我们可以查询所有有未批准评论的帖子
1use App\Models\Post;2 3$posts = Post::whereRelation('comments', 'is_approved', false)->get();
当然,就像调用查询构建器的 where 方法一样,您也可以指定一个操作符
1$posts = Post::whereRelation(2 'comments', 'created_at', '>=', now()->minus(hours: 1)3)->get();
查询关联是否缺失
检索模型记录时,您可能希望根据关联的缺失来限制结果。例如,想象您想要检索所有 没有 任何评论的博客文章。为此,您可以将关联名称传递给 doesntHave 和 orDoesntHave 方法
1use App\Models\Post;2 3$posts = Post::doesntHave('comments')->get();
如果您需要更强大的功能,可以使用 whereDoesntHave 和 orWhereDoesntHave 方法向您的 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 关联
要查询“morph to”关联的存在性,可以使用 whereHasMorph 和 whereDoesntHaveMorph 方法。这些方法接受关联名称作为它们的第一个参数。接下来,这些方法接受您希望包含在查询中的相关模型的名称。最后,您可以提供一个自定义关联查询的闭包
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();
有时您可能想查询“morph to”关联的父级的子级。您可以使用 whereMorphedTo 和 whereNotMorphedTo 方法来实现,它们将自动为给定模型确定正确的 morph 类型映射。这些方法接受 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}])
关联计数和自定义 Select 语句
如果您将 withCount 与 select 语句组合使用,请确保在 select 方法之后调用 withCount
1$posts = Post::select(['title', 'body'])2 ->withCount('comments')3 ->get();
其他聚合函数
除了 withCount 方法外,Eloquent 还提供了 withMin、withMax、withAvg、withSum 和 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();
在 Morph To 关联上统计关联模型
如果您希望渴求式加载“morph to”关联,以及该关联可能返回的各种实体的关联模型计数,您可以结合使用 with 方法和 morphTo 关联的 morphWithCount 方法。
在此示例中,让我们假设 Photo 和 Post 模型可以创建 ActivityFeed 模型。我们将假定 ActivityFeed 模型定义了一个名为 parentable 的“morph to”关联,它允许我们为给定的 ActivityFeed 实例检索父级 Photo 或 Post 模型。此外,让我们假设 Photo 模型“has many” Tag 模型,而 Post 模型“has many” Comment 模型。
现在,让我们想象我们要检索 ActivityFeed 实例并为每个 ActivityFeed 实例渴求式加载 parentable 父模型。此外,我们要检索与每个父级照片关联的标签数量以及与每个父级帖子关联的评论数量
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 模型加载嵌套关联计数。您可以使用 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 查询问题,请考虑一个“belongs to”一个 Author 模型的 Book 模型
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(): BelongsTo14 {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 books2 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 方法结合 morphTo 关联的 morphWith 方法。为了帮助说明此方法,让我们考虑以下模型
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(): MorphTo12 {13 return $this->morphTo();14 }15}
在此示例中,让我们假设 Event、Photo 和 Post 模型可以创建 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 array14 */15 protected $with = ['author'];16 17 /**18 * Get the author that wrote the book.19 */20 public function author(): BelongsTo21 {22 return $this->belongsTo(Author::class);23 }24 25 /**26 * Get the genre of the book.27 */28 public function genre(): BelongsTo29 {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;2 3$users = User::with(['posts' => function ($query) {4 $query->where('title', 'like', '%code%');5}])->get();
在此示例中,Eloquent 将仅渴求式加载帖子 title 列包含单词 code 的帖子。您可以调用其他 查询构建器 方法来进一步自定义渴求式加载操作
1$users = User::with(['posts' => function ($query) {2 $query->orderBy('created_at', 'desc');3}])->get();
约束 morphTo 关联的渴求式加载
如果您正在渴求式加载 morphTo 关联,Eloquent 将运行多个查询来获取每种类型的相关模型。您可以使用 MorphTo 关联的 constrain 方法向这些查询中的每一个添加额外的约束
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”的视频。
通过关联存在性约束渴求式加载
有时您可能会发现自己需要检查关联的存在性,同时根据相同的条件加载该关联。例如,您可能希望仅检索具有与给定查询条件匹配的子 Post 模型的 User 模型,同时也渴求式加载匹配的帖子。您可以使用 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 ($condition) {6 $books->load('author', 'publisher');7}
如果您需要在渴求式加载查询上设置额外的查询约束,您可以传递一个以您想要加载的关联为键的数组。数组值应该是接收查询实例的闭包实例
1$author->load(['books' => function ($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(): MorphTo12 {13 return $this->morphTo();14 }15}
在此示例中,让我们假设 Event、Photo 和 Post 模型可以创建 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 可以自动渴求式加载您访问的关联。要启用自动渴求式加载,您应该在应用程序 AppServiceProvider 的 boot 方法中调用 Model::automaticallyEagerLoadRelationships 方法
1use Illuminate\Database\Eloquent\Model;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{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 总是防止关联的延迟加载。为此,您可以调用基 Eloquent 模型类提供的 preventLazyLoading 方法。通常,您应该在应用程序 AppServiceProvider 类的 boot 方法中调用此方法。
preventLazyLoading 方法接受一个可选的布尔参数,该参数指示是否应防止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,以便您的生产环境即使在意外存在延迟加载的关联时也能正常运行
1use Illuminate\Database\Eloquent\Model;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Model::preventLazyLoading(! $this->app->isProduction());9}
防止延迟加载后,当您的应用程序尝试延迟加载任何 Eloquent 关联时,Eloquent 将抛出 Illuminate\Database\LazyLoadingViolationException 异常。
您可以使用 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 提供了向关联添加新模型的便捷方法。例如,也许您需要向帖子添加新评论。您可以插入评论,而不是手动在 Comment 模型上设置 post_id 属性,使用关联的 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]);
save 和 saveMany 方法将持久化给定的模型实例,但不会将新持久化的模型添加到已加载到父模型上的任何内存中关联中。如果您计划在使用 save 或 saveMany 方法后访问该关联,您可能希望使用 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 方法
除了 save 和 saveMany 方法外,您还可以使用 create 方法,它接受属性数组,创建一个模型,并将其插入数据库。save 和 create 之间的区别在于,save 接受完整的 Eloquent 模型实例,而 create 接受纯 PHP 数组。新创建的模型将由 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]);
createQuietly 和 createManyQuietly 方法可用于创建模型,而不分发任何事件
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、firstOrNew、firstOrCreate 和 updateOrCreate 方法来 创建和更新关联上的模型。
在使用 create 方法之前,请务必查看 批量赋值 文档。
Belongs To 关联
如果您想将子模型分配给新的父模型,可以使用 associate 方法。在此示例中,User 模型定义了对 Account 模型的 belongsTo 关联。此 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();
为了方便起见,attach 和 detach 也接受 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]);
触发父级时间戳更新
当模型定义了对另一个模型的 belongsTo 或 belongsToMany 关联时(例如属于 Post 的 Comment),有时在更新子模型时更新父级的时间戳会很有帮助。
例如,更新 Comment 模型时,您可能希望自动“触摸”拥有它的 Post 的 updated_at 时间戳,以便将其设置为当前日期和时间。为此,您可以在子模型上使用 Touches 属性,其中包含在更新子模型时应更新其 updated_at 时间戳的关联名称
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Attributes\Touches; 6use Illuminate\Database\Eloquent\Model; 7use Illuminate\Database\Eloquent\Relations\BelongsTo; 8 9#[Touches(['post'])]10class Comment extends Model11{12 /**13 * Get the post that the comment belongs to.14 */15 public function post(): BelongsTo16 {17 return $this->belongsTo(Post::class);18 }19}
仅当使用 Eloquent 的 save 方法更新子模型时,才会更新父模型时间戳。