跳转到内容

Eloquent:关系

介绍

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

定义关系

Eloquent 关系被定义为 Eloquent 模型类上的方法。由于关系也可以作为强大的查询构建器,因此将关系定义为方法可以提供强大的方法链式调用和查询能力。例如,我们可以在此 posts 关系上链接其他查询约束

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

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

一对一 / Has One

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
 
class User extends Model
{
/**
* Get the phone associated with the user.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}

传递给 hasOne 方法的第一个参数是相关模型类的名称。定义关系后,我们可以使用 Eloquent 的动态属性检索相关记录。动态属性允许您访问关系方法,就像它们是模型上定义的属性一样

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

Eloquent 根据父模型名称确定关系的外键。在本例中,自动假定 Phone 模型具有 user_id 外键。如果您想覆盖此约定,可以向 hasOne 方法传递第二个参数

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

此外,Eloquent 假定外键应具有与父级主键列匹配的值。换句话说,Eloquent 将在 Phone 记录的 user_id 列中查找用户的 id 列的值。如果您希望关系使用除 id 或模型的 $primaryKey 属性之外的主键值,可以向 hasOne 方法传递第三个参数

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

定义关系的逆关系

因此,我们可以从 User 模型访问 Phone 模型。接下来,让我们在 Phone 模型上定义一个关系,该关系将使我们能够访问拥有该电话的用户。我们可以使用 belongsTo 方法定义 hasOne 关系的逆关系

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

当调用 user 方法时,Eloquent 将尝试查找一个 User 模型,该模型的 idPhone 模型上的 user_id 列匹配。

Eloquent 通过检查关系方法的名称并在方法名称后附加 _id 来确定外键名称。因此,在本例中,Eloquent 假定 Phone 模型具有 user_id 列。但是,如果 Phone 模型上的外键不是 user_id,您可以将自定义键名作为第二个参数传递给 belongsTo 方法

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

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

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

一对多 / Has Many

一对多关系用于定义单个模型是父级对一个或多个子模型的关系。例如,一篇博客文章可能包含无限数量的评论。与所有其他 Eloquent 关系一样,通过在 Eloquent 模型上定义方法来定义一对多关系

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}

请记住,Eloquent 将自动确定 Comment 模型的正确外键列。按照惯例,Eloquent 将采用父模型的“蛇形命名法”名称并在其后附加 _id。因此,在本示例中,Eloquent 将假定 Comment 模型上的外键列为 post_id

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

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
// ...
}

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

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

hasOne 方法类似,您还可以通过向 hasMany 方法传递其他参数来覆盖外键和本地键

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

自动在子级上水合父模型

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

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}

在上面的示例中,引入了“N + 1”查询问题,因为即使为每个 Post 模型预加载了注释,Eloquent 也不会在每个子 Comment 模型上自动水合父 Post

如果你希望 Eloquent 自动将父模型的数据填充到其子模型上,可以在定义 hasMany 关系时调用 chaperone 方法。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
}

或者,如果你希望在运行时选择加入自动父级数据填充,可以在预加载关系时调用 chaperone 模型。

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

一对多(反向) / Belongs To

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

一旦关系被定义,我们可以通过访问 post “动态关系属性”来检索评论的父文章。

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

在上面的例子中,Eloquent 将尝试查找一个 Post 模型,其 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,则将返回该模型。这种模式通常被称为 Null Object 模式,可以帮助你消除代码中的条件检查。在下面的示例中,如果 Post 模型没有附加用户,则 user 关系将返回一个空的 App\Models\User 模型。

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}

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

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
 
/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}

查询属于的关系

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

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

但是,你可能会发现使用 whereBelongsTo 方法更方便,该方法会自动确定给定模型的正确关系和外键。

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

你还可以将一个 集合 实例提供给 whereBelongsTo 方法。这样做时,Laravel 将检索属于集合中任何父模型的模型。

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

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

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

Has One of Many

有时,一个模型可能有很多相关的模型,但你希望轻松检索关系的“最新”或“最旧”相关模型。例如,一个 User 模型可能与许多 Order 模型相关,但你希望定义一种方便的方式来与用户下的最新订单进行交互。你可以使用 hasOne 关系类型和 ofMany 方法来实现此目的。

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

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

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

默认情况下,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 列结合使用。

将“多”关系转换为一对一关系

通常,当使用 latestOfManyoldestOfManyofMany 方法检索单个模型时,你已经为同一模型定义了“has many”关系。为了方便起见,Laravel 允许你通过在关系上调用 one 方法来轻松地将此关系转换为“has one”关系。

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

高级一对多关系

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

因此,总而言之,我们需要检索最新的已发布定价,其中发布日期不是在未来。此外,如果两个价格具有相同的发布日期,我们将优先选择具有最大 ID 的价格。为了实现这一点,我们必须将包含确定最新价格的可排序列的数组传递给 ofMany 方法。此外,还将提供一个闭包作为 ofMany 方法的第二个参数。此闭包将负责向关系查询添加其他发布日期约束。

/**
* Get the current pricing for the product.
*/
public function currentPricing(): HasOne
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function (Builder $query) {
$query->where('published_at', '<', now());
});
}

