跳至内容

Eloquent:关系

简介

数据库表通常彼此相关。例如,一篇博文可能有多条评论,或者一个订单可能与下订单的用户相关联。Eloquent 使管理和处理这些关系变得容易,并支持各种常见的关系。

定义关系

Eloquent 关系在您的 Eloquent 模型类中定义为方法。由于关系也充当强大的 查询构建器,因此将关系定义为方法提供了强大的方法链和查询功能。例如,我们可以在此 posts 关系上链接其他查询约束。

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

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

一对一/HasOne

一对一关系是一种非常基本的数据库关系类型。例如,一个 User 模型可能与一个 Phone 模型关联。要定义此关系,我们将在 User 模型上放置一个 phone 方法。phone 方法应调用 hasOne 方法并返回其结果。hasOne 方法可通过模型的 Illuminate\Database\Eloquent\Model 基类提供给您的模型。

<?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 模型,该模型具有与 Phone 模型上的 user_id 列匹配的 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');
}

一对多/HasMany

一对多关系用于定义一个模型是另一个或多个子模型的父级的关系。例如,一篇博文可能拥有无限数量的评论。与所有其他 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();

一对多(反向)/BelongsTo

现在我们能够访问帖子中的所有评论了,让我们定义一个关系,以便评论能够访问其父帖子。为了定义 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 模型,该模型的 idComment 模型上的 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');
}

默认模型

belongsTohasOnehasOneThroughmorphOne 关系允许您定义一个默认模型,如果给定的关系为 null,则将返回该模型。这种模式通常被称为 空对象模式,它可以帮助您消除代码中的条件检查。在下面的示例中,如果 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';
});
}

查询 Belongs To 关系

当查询“属于”关系的子项时,您可以手动构建 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();
}

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

例如,使用 ofMany 方法,您可以检索用户的最高价订单。ofMany 方法接受可排序列作为其第一个参数,以及在查询相关模型时要应用的聚合函数(minmax)。

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

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

将“多”关系转换为 Has One 关系

通常,当使用 latestOfManyoldestOfManyofMany 方法检索单个模型时,您已经为同一个模型定义了“多对多”关系。为了方便起见,Laravel 允许您通过在关系上调用 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');
}

高级 Has One of Many 关系

可以构建更高级的“一对多”关系。例如,一个 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();

多对多关系

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

表结构

要定义此关系,需要三个数据库表:usersrolesrole_userrole_user 表派生自相关模型名称的字母顺序,并包含 user_idrole_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');

如果您希望您的中间表具有由 Eloquent 自动维护的created_atupdated_at时间戳,则在定义关系时调用withTimestamps方法。

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

利用 Eloquent 自动维护的时间戳的中间表需要同时具有created_atupdated_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;
}

通过中间表列过滤查询

您还可以使用wherePivotwherePivotInwherePivotNotInwherePivotBetweenwherePivotNotBetweenwherePivotNullwherePivotNotNull方法在定义关系时过滤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
{
// ...
}
exclamation

枢轴模型不能使用SoftDeletes特性。如果您需要软删除枢轴记录,请考虑将您的枢轴模型转换为实际的 Eloquent 模型。

自定义枢轴模型和自增 ID

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

/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;

多态关系

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

一对一(多态)

表结构

一对一多态关系类似于典型的一对一关系;但是,子模型可以使用单个关联属于多个类型的模型。例如,博客PostUser可以与Image模型共享多态关系。使用一对一多态关系允许您拥有一个唯一的图像表,该表可以与帖子和用户关联。首先,让我们检查一下表结构。

posts
id - integer
name - string
 
users
id - integer
name - string
 
images
id - integer
url - string
imageable_id - integer
imageable_type - string

请注意images表上的imageable_idimageable_type列。imageable_id列将包含帖子或用户的 ID 值,而imageable_type列将包含父模型的类名。imageable_type列由 Eloquent 用于确定在访问imageable关系时返回哪种“类型”的父模型。在这种情况下,该列将包含App\Models\PostApp\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关系将返回PostUser实例,具体取决于哪种类型的模型拥有该图像。

键约定

如有必要,您可以指定多态子模型使用的“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关系将返回PostVideo实例,具体取决于哪种类型的模型是评论的父级。

自动在子级上加载父级模型

即使使用 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 自动将父模型加载到其子模型上,则可以在定义 hasMany 关系时调用 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();
}

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

例如,使用ofMany方法,您可以检索用户最“喜欢”的图像。ofMany方法接受可排序列作为其第一个参数,以及在查询相关模型时应用的聚合函数(minmax)。

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

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

多对多(多态)

表结构

多对多多态关系比“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
lightbulb

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

模型结构

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

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模型上的postsvideos方法。

use App\Models\Tag;
 
$tag = Tag::find(1);
 
foreach ($tag->posts as $post) {
// ...
}
 
foreach ($tag->videos as $video) {
// ...
}

