数据库表之间常常存在关联。例如,一篇博客文章可以有许多评论,或者一个订单可能与下订单的用户相关联。Eloquent 使管理和使用这些关联变得容易,并且支持各种常见的关联类型:
Eloquent 关联在你的 Eloquent 模型类上被定义为方法。由于关联也充当着强大的查询构建器,将关联定义为方法提供了强大的方法链式调用和查询能力。例如,我们可以在这个posts关联上链式调用额外的查询约束:
$user->posts()->where('active', 1)->get();但是,在深入了解如何使用关联关系之前,让我们先学习如何定义 Eloquent 支持的每种关联关系。
一对一关系是一种非常基本的数据库关系类型。 例如, 一个 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 将查找用户 id 列的值在 Phone 记录的 user_id 列中。如果你希望该关系使用除 id 之外的主键值或模型的 $primaryKey 属性,你可以传递第三个参数给 hasOne 方法:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');因此,我们可以从我们的User 模型访问Phone 模型。接下来,让我们在Phone 模型上定义一个关系,以便我们能够访问拥有该手机的用户。我们可以使用belongsTo 方法来定义hasOne 关系的逆向关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}调用 user 方法时,Eloquent 将尝试查找一个 User 模型其 id 与 Phone 模型上的 user_id 列相匹配。
Eloquent 通过检查关系方法的名称,并在方法名称后加上 _id 后缀来确定外键名称。因此,在这种情况下,Eloquent 假定 Phone 模型有一个 user_id 列。但是,如果 Phone 模型上的外键不是 user_id,你可以将自定义键名作为第二个参数传递给 belongsTo 方法:
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}如果父模型不使用 id 作为其主键,或者你希望使用不同的列来查找关联模型,你可以向 belongsTo 方法传入第三个参数,指定父表的自定义键:
/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}一对多关系用于定义其中一个模型是其一个或多个子模型的父级的关系。例如,一篇博客文章可能拥有无限数量的评论。与所有其他 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 eager loading, "N + 1" 查询问题可能会产生 如果您在遍历子模型时尝试从子模型访问父模型:
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}在上述示例中,引入了一个“N + 1”查询问题,因为,尽管评论已为每个 Post 模型进行了预加载,Eloquent 并不会自动将父级 Post 填充到每个子级 Comment 模型上。
如果你希望 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();既然我们现在可以访问帖子的所有评论,让我们定义一个关系,允许评论访问其父级帖子。要定义 hasMany 关系的逆向关系,请在子模型上定义一个关系方法,该方法调用 belongsTo 方法:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}一旦关系被定义,我们就可以通过访问 post “动态关系属性”来获取评论的父帖子:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;在上面的例子中,Eloquent将尝试查找一个Post模型,该模型具有一个id,其与Comment模型上的post_id列匹配。
Eloquent 通过检查关系方法的名称,并在方法名后附加一个 _,然后是父模型主键列的名称,来确定默认外键名。因此,在这个例子中,Eloquent 将会假设 comments 表上 Post 模型的外键是 post_id。
然而,如果您的关联关系的外键不遵循这些约定,您可以将自定义的外键名称作为第二个参数传递给 belongsTo 方法:
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}如果你的父模型没有使用id作为其主键,或者你希望通过不同的列来查找关联模型,你可以向belongsTo方法传递第三个参数,以指定你的父表的自定义键:
/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}这些 belongsTo, hasOne, hasOneThrough, 和 morphOne 关系允许您定义一个默认模型,该模型将在给定关系为 null 时返回。这种模式通常被称为 空对象模式 和 可以帮助您消除代码中的条件检查。在以下示例中,该 user 关系将返回一个空的 App\Models\User 模型 如果没有用户附加到 Post 模型:
/**
* 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();`You are welcome to visit our website www.fuerfuk.cn. This Privacy Policy explains how Fuefuku (the “Company,” “we,” or “us”) collects, uses, and discloses information about you when you use our website or services.
$users = User::where('vip', true)->get();
$posts = Post::whereBelongsTo($users)->get();默认情况下,Laravel 会根据模型的类名确定与给定模型关联的关系;但是,你可以通过将其作为第二个参数提供给 whereBelongsTo 方法来手动指定关系名称:
$posts = Post::whereBelongsTo($user, 'author')->get();有时,一个模型可能有许多关联模型,但你想要轻松检索关系中“最新”或“最旧”的关联模型。例如,一个 User 模型可能关联了许多 Order 模型,但你想要定义一种方便的方式来与用户下的最新订单进行交互。你可以使用 hasOne 关系类型结合 ofMany 方法来完成此操作:
/**
* Get the user's most recent order.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}同样地,您可以定义一个方法来检索某个关系中“最旧的”,或者说第一个,相关模型:
/**
* Get the user's oldest order.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}默认情况下,latestOfMany 和 oldestOfMany 方法将基于模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。然而,有时您可能希望使用不同的排序标准从更大的关联关系中检索单个模型。
例如,使用 ofMany 方法,你可以检索用户最昂贵的订单。 ofMany 方法接受可排序列作为其第一个参数,以及在查询相关模型时要应用的聚合函数(min 或 max):
/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}[!WARNING]
由于 PostgreSQL 不支持对 UUID 列执行MAX函数,目前无法将一对多关系与 PostgreSQL UUID 列结合使用。
通常,当使用 latestOfMany, oldestOfMany, 或 ofMany 方法检索单个模型时,你可能已经为同一个模型定义了一个“has many”关系。为了方便,Laravel 允许你通过在该关系上调用 one 方法,轻松地将此关系转换为“has one”关系:
/**
* Get the user's orders.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}您也可以使用 one 方法将 HasManyThrough 关系转换为 HasOneThrough 关系:
public function latestDeployment(): HasOneThrough
{
return $this->deployments()->one()->latestOfMany();
}可以构建更高级的“多项之一”关系。例如,一个 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 关系定义了一个与另一个模型的一对一关系。然而,这种关系表明声明模型可以与另一个模型的一个实例进行 经由 第三个模型。
例如,在车辆维修店应用程序中,每个 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 方法并提供这些关系的名称。例如,如果 Mechanic 模型有一个 cars 关系,并且 Car 模型有一个 owner 关系,你可以定义一个"一对一远程"关系,连接技工和所有者,如下所示:
// 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" 关系提供了一种通过中间关联访问远层关联的便捷方式。例如,假设我们正在构建一个部署平台,比如 Laravel Cloud。一个 Application 模型可能通过一个中间 Environment 模型访问多个 Deployment 模型。使用这个例子,你可以轻松地收集给定应用的所有部署。让我们看看定义这种关系所需的表:
applications
id - integer
name - string
environments
id - integer
application_id - integer
name - string
deployments
id - integer
environment_id - integer
commit_hash - string既然我们已经研究了关系的表结构,接下来我们来定义 Application 模型上的关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Application extends Model
{
/**
* Get all of the deployments for the application.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}传递给 hasManyThrough 方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果相关联的模型上已经定义了所有相关联的关系,你可以通过调用 through 方法并提供这些关系的名称来流畅地定义一个“has-many-through”关系。例如,如果 Application 模型有一个 environments 关系,并且 Environment 模型有一个 deployments 关系,你可以像这样定义一个连接应用程序和部署的“has-many-through”关系:
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();尽管 Deployment 模型的表不包含 application_id 字段,hasManyThrough 关系仍然可以通过 $application->deployments 访问某个应用的部署。为了检索这些模型,Eloquent 会检查中间 Environment 模型表中的 application_id 字段。找到相关的环境 ID 后,它们被用于查询 Deployment 模型的表。
典型的 Eloquent 外键约定将在执行关系查询时被使用。如果你想要自定义关系中的键,你可以把它们作为第三个和第四个参数传递给 hasManyThrough 方法。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:
class Application extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'application_id', // Foreign key on the environments table...
'environment_id', // Foreign key on the deployments table...
'id', // Local key on the applications 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();通常,我们会向约束关联关系的模型中添加额外方法。例如,你可以向 User 模型中添加一个 featuredPosts 方法,该方法会通过一个额外的 where 约束来限定更宽泛的 posts 关联关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/**
* Get the user's posts.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class)->latest();
}
/**
* Get the user's featured posts.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->where('featured', true);
}
}然而,如果你尝试通过 featuredPosts 方法创建一个模型,其 featured 属性将不会被设置为 true。如果你希望通过关系方法创建模型,并且还希望指定应添加到通过该关系创建的所有模型中的属性,你可以在构建关系查询时使用 withAttributes 方法:
/**
* Get the user's featured posts.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}该 withAttributes 方法会将 where 条件添加到查询中,使用给定的属性,并且它还会将给定的属性添加到通过关系方法创建的任何模型中:
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
$post->featured; // true为了指示 withAttributes 方法不向查询添加 where 条件, 您可以将 asConditions 参数设置为 false:`
return $this->posts()->withAttributes(['featured' => true], asConditions: false);多对多关系比 hasOne 和 hasMany 关系稍微复杂。多对多关系的一个例子是一个用户拥有多个角色,并且这些角色也由应用程序中的其他用户共享。例如,一个用户可能被分配“作者”和“编辑”的角色;然而,这些角色也可能被分配给其他用户。因此,一个用户拥有多个角色,并且一个角色拥有多个用户。
`
请记住,由于一个角色可以属于多个用户,我们不能简单地将一个 user_id 列放在 roles 表上。这意味着一个角色只能属于一个用户。为了支持将角色分配给多个用户,需要 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_at 和 updated_at 时间戳,请在定义关系时调用 withTimestamps 方法:
return $this->belongsToMany(Role::class)->withTimestamps();[!WARNING]
利用 Eloquent 自动维护时间戳的中间表,必须同时包含created_at和updated_at两个时间戳列.
pivot 属性名称如前所述,可以通过 pivot 属性在模型上访问中间表中的属性。但是,你可以自由地自定义此属性的名称,以更好地反映其在应用程序中的用途。
例如,如果你的应用程序包含可以订阅播客的用户,你可能在用户和播客之间存在多对多关系。如果是这种情况,你可能希望将你的中间表属性重命名为 subscription 而不是 pivot。这可以通过使用 as 方法在定义关系时完成:
return $this->belongsToMany(Podcast::class)
->as('subscription')
->withTimestamps();一旦自定义中间表属性已指定,您就可以使用自定义名称访问中间表数据:
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}您还可以在定义关联时,使用 wherePivot、wherePivotIn、wherePivotNotIn、wherePivotBetween、wherePivotNotBetween、wherePivotNull 和 wherePivotNotNull 方法来过滤 belongsToMany 关联查询返回的结果:
return $this->belongsToMany(Role::class)
->wherePivot('approved', 1);
return $this->belongsToMany(Role::class)
->wherePivotIn('priority', [1, 2]);
return $this->belongsToMany(Role::class)
->wherePivotNotIn('priority', [1, 2]);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNull('expired_at');
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotNull('expired_at');wherePivot 会在查询中添加一个 where 子句约束,但通过已定义的关系创建新模型时,不会添加指定的值。如果你需要同时查询和创建具有特定枢轴值的关系,可以使用 withPivotValue 方法:
return $this->belongsToMany(Role::class)
->withPivotValue('approved', 1);你可以使用 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 类。例如,我们可以定义一个 Role 模型,它使用一个自定义的 RoleUser 中间模型:
<?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
{
// ...
}[!WARNING]
枢纽模型不应使用SoftDeletestrait。如果你需要软删除枢纽记录,请考虑将你的枢纽模型转换为一个实际的 Eloquent 模型。
如果您定义了一个多对多关系,该关系使用了自定义中间模型,并且该中间模型具有自增主键,您应该确保您的自定义中间模型类定义了一个 incrementing 属性并将其设置为 true。
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;多态关联允许子模型通过单一关联属于多种模型类型。例如,设想你正在构建一个允许用户分享博客文章和视频的应用程序。在这样的应用程序中,一个Comment模型可能同时属于Post和Video模型。
一对一多态关联类似于典型的一对一关联;然而,子模型可以使用单个关联属于多种模型类型。例如,一个博客 Post 和一个 User 可能与一个 Image 模型共享一个多态关联。使用一对一多态关联可以让你拥有一张存储唯一图片的表,这些图片可以与帖子和用户关联。首先,让我们检查表结构:
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string请注意 images 表上的 imageable_id 和 imageable_type 列。 imageable_id 列将包含文章或用户的 ID 值,而 imageable_type 列将包含父模型的类名。 imageable_type 列被 Eloquent 用来确定在访问 imageable 关系时返回哪种“类型”的父模型。 在这种情况下,该列将包含 App\Models\Post 或 App\Models\User。
接下来,让我们来研究构建这种关系所需的模型定义:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
/**
* Get the parent imageable model (user or post).
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Post extends Model
{
/**
* Get the post's image.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class User extends Model
{
/**
* Get the user's image.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}一旦你的数据库表和模型定义好,你就可以通过你的模型来访问这些关系。例如,要获取某个帖子的图片,我们可以访问 image 动态关系属性:
use App\Models\Post;
$post = Post::find(1);
$image = $post->image;您可以通过访问执行 morphTo 调用的方法的名称来检索多态模型的父级。在此示例中,该方法是 Image 模型上的 imageable 方法。因此,我们将把该方法作为动态关联属性来访问:
use App\Models\Image;
$image = Image::find(1);
$imageable = $image->imageable;在 Image 模型上的 imageable 关联将返回一个 Post 或 User 实例,具体取决于哪种类型的模型拥有该图片。
如果需要,您可以指定您的多态子模型所使用的 "id" 和 "type" 列的名称。如果这样做,请确保始终将关系的名称作为第一个参数传递给 morphTo 方法。通常,此值应与方法名称匹配,因此您可以使用 PHP 的 __FUNCTION__ 常量:
/**
* Get the model that the image belongs to.
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}一对多多态关联类似于典型的一对多关联;然而,子模型可以使用单个关联属于多种模型类型。例如,设想您的应用程序用户可以对帖子和视频进行“评论”。使用多态关联,您可以使用单个 comments 表来包含帖子和视频的评论。首先,我们来审视构建此关联所需的表结构:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string接下来,让我们来检查构建这种关系所需的模型定义:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
/**
* Get the parent commentable model (post or video).
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Video extends Model
{
/**
* Get all of the video's comments.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}一旦您的数据库表和模型定义好后,您就可以通过模型的动态关系属性来访问这些关系。例如,要访问某个帖子的所有评论,我们可以使用 comments 动态属性:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->comments as $comment) {
// ...
} You may also also retrieve the parent of a polymorphic child model by accessing the name of the method that performs the call tomorphTo. In this case, that is thecommentablemethod on theComment` model. So, we will access that method as a dynamic relationship property in order to access the comment's parent model:
use App\Models\Comment;
$comment = Comment::find(1);
$commentable = $comment->commentable;在 Comment 模型上的 commentable 关系将返回一个 Post 或 Video 实例,具体取决于评论的父级是哪种类型的模型。
即使在使用 Eloquent 预加载时, "N + 1" 查询问题也可能出现, 如果您在遍历子模型时尝试从子模型访问父模型:
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}在上述示例中,引入了一个“N + 1”查询问题因为,尽管为每个 Post 模型预加载了评论,Eloquent 不会自动将父级 Post 填充到每个子级 Comment 模型上。
如果您希望 Eloquent 自动将父模型填充到它们的子模型上,您可以在定义 morphMany 关系时调用 chaperone 方法:
class Post extends Model
{
/**
* Get all of the post's comments.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}或者,如果你想选择加入在运行时自动父级数据填充,你可以在急切加载该关系时调用 chaperone 模型:
use App\Models\Post;
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();有时一个模型可能拥有许多相关模型,但你希望能够轻松地检索到该关系中“最新”或“最旧”的相关模型。例如,一个 User 模型可能关联许多 Image 模型,但你希望定义一种便捷的方式来与其上传的最新图片进行交互。你可以使用 morphOne 关系类型与 ofMany 方法相结合来实现此目的:
/**
* Get the user's most recent image.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}同样地,你可以定义一个方法来获取关系中“最旧的”,或第一个相关模型:
/**
* Get the user's oldest image.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}默认情况下,latestOfMany 和 oldestOfMany 方法将根据模型的主键检索最新或最旧的相关模型,该主键必须可排序。然而,有时你可能希望使用不同的排序标准从更大的关联关系中检索单个模型。
例如,使用 ofMany 方法,你可以检索用户最“喜欢”的图片。ofMany 方法接受可排序列作为它的第一个参数,以及在查询相关模型时要应用的聚合函数(min 或 max):
/**
* Get the user's most popular image.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}[!注意]
可以构建更高级的“多选一”关系。欲了解更多信息,请查阅“多选一”文档。
多对多多态关联比“morph one”和“morph many”关系稍微复杂一些。例如,一个 Post 模型和 Video 模型可以与一个 Tag 模型共享多态关联。在这种情况下使用多对多多态关联将允许你的应用程序拥有一个包含唯一标签的单一表,这些标签可以关联到文章或视频。首先,让我们检查构建此关系所需的表结构:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string[!NOTE]
在深入探讨多态多对多关系之前,您可能会受益于阅读关于典型多对多关系的文档。
接下来,我们准备定义模型上的关系。Post 和 Video 模型都将包含一个 tags 方法,该方法调用基础 Eloquent 模型类提供的 morphToMany 方法。
此 morphToMany 方法接受关联模型的名称以及 "关系名称". 根据我们为中间表名分配的名称及其包含的键,我们将把此关系称为 "可标记的":
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Post extends Model
{
/**
* Get all of the tags for the post.
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}接下来,在 Tag 模型上,你应该为其每个可能的父模型定义一个方法。因此,在这个例子中,我们将定义一个 posts 方法和一个 videos 方法。这两个方法都应该返回 morphedByMany 方法的结果。
该 morphedByMany 方法接受相关模型的名称以及“关系名称”。根据我们为中间表名称分配的名称以及它包含的键,我们将把这个关系称为“taggable”:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
/**
* Get all of the videos that are assigned this tag.
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}一旦定义了数据库表和模型,你就可以通过模型访问这些关系。例如,要访问一篇帖子的所有标签,你可以使用 tags 动态关系属性:
use App\Models\Post;
$post = Post::find(1);
foreach ($post->tags as $tag) {
// ...
}您可以通过访问执行对 morphedByMany 调用的方法的名称,从多态子模型中检索多态关系的父级。在本例中,即是 Tag 模型上的 posts 或 videos 方法:
use App\Models\Tag;
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
// ...
}
foreach ($tag->videos as $video) {
// ...
}默认情况下,Laravel 将使用完全限定类名来存储相关模型的"类型"。例如,根据上面的一对多关系示例,其中一个 Comment 模型可能属于一个 Post 模型或一个 Video 模型,默认的 commentable_type 将是 App\Models\Post 或 App\Models\Video,分别地。但是,你可能希望将这些值与应用程序的内部结构解耦。
例如,我们不使用模型名称作为“类型”,可以改用简单的字符串,例如 post 和 video。通过这样做,我们数据库中多态的“类型”列值即使在模型被重命名后仍然有效:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);你可以调用 enforceMorphMap 方法,在你的 App\Providers\AppServiceProvider 类的 boot 方法中,或者如果你愿意,可以创建一个单独的服务提供者。
你可以在运行时使用模型的 getMorphClass 方法确定给定模型的 morph 别名。反之,你可以使用 Relation::getMorphedModel 方法确定与 morph 别名关联的完全限定类名:
use Illuminate\Database\Eloquent\Relations\Relation;
$alias = $post->getMorphClass();
$class = Relation::getMorphedModel($alias);[!WARNING]
当向现有应用程序添加"morph map"时,你的数据库中每个仍然包含完全限定类的可多态*_type列值都需要转换为其"map"名称。
您可以使用 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');
});[!警告]
在定义动态关联时,始终向 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 关系查询添加额外的约束,你可以像访问属性一样访问该关系。例如,继续使用我们的 User 和 Post 示例模型,我们可以这样访问用户的所有帖子:
use App\Models\User;
$user = User::find(1);
foreach ($user->posts as $post) {
// ...
}动态关系属性执行“惰性加载”,这意味着它们只会在您实际访问时才加载其关系数据。因此,开发人员通常使用预加载来预加载他们知道在模型加载后将会访问的关系。预加载大幅减少了加载模型关系时必须执行的 SQL 查询。
在检索模型记录时,您可能希望根据关系的存在来限制您的结果。例如,假设您想检索所有至少有一个评论的博客文章。为此,您可以将关系名称传递给 has 和 orHas 方法:
use App\Models\Post;
// Retrieve all posts that have at least one comment...
$posts = Post::has('comments')->get();你还可以指定一个运算符和计数数值,以进一步自定义查询:
// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', `>=`, 3)->get();嵌套的 has 语句可以使用“点”表示法构建。例如,您可以检索所有至少包含一条评论且该评论又至少包含一张图片的帖子:
// Retrieve posts that have at least one comment with images...
$posts = Post::has('comments.images')->get();如果您需要更强大的功能,您可以使用 whereHas 和 orWhereHas 方法来定义 has 查询上的额外查询约束,例如检查评论内容:
use Illuminate\Database\Eloquent\Builder;
// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, `>=`, 10)->get();[!WARNING]
Eloquent 当前不支持跨数据库查询关系是否存在。这些关系必须存在于同一个数据库中。
whereAttachedTo 方法可用于查询与一个模型或模型集合具有多对多关联的模型:
$users = User::whereAttachedTo($role)->get();你也可以提供一个 集合 实例给 whereAttachedTo 方法。这样做时,Laravel 将会检索关联到该集合中任何一个模型的模型:
$tags = Tag::whereLike('name', '%laravel%')->get();
$posts = Post::whereAttachedTo($tags)->get();如果你希望查询某个关联关系是否存在,并且该关联查询附加了一个单一、简单的 where 条件,你可能会发现使用 whereRelation、orWhereRelation、whereMorphRelation 和 orWhereMorphRelation 方法更为方便。例如,我们可以查询所有拥有未批准评论的文章:
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();当然, 像调用查询构建器的 where 方法一样, 你也可以指定一个操作符:
$posts = Post::whereRelation(
'comments', 'created_at', `>=`, now()->subHour()
)->get();检索模型记录时, 您可能希望根据关系不存在来限制结果. 例如, 假设您想要检索所有没有任何评论的博客文章. 为此, 您可以将关系的名称传递给 doesntHave 和 orDoesntHave 方法:
use App\Models\Post;
$posts = Post::doesntHave('comments')->get();如果您需要更强大的功能,您可以使用 whereDoesntHave 和 orWhereDoesntHave 方法为您的 doesntHave 查询添加额外的查询约束,例如检查评论的内容:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();您可以使用“点”符号来对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子,以及那些有评论但所有评论均非来自被禁用用户的帖子:
use Illuminate\Database\Eloquent\Builder;
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();要查询是否存在“morph to”关系, 您可以使用 whereHasMorph 和 whereDoesntHaveMorph 方法.
这些方法接受关系名称作为它们的第一个参数.
接下来, 这些方法接受您希望在查询中包含的相关模型的名称.
最后, 您可以提供一个闭包, 用于自定义关系查询:
use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
// Retrieve comments associated to posts with a title not like code%...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();您可能偶尔需要基于相关多态模型的“类型”添加查询约束。传递给 whereHasMorph 方法的闭包可能会接收一个 $type 值作为它的第二个参数。这个参数允许您检查正在构建的查询的“类型”:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query, string $type) {
$column = $type === Post::class ? 'content' : 'title';
$query->where($column, 'like', 'code%');
}
)->get();有时您可能希望查询“morph to”关系的父级的子级。您可以使用 whereMorphedTo 和 whereNotMorphedTo 方法实现此目的,它们将自动确定给定模型的适当 morph 类型映射。这些方法接受 morphTo 关系的名称作为它们的第一个参数,以及相关的父模型作为它们的第二个参数:
$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->get();您不必传递一个可能的多态模型数组,您可以提供 * 作为通配符值。这会指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将会执行一个额外的查询以执行此操作:
use Illuminate\Database\Eloquent\Builder;
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();有时你可能希望统计给定关系的相关模型的数量,而不实际加载这些模型。为此,你可以使用 withCount 方法。withCount 方法会在结果模型上放置一个 {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);
}])如果你将 withCount 与 select 语句结合使用,请确保你在 select 方法之后调用 withCount:
$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();除了 withCount 方法, Eloquent 还提供了 withMin, withMax, withAvg, withSum, 和 withExists 方法. 这些方法会在你的结果模型上放置一个 {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();如果你希望预加载一个“morph to”关系,以及该关系可能返回的各种实体相关的模型计数,你可以使用 with 方法结合 morphTo 关系的 morphWithCount 方法。
在此示例中, 我们假设 Photo 模型和 Post 模型可能会创建 ActivityFeed 模型. 我们假设 ActivityFeed 模型定义了一个名为 parentable 的“多态关联”关系, 它允许我们为一个给定的 ActivityFeed 实例检索其父 Photo 模型或 Post 模型. 此外, 我们假设 Photo 模型“拥有多个” Tag 模型以及 Post 模型“拥有多个” Comment 模型.
现在,让我们假设我们想要检索 ActivityFeed 实例并预加载每个 ActivityFeed 实例的 parentable 父模型。此外,我们想要检索与每个父级照片关联的标签数量以及与每个父级帖子关联的评论数量:
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::with([
'parentable' => function (MorphTo $morphTo) {
$morphTo->morphWithCount([
Photo::class => ['tags'],
Post::class => ['comments'],
]);
}])->get();我们假设已经检索到一组 ActivityFeed 模型,现在我们想要加载与活动动态关联的各种 parentable 模型的嵌套关系计数。您可以使用 loadMorphCount 方法来完成此操作:
$activities = ActivityFeed::with('parentable')->get();
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);当将 Eloquent 关系作为属性访问时,关联模型是“懒加载”的。这意味着关系数据直到你首次访问该属性时才会被实际加载。然而,Eloquent 可以在你查询父模型时“预加载”关系。预加载解决了“N + 1”查询问题。为了说明 N + 1 查询问题,考虑一个“属于”到一个 Author 模型的 Book 模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
/**
* Get the author that wrote the book.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}现在,让我们检索所有书籍及其作者:
use App\Models\Book;
$books = Book::all();
foreach ($books as $book) {
echo $book->author->name;
}这个循环将执行一次查询以检索数据库表中所有的书籍,然后为每本书执行另一次查询以检索该书的作者。因此,如果我们有25本书,上面的代码将运行26次查询:一次用于原始书籍,以及25次额外查询以检索每本书的作者。
值得庆幸的是,我们可以使用预加载将此操作减少到仅仅两次查询。构建查询时,你可以使用 with 方法指定应该预加载哪些关系:
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}对于此操作,将只执行两个查询 - 一个查询用于检索所有书籍和一个查询用于检索所有书籍的所有作者:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)有时您可能需要预加载多个不同的关联。为此,只需将一个关联数组传递给 with 方法:
$books = Book::with(['author', 'publisher'])->get();要预加载一个关联的关联,你可以使用“点”语法。例如,我们来预加载一本书的所有作者以及该作者的所有个人联系人:
$books = Book::with('author.contacts')->get();或者,您可以通过向 with 方法提供一个嵌套数组来指定嵌套的急切加载关系,这在急切加载多个嵌套关系时会很方便:
$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();morphTo 关系如果您想要预加载一个 morphTo 关联,以及在该关联可能返回的各种实体上的嵌套关联,您可以使用 with 方法结合 morphTo 关联的 morphWith 方法。为了帮助说明这个方法,让我们考虑以下模型:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}在此示例中,我们假设 Event, Photo, 和 Post 模型可能会创建 ActivityFeed 模型。此外,我们假设 Event 模型属于一个 Calendar 模型, Photo 模型与 Tag 模型相关联, 并且 Post 模型属于一个 Author 模型。
利用这些模型定义和关系,我们可以检索 ActivityFeed 模型实例,并预加载所有 parentable 模型及其各自的嵌套关系。
use Illuminate\Database\Eloquent\Relations\MorphTo;
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
}])->get();您可能并非总是需要从您正在检索的关联关系中获取所有列。因此,Eloquent 允许您指定希望从该关联关系中检索哪些列:
$books = Book::with('author:id,name,book_id')->get();[!WARNING]
使用此功能时,您应始终包含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;
$users = User::with(['posts' => function ($query) {
$query->where('title', 'like', '%code%');
}])->get();在此示例中,Eloquent 将只预加载那些帖子的 title 字段包含单词 code 的帖子。你可以调用其他 查询构建器 方法来进一步自定义预加载操作:
$users = User::with(['posts' => function ($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" 的视频。
你有时可能需要检查某个关联是否存在,同时基于相同的条件加载该关联。
use App\Models\User;
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();有时候你可能需要在父模型已被检索之后预加载关联关系。例如,如果你需要动态地决定是否加载关联模型,这可能很有用:
use App\Models\Book;
$books = Book::all();
if ($condition) {
$books->load('author', 'publisher');
}如果你需要在预加载查询上设置额外的查询约束,你可以传递一个以你希望加载的关系为键的数组。数组的值应该是接收查询实例的闭包实例:
$author->load(['books' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);仅当关系尚未加载时才加载它,请使用 loadMissing 方法:
$book->loadMissing('author');morphTo如果你想预加载一个 morphTo 关系,以及在该关系可能返回的各种实体上的嵌套关系,你可以使用 loadMorph 方法。
此方法接受 morphTo 关系的名称作为其第一个参数,以及一个模型/关系对数组作为其第二个参数。为了帮助说明此方法,让我们考虑以下模型:
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class ActivityFeed extends Model
{
/**
* Get the parent of the activity feed record.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}在此示例中,我们假设 Event、Photo 和 Post 模型可以创建 ActivityFeed 模型。 此外,我们假设 Event 模型属于 Calendar 模型,Photo 模型与 Tag 模型关联,并且 Post 模型属于 Author 模型。
利用这些模型定义和关系,我们可以检索 ActivityFeed 模型实例,并预加载所有 parentable 模型及其各自的嵌套关系:
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);[!WARNING]
此功能目前处于测试阶段,旨在收集社区反馈。此功能的行为和功能可能会发生变化,即使在补丁版本中也是如此。
在许多情况下,Laravel 可以自动预加载您访问的关系。要启用自动预加载,您应该在应用程序的 AppServiceProvider 的 boot 方法中调用 Model::automaticallyEagerLoadRelationships 方法:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}当此功能启用时,Laravel 将尝试自动加载您访问的任何尚未预加载的关系。例如,请考虑以下场景:
use App\Models\User;
$users = User::all();
foreach ($users as $user) {
foreach ($user->posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->content;
}
}
}通常,上述代码会为每个用户执行一次查询以获取他们的帖子,以及为每个帖子执行一次查询以获取其评论。然而,当 automaticallyEagerLoadRelationships 功能被启用时,当你尝试访问任何已检索到的用户的帖子时,Laravel 将自动为用户集合中的所有用户 惰性预加载 帖子。同样,当你尝试访问任何已检索到的帖子的评论时,所有评论都将被惰性预加载为所有最初检索到的帖子。
如果您不想全局启用自动预加载,您仍然可以通过在该集合上调用 withRelationshipAutoloading 方法,为单个 Eloquent 集合实例启用此功能:
$users = User::where('vip', true)->get();
return $users->withRelationshipAutoloading();正如之前所讨论的,预加载关系通常可以为您的应用程序带来显著的性能优势。因此,如果您愿意,您可以指示 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 将会抛出 Illuminate\Database\LazyLoadingViolationException 异常,当你的应用程序尝试延迟加载任何 Eloquent 关系时。
您可以使用 handleLazyLoadingViolationsUsing 方法自定义惰性加载违规行为. 例如, 通过此方法, 您可以指示惰性加载违规行为仅被记录, 而不是通过异常中断应用程序的执行:
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("Attempted to lazy load [{$relation}] on model [{$class}].");
});保存 方法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 方法将自动为新的 Comment 模型添加适当的 post_id 值。
如果您需要保存多个相关的模型,您可以使用 saveMany 方法:
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);save 和 saveMany 方法将持久化给定的模型实例,但不会将新持久化的模型添加到任何内存中的关联关系中,这些关联关系已经加载到父模型上。如果你计划在使用 save 或 saveMany 方法后访问该关联关系,你可能希望使用 refresh 方法来重新加载模型及其关联关系:
$post->comments()->save($comment);
$post->refresh();
// All comments, including the newly saved comment...
$post->comments;如果您想保存您的模型及其所有关联关系,您可以使用push方法。在此示例中,Post模型以及其评论和评论作者都将被保存:
$post = Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();pushQuietly 方法可用于保存模型及其关联关系,而不触发任何事件:
$post->pushQuietly();create方法除了 save 和 saveMany 方法之外,你还可以使用 create 方法,该方法接受一个属性数组,创建一个模型,并将其插入到数据库中。 save 和 create 的区别在于,save 接受一个完整的 Eloquent 模型实例,而 create 接受一个普通的 PHP array。 新创建的模型将由 create 方法返回:
use App\Models\Post;
$post = Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);您可以使用createMany方法来创建多个相关模型:
$post = Post::find(1);
$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);createQuietly 和 createManyQuietly 方法可用于创建模型,而不分发任何事件:
$user = User::find(1);
$user->posts()->createQuietly([
'title' => 'Post title.',
]);
$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);你也可以使用 findOrNew、firstOrNew、firstOrCreate 和 updateOrCreate 方法来在关联关系上创建和更新模型。
[!NOTE]
在使用create方法之前,请务必查阅 批量赋值 文档。
如果您想将一个子模型分配给一个新的父模型,您可以使用 associate 方法。在此示例中,User 模型定义了一个到 Account 模型的 belongsTo 关系。此 associate 方法将会在子模型上设置外键:
use App\Models\Account;
$account = Account::find(10);
$user->account()->associate($account);
$user->save();要从子模型中移除父模型,您可以使用 dissociate 方法。此方法会将关系的外键设置为 null:
$user->account()->dissociate();
$user->save();Eloquent 还提供了方法,使处理多对多关系更方便。
例如,我们假设一个用户可以拥有多个角色,一个角色也可以拥有多个用户。
你可以使用 attach 方法,通过在关系的中间表中插入一条记录,将一个角色附加到一个用户:
use App\Models\User;
$user = User::find(1);
$user->roles()->attach($roleId);当将关系附加到模型时,你还可以传入一个包含额外数据的数组,以插入到中间表:
$user->roles()->attach($roleId, ['expires' => $expires]);有时可能需要从用户中移除某个角色。要移除多对多关系记录,请使用 detach 方法。该 detach 方法将删除中间表中的相应记录;但是,两个模型都将保留在数据库中:
// Detach a single role from the user...
$user->roles()->detach($roleId);
// Detach all roles from the user...
$user->roles()->detach();为方便起见,attach 和 detach 也接受 ID 数组作为输入:
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);您也可以使用 sync 方法来构建多对多关联。 sync 方法接受一个 ID 数组,这些 ID 将被放置到中间表上。任何不在给定数组中的 ID 都将从中间表中移除。因此,此操作完成后,只有给定数组中的 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,
]);当模型定义一个 belongsTo 或 belongsToMany 关系到另一个模型,例如一个 Comment 属于一个 Post,有时更新父模型的时间戳很有帮助,当子模型更新时。
例如,当一个 Comment 模型更新时,您可能希望自动“触摸”拥有者 Post 的 updated_at 时间戳,以便将其设置为当前日期和时间。为此,您可以向您的子模型添加一个 touches 属性,其中包含当子模型更新时,应更新其 updated_at 时间戳的关系名称:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['post'];
/**
* Get the post that the comment belongs to.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}[!WARNING]
父模型时间戳只会在子模型使用 Eloquent 的save方法更新时才会更新。