Has One Through

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

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

mechanics
id - integer
name - string
 
cars
id - integer
model - string
mechanic_id - integer
 
owners
id - integer
name - string
car_id - integer

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
 
class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}

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

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

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

键约定

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

class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Foreign key on the cars table...
'car_id', // Foreign key on the owners table...
'id', // Local key on the mechanics table...
'id' // Local key on the cars table...
);
}
}

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

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

Has Many Through

“has-many-through”关系提供了一种通过中间关系访问远程关系的便捷方法。例如,假设我们正在构建一个像 Laravel Vapor 这样的部署平台。一个 Project 模型可以通过一个中间的 Environment 模型访问许多 Deployment 模型。使用此示例,你可以轻松收集给定项目的所有部署。让我们看一下定义此关系所需的表。

projects
id - integer
name - string
 
environments
id - integer
project_id - integer
name - string
 
deployments
id - integer
environment_id - integer
commit_hash - string

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
 
class Project extends Model
{
/**
* Get all of the deployments for the project.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}

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

或者,如果相关关系已经在关系中涉及的所有模型上定义,你可以通过调用 through 方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果 Project 模型具有 environments 关系,并且 Environment 模型具有 deployments 关系,你可以像这样定义一个连接项目和部署的“has-many-through”关系。

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

虽然 Deployment 模型的表不包含 project_id 列,但 hasManyThrough 关系允许通过 $project->deployments 访问项目的部署。要检索这些模型,Eloquent 会检查中间 Environment 模型的表上的 project_id 列。在找到相关的环境 ID 后,它们用于查询 Deployment 模型的表。

键约定

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

class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // Foreign key on the environments table...
'environment_id', // Foreign key on the deployments table...
'id', // Local key on the projects table...
'id' // Local key on the environments table...
);
}
}

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

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

多对多关系

多对多关系比 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');

如果您希望您的中间表具有 created_atupdated_at 时间戳,并且这些时间戳由 Eloquent 自动维护,请在定义关系时调用 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 模型。

一对一(多态)

表结构

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

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

请注意 images 表上的 imageable_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 自动将父模型水合到其子模型上,您可以在定义 morphMany 关系时调用 chaperone 方法。

class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}

或者,如果你希望在运行时选择加入自动父级数据填充,可以在预加载关系时调用 chaperone 模型。

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

多对一(多态)

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

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

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

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

默认情况下,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 模型都将包含一个调用基类 Eloquent 模型提供的 morphToMany 方法的 tags 方法。

morphToMany 方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称和它包含的键的名称,我们将把这种关系称为“taggable”。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Post extends Model
{
/**
* Get all of the tags for the post.
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}

定义关系的逆关系

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

morphedByMany 方法接受相关模型的名称以及“关系名称”。根据我们分配给中间表名称和它包含的键的名称,我们将把这种关系称为“taggable”。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Tag extends Model
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
 
/**
* Get all of the videos that are assigned this tag.
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}

检索关系

一旦你的数据库表和模型被定义好,你就可以通过你的模型访问这些关系。例如,要访问一个帖子的所有标签,你可以使用 tags 动态关系属性。

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

你可以通过访问执行 morphedByMany 调用的方法名,从多态子模型中检索多态关系的父级。在本例中,它是 Tag 模型上的 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 方法确定给定模型的 morph 别名。相反,你可以使用 Relation::getMorphedModel 方法确定与 morph 别名关联的完全限定的类名。

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

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

动态关系

你可以使用 resolveRelationUsing 方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议用于正常的应用程序开发,但在开发 Laravel 包时,这有时可能很有用。

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

use App\Models\Order;
use App\Models\Customer;
 
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
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)

关系方法 vs. 动态属性

如果你不需要向 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 关系

要查询“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();

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

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

你可以提供 * 作为通配符值,而不是传递可能的多个多态模型的数组。这将指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将执行额外的查询以执行此操作。

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

有时,你可能想计算给定关系的关联模型的数量,而无需实际加载这些模型。要实现此目的,你可以使用 withCount 方法。withCount 方法将在结果模型上放置一个 {关系}_count 属性。

use App\Models\Post;
 
$posts = Post::withCount('comments')->get();
 
foreach ($posts as $post) {
echo $post->comments_count;
}

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

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

你还可以为关系计数结果设置别名,从而允许在同一关系上进行多次计数。

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::withCount([
'comments',
'comments as pending_comments_count' => function (Builder $query) {
$query->where('approved', false);
},
])->get();
 
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

延迟计数加载

使用 loadCount 方法,你可以在父模型已被检索后加载关系计数。

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

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

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

关系计数和自定义 Select 语句

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

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

其他聚合函数

除了 withCount 方法外,Eloquent 还提供了 withMinwithMaxwithAvgwithSumwithExists 方法。这些方法将在你的结果模型上放置一个 {关系}_{函数}_{列} 属性。

use App\Models\Post;
 
$posts = Post::withSum('comments', 'votes')->get();
 
foreach ($posts as $post) {
echo $post->comments_sum_votes;
}

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

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

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

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

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

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

如果你想预加载一个“morph to”关系,以及该关系可能返回的各种实体的相关模型计数,你可以结合使用 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 将仅急切加载未隐藏的帖子和 type 值为“educational”的视频。

使用关系存在来约束急切加载

有时您可能会发现自己需要检查关系的存在,同时根据相同的条件加载关系。例如,您可能希望仅检索具有与给定查询条件匹配的子 Post 模型的 User 模型,同时还急切加载匹配的帖子。您可以使用 withWhereHas 方法来实现此目的。

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

延迟预加载

有时您可能需要在检索父模型后急切加载关系。例如,如果您需要动态决定是否加载相关模型,这可能很有用。

use App\Models\Book;
 
$books = Book::all();
 
if ($someCondition) {
$books->load('author', 'publisher');
}

如果您需要在急切加载查询中设置其他查询约束,则可以传递一个以要加载的关系为键的数组。数组值应为接收查询实例的闭包实例。

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

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

$book->loadMissing('author');

嵌套延迟急切加载和 morphTo

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

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

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}

在此示例中,假设 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 提供了方便的方法来向关系中添加新模型。例如,也许您需要向帖子添加新评论。您可以使用关系的 save 方法插入评论,而不是手动设置 Comment 模型上的 post_id 属性

use App\Models\Comment;
use App\Models\Post;
 
$comment = new Comment(['message' => 'A new comment.']);
 
$post = Post::find(1);
 
$post->comments()->save($comment);

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

如果您需要保存多个相关模型,可以使用 saveMany 方法。

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

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

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

递归保存模型和关系

如果您想 save 您的模型及其所有关联的关系,可以使用 push 方法。在此示例中,将保存 Post 模型以及其评论和评论的作者

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

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

$post->pushQuietly();

create 方法

除了 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,
]);

更新父级时间戳

当一个模型定义与另一个模型(例如属于 PostComment)的 belongsTobelongsToMany 关系时,有时在子模型更新时更新父模型的时间戳会很有帮助。

例如,当 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 方法更新子模型时,父模型的时间戳才会被更新。