自定义多态类型

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

例如,我们可以使用简单的字符串(如postvideo)而不是使用模型名称作为“类型”。通过这样做,即使模型被重命名,数据库中的多态“类型”列值也将保持有效。

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

您可以在 App\Providers\AppServiceProvider 类的 boot 方法中调用 enforceMorphMap 方法,或者如果您愿意,可以创建一个单独的服务提供程序。

您可以使用模型的 getMorphClass 方法在运行时确定给定模型的多态别名。相反,您可以使用 Relation::getMorphedModel 方法确定与多态别名关联的完全限定类名。

use Illuminate\Database\Eloquent\Relations\Relation;
 
$alias = $post->getMorphClass();
 
$class = Relation::getMorphedModel($alias);
exclamation

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

动态关系

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

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

use App\Models\Order;
use App\Models\Customer;
 
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
exclamation

在定义动态关系时,始终为 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 posts
where 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 posts
where user_id = ? and (active = 1 or votes >= 100)

关系方法与动态属性

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

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

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

查询关系是否存在

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

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();

如果您需要更强大的功能,可以使用 whereHasorWhereHas 方法在 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();
exclamation

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

内联关系存在查询

如果您希望使用附加到关系查询的单个简单 where 条件查询关系的存在,您可能会发现使用 whereRelationorWhereRelationwhereMorphRelationorWhereMorphRelation 方法更方便。例如,我们可以查询所有具有未批准评论的帖子。

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

当然,就像对查询构建器的 where 方法的调用一样,您也可以指定运算符。

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

查询关系是否不存在

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

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

如果您需要更强大的功能,可以使用 whereDoesntHaveorWhereDoesntHave 方法向 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 关系

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

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();

有时您可能希望查询“多态关联”关系的父级的子级。您可以使用 whereMorphedTowhereNotMorphedTo 方法来实现这一点,这些方法会自动确定给定模型的正确多态类型映射。这些方法将 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 方法将在结果模型上放置一个 {relation}_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);
}])

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

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

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

其他聚合函数

除了 withCount 方法之外,Eloquent 还提供了 withMinwithMaxwithAvgwithSumwithExists 方法。这些方法将在结果模型上放置一个 {relation}_{function}_{column} 属性。

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();

如果您想预加载“多态关联”关系,以及该关系可能返回的各种实体的相关模型计数,则可以使用 with 方法结合 morphTo 关系的 morphWithCount 方法。

在此示例中,假设 PhotoPost 模型可以创建 ActivityFeed 模型。我们将假设 ActivityFeed 模型定义了一个名为 parentable 的“多态关联”关系,该关系允许我们为给定的 ActivityFeed 实例检索父 PhotoPost 模型。此外,让我们假设 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();
}
}

在这个例子中,让我们假设 EventPhotoPost 模型可以创建 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();
exclamation

使用此功能时,应始终在要检索的列列表中包含 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 将只预加载尚未隐藏的帖子和类型值为“教育”的视频。

使用关系存在约束预加载

你可能会发现自己需要检查关系的存在性,同时根据相同的条件加载关系。例如,你可能希望只检索具有与给定查询条件匹配的子 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();
}
}

在这个例子中,让我们假设 EventPhotoPost 模型可以创建 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 提供了方便的方法来将新模型添加到关系中。例如,也许你需要向帖子添加新评论。无需在 Comment 模型上手动设置 post_id 属性,可以使用关系的 save 方法插入评论

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.']),
]);

savesaveMany 方法将保存给定的模型实例,但不会将新保存的模型添加到已加载到父模型上的任何内存中关系中。如果你计划在使用 savesaveMany 方法后访问关系,则可能希望使用 refresh 方法重新加载模型及其关系

$post->comments()->save($comment);
 
$post->refresh();
 
// All comments, including the newly saved comment...
$post->comments;

递归保存模型和关系

如果你想保存你的模型及其所有关联关系,可以使用 push 方法。在这个例子中,Post 模型将被保存,以及它的评论和评论的作者

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

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

$post->pushQuietly();

create 方法

除了 savesaveMany 方法之外,还可以使用 create 方法,该方法接受一个属性数组,创建一个模型并将其插入数据库。savecreate 之间的区别在于,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.'],
]);

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

$user = User::find(1);
 
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
 
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);

还可以使用 findOrNewfirstOrNewfirstOrCreateupdateOrCreate 方法来 在关系上创建和更新模型

lightbulb

在使用 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();

为了方便起见,attachdetach 也接受 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,
]);

触碰父级时间戳

当模型定义到另一个模型的 belongsTobelongsToMany 关系时,例如属于 PostComment,有时在更新子模型时更新父模型的时间戳很有用。

例如,当 Comment 模型更新时,你可能希望自动“触碰”拥有 Postupdated_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);
}
}
exclamation

只有在使用 Eloquent 的 save 方法更新子模型时,才会更新父模型的时间戳。