Eloquent:关系
- Introduction
- Defining Relationships
- Scoped Relationships
- Many to Many Relationships
- Polymorphic Relationships
- Dynamic Relationships
- Querying Relations
- Aggregating Related Models
- Eager Loading
- Inserting and Updating Related Models
- Touching Parent Timestamps
介绍
数据库表通常彼此关联。例如,一篇博客文章可能包含多条评论,或者一个订单可能与下单的用户相关。Eloquent 简化了这些关系的管理和使用,并支持多种常见的关系:
定义关系
Eloquent 关系在 Eloquent 模型类中定义为方法。由于关系本身也是强大的查询构建器,因此将关系定义为方法可以提供强大的方法链和查询功能。例如,我们可以在此posts
关系上链式添加其他查询约束:
1$user->posts()->where('active', 1)->get();
但是,在深入研究使用关系之前,让我们先学习如何定义 Eloquent 支持的每种关系类型。
一对一/有一个
A one-to-one relationship is a very basic type of database relationship. For example, a User
model might be associated with one Phone
model. To define this relationship, we will place a phone
method on the User
model. The phone
method should call the hasOne
method and return its result. The hasOne
method is available to your model via the model's Illuminate\Database\Eloquent\Model
base class:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasOne; 7 8class User extends Model 9{10 /**11 * Get the phone associated with the user.12 */13 public function phone(): HasOne14 {15 return $this->hasOne(Phone::class);16 }17}
The first argument passed to the hasOne
method is the name of the related model class. Once the relationship is defined, we may retrieve the related record using Eloquent's dynamic properties. Dynamic properties allow you to access relationship methods as if they were properties defined on the model:
1$phone = User::find(1)->phone;
Eloquent determines the foreign key of the relationship based on the parent model name. In this case, the Phone
model is automatically assumed to have a user_id
foreign key. If you wish to override this convention, you may pass a second argument to the hasOne
method:
1return $this->hasOne(Phone::class, 'foreign_key');
Additionally, Eloquent assumes that the foreign key should have a value matching the primary key column of the parent. In other words, Eloquent will look for the value of the user's id
column in the user_id
column of the Phone
record. If you would like the relationship to use a primary key value other than id
or your model's $primaryKey
property, you may pass a third argument to the hasOne
method:
1return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
Defining the Inverse of the Relationship
So, we can access the Phone
model from our User
model. Next, let's define a relationship on the Phone
model that will let us access the user that owns the phone. We can define the inverse of a hasOne
relationship using the belongsTo
method:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 8class Phone extends Model 9{10 /**11 * Get the user that owns the phone.12 */13 public function user(): BelongsTo14 {15 return $this->belongsTo(User::class);16 }17}
When invoking the user
method, Eloquent will attempt to find a User
model that has an id
which matches the user_id
column on the Phone
model.
Eloquent determines the foreign key name by examining the name of the relationship method and suffixing the method name with _id
. So, in this case, Eloquent assumes that the Phone
model has a user_id
column. However, if the foreign key on the Phone
model is not user_id
, you may pass a custom key name as the second argument to the belongsTo
method:
1/**2 * Get the user that owns the phone.3 */4public function user(): BelongsTo5{6 return $this->belongsTo(User::class, 'foreign_key');7}
If the parent model does not use id
as its primary key, or you wish to find the associated model using a different column, you may pass a third argument to the belongsTo
method specifying the parent table's custom key:
1/**2 * Get the user that owns the phone.3 */4public function user(): BelongsTo5{6 return $this->belongsTo(User::class, 'foreign_key', 'owner_key');7}
One to Many / Has Many
A one-to-many relationship is used to define relationships where a single model is the parent to one or more child models. For example, a blog post may have an infinite number of comments. Like all other Eloquent relationships, one-to-many relationships are defined by defining a method on your Eloquent model:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasMany; 7 8class Post extends Model 9{10 /**11 * Get the comments for the blog post.12 */13 public function comments(): HasMany14 {15 return $this->hasMany(Comment::class);16 }17}
请记住,Eloquent 会自动为模型确定合适的外键列Comment
。按照惯例,Eloquent 会采用父模型的“蛇形命名法”名称,并在其后添加。因此,在本例中,Eloquent 会假设模型_id
的外键列为。Comment
post_id
一旦定义了关系方法,我们就可以通过访问属性来访问相关评论的集合comments
。记住,由于 Eloquent 提供了“动态关系属性”,我们可以像访问模型上的属性一样访问关系方法:
1use App\Models\Post;2 3$comments = Post::find(1)->comments;4 5foreach ($comments as $comment) {6 // ...7}
由于所有关系也充当查询构建器,因此您可以通过调用comments
方法并继续将条件链接到查询上来向关系查询添加进一步的约束:
1$comment = Post::find(1)->comments()2 ->where('title', 'foo')3 ->first();
与方法类似hasOne
,您也可以通过向方法传递附加参数来覆盖外键和本地键hasMany
:
1return $this->hasMany(Comment::class, 'foreign_key');2 3return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
自动为子级添加父级模型
即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题:
1$posts = Post::with('comments')->get();2 3foreach ($posts as $post) {4 foreach ($post->comments as $comment) {5 echo $comment->post->title;6 }7}
在上面的例子中,引入了一个“N + 1”查询问题,因为尽管每个Post
模型都预先加载了评论,但 Eloquent 不会自动Post
在每个子Comment
模型上补充父模型的评论。
如果您希望 Eloquent 自动将父模型绑定到其子模型上,您可以chaperone
在定义hasMany
关系时调用该方法:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasMany; 7 8class Post extends Model 9{10 /**11 * Get the comments for the blog post.12 */13 public function comments(): HasMany14 {15 return $this->hasMany(Comment::class)->chaperone();16 }17}
或者,如果您希望在运行时选择自动父级水化,则可以chaperone
在急切加载关系时调用该模型:
1use App\Models\Post;2 3$posts = Post::with([4 'comments' => fn ($comments) => $comments->chaperone(),5])->get();
一对多(逆)/属于
现在我们可以访问一篇博文的所有评论了,接下来让我们定义一个关系,允许评论访问其父博文。要定义逆关系hasMany
,请在子模型上定义一个调用该belongsTo
方法的关系方法:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 8class Comment extends Model 9{10 /**11 * Get the post that owns the comment.12 */13 public function post(): BelongsTo14 {15 return $this->belongsTo(Post::class);16 }17}
一旦定义了关系,我们就可以通过访问post
“动态关系属性”来检索评论的父帖子:
1use App\Models\Comment;2 3$comment = Comment::find(1);4 5return $comment->post->title;
在上面的例子中,Eloquent 将尝试找到一个与模型上的列匹配Post
的模型。id
post_id
Comment
Eloquent 通过检查关联方法的名称,并在方法名称后添加 后缀,_
后跟父模型主键列的名称来确定默认外键名称。因此,在本例中,Eloquent 将假定该Post
模型在表上的外键comments
为post_id
。
但是,如果您的关系的外键不遵循这些约定,您可以将自定义外键名称作为第二个参数传递给该belongsTo
方法:
1/**2 * Get the post that owns the comment.3 */4public function post(): BelongsTo5{6 return $this->belongsTo(Post::class, 'foreign_key');7}
如果您的父模型未使用id
作为其主键,或者您希望使用不同的列查找关联模型,则可以将第三个参数传递给belongsTo
指定父表的自定义键的方法:
1/**2 * Get the post that owns the comment.3 */4public function post(): BelongsTo5{6 return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');7}
默认模型
、belongsTo
、和关系允许您定义一个默认模型,当给定关系为 时将返回该模型hasOne
。此模式通常被称为空对象模式,可以帮助您移除代码中的条件检查。在以下示例中,如果没有用户连接到该模型,则该关系将返回一个空模型:hasOneThrough
morphOne
null
user
App\Models\User
Post
1/**2 * Get the author of the post.3 */4public function user(): BelongsTo5{6 return $this->belongsTo(User::class)->withDefault();7}
要用属性填充默认模型,您可以将数组或闭包传递给withDefault
方法:
1/** 2 * Get the author of the post. 3 */ 4public function user(): BelongsTo 5{ 6 return $this->belongsTo(User::class)->withDefault([ 7 'name' => 'Guest Author', 8 ]); 9}10 11/**12 * Get the author of the post.13 */14public function user(): BelongsTo15{16 return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {17 $user->name = 'Guest Author';18 });19}
查询属于关系
当查询“属于”关系的子项时,您可以手动构建where
子句来检索相应的 Eloquent 模型:
1use App\Models\Post;2 3$posts = Post::where('user_id', $user->id)->get();
但是,您可能会发现使用该whereBelongsTo
方法更方便,它将自动确定给定模型的适当关系和外键:
1$posts = Post::whereBelongsTo($user)->get();
你也可以为该方法提供一个集合whereBelongsTo
实例。这样,Laravel 就会检索属于该集合中任意父模型的模型:
1$users = User::where('vip', true)->get();2 3$posts = Post::whereBelongsTo($users)->get();
默认情况下,Laravel 将根据模型的类名确定与给定模型关联的关系;但是,您可以通过将关系名称作为whereBelongsTo
方法的第二个参数来手动指定关系名称:
1$posts = Post::whereBelongsTo($user, 'author')->get();
拥有多个中的一个
有时,一个模型可能包含多个相关模型,但您希望能够轻松地检索该关系中“最新”或“最旧”的相关模型。例如,一个User
模型可能与多个模型相关Order
,但您希望定义一种便捷的方式来与用户最近下达的订单进行交互。您可以使用hasOne
关系类型结合以下ofMany
方法来实现此目的:
1/**2 * Get the user's most recent order.3 */4public function latestOrder(): HasOne5{6 return $this->hasOne(Order::class)->latestOfMany();7}
同样,您可以定义一种方法来检索关系的“最旧”或第一个相关模型:
1/**2 * Get the user's oldest order.3 */4public function oldestOrder(): HasOne5{6 return $this->hasOne(Order::class)->oldestOfMany();7}
默认情况下,latestOfMany
和oldestOfMany
方法会根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时您可能希望使用不同的排序条件从更大的关系中检索单个模型。
例如,使用 该ofMany
方法可以检索用户最昂贵的订单。该ofMany
方法接受可排序列作为其第一个参数,以及查询关联模型时要应用的聚合函数(min
或):max
1/**2 * Get the user's largest order.3 */4public function largestOrder(): HasOne5{6 return $this->hasOne(Order::class)->ofMany('price', 'max');7}
由于 PostgreSQL 不支持MAX
针对 UUID 列执行该函数,因此目前无法将多对一关系与 PostgreSQL UUID 列结合使用。
将“多”关系转换为“有一个”关系
latestOfMany
通常,当使用、oldestOfMany
或方法检索单个模型时ofMany
,您已经为同一模型定义了“包含多个”关系。为了方便起见,Laravel 允许您通过one
在关系上调用 方法来轻松地将此关系转换为“包含一个”关系:
1/** 2 * Get the user's orders. 3 */ 4public function orders(): HasMany 5{ 6 return $this->hasMany(Order::class); 7} 8 9/**10 * Get the user's largest order.11 */12public function largestOrder(): HasOne13{14 return $this->orders()->one()->ofMany('price', 'max');15}
您还可以使用该one
方法将HasManyThrough
关系转换为HasOneThrough
关系:
1public function latestDeployment(): HasOneThrough2{3 return $this->deployments()->one()->latestOfMany();4}
高级有多种关系之一
可以构建更高级的“多选一”关系。例如,一个Product
模型可能关联多个Price
模型,即使新定价发布后,这些模型仍会保留在系统中。此外,产品的新定价数据可以提前发布,以便在未来某个日期通过列生效published_at
。
总而言之,我们需要检索最新发布的价格,且发布日期不在未来。此外,如果两个价格的发布日期相同,我们将优先选择 ID 最大的价格。为此,我们必须将一个数组传递给该ofMany
方法,其中包含用于确定最新价格的可排序列。此外,该ofMany
方法的第二个参数将是一个闭包。此闭包负责为关系查询添加额外的发布日期约束:
1/** 2 * Get the current pricing for the product. 3 */ 4public function currentPricing(): HasOne 5{ 6 return $this->hasOne(Price::class)->ofMany([ 7 'published_at' => 'max', 8 'id' => 'max', 9 ], function (Builder $query) {10 $query->where('published_at', '<', now());11 });12}
有一个通过
“has-one-through” 关系定义了与另一个模型的一对一关系。然而,这种关系表明声明模型可以通过第三个模型与另一个模型的一个实例匹配。
例如,在汽车修理厂应用程序中,每个Mechanic
模型可能与一个模型关联Car
,每个Car
模型也可能与一个模型关联。虽然技工和车主在数据库中没有直接关系,但技工可以通过模型Owner
访问车主。让我们看一下定义这种关系所需的表:Car
1mechanics 2 id - integer 3 name - string 4 5cars 6 id - integer 7 model - string 8 mechanic_id - integer 910owners11 id - integer12 name - string13 car_id - integer
现在我们已经检查了关系的表结构,让我们在Mechanic
模型上定义关系:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasOneThrough; 7 8class Mechanic extends Model 9{10 /**11 * Get the car's owner.12 */13 public function carOwner(): HasOneThrough14 {15 return $this->hasOneThrough(Owner::class, Car::class);16 }17}
传递给该hasOneThrough
方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果所有涉及该关系的模型都已定义好相关关系,您可以通过调用该through
方法并提供这些关系的名称,流畅地定义一个“has-one-through”关系。例如,如果Mechanic
模型之间存在一个cars
关系,并且Car
模型也存在一个owner
关系,您可以像这样定义一个连接技工和车主的“has-one-through”关系:
1// String based syntax...2return $this->through('cars')->has('owner');3 4// Dynamic syntax...5return $this->throughCars()->hasOwner();
关键约定
执行关系查询时,将使用典型的 Eloquent 外键约定。如果您想自定义关系的键,可以将它们作为该hasOneThrough
方法的第三和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:
1class Mechanic extends Model 2{ 3 /** 4 * Get the car's owner. 5 */ 6 public function carOwner(): HasOneThrough 7 { 8 return $this->hasOneThrough( 9 Owner::class,10 Car::class,11 'mechanic_id', // Foreign key on the cars table...12 'car_id', // Foreign key on the owners table...13 'id', // Local key on the mechanics table...14 'id' // Local key on the cars table...15 );16 }17}
或者,如前所述,如果所有涉及该关系的模型上都已定义相关关系,则可以通过调用该through
方法并提供这些关系的名称来流畅地定义“has-one-through”关系。这种方法的优势在于可以重用现有关系中已定义的关键约定:
1// String based syntax...2return $this->through('cars')->has('owner');3 4// Dynamic syntax...5return $this->throughCars()->hasOwner();
有许多通过
“has-many-through” 关系提供了一种通过中间关系访问远距离关系的便捷方法。例如,假设我们正在构建一个像Laravel Cloud这样的部署平台。一个模型可能通过一个中间模型Application
访问多个模型。使用此示例,您可以轻松收集给定应用程序的所有部署。让我们看一下定义此关系所需的表:Deployment
Environment
1applications 2 id - integer 3 name - string 4 5environments 6 id - integer 7 application_id - integer 8 name - string 910deployments11 id - integer12 environment_id - integer13 commit_hash - string
现在我们已经检查了关系的表结构,让我们在Application
模型上定义关系:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasManyThrough; 7 8class Application extends Model 9{10 /**11 * Get all of the deployments for the application.12 */13 public function deployments(): HasManyThrough14 {15 return $this->hasManyThrough(Deployment::class, Environment::class);16 }17}
传递给该hasManyThrough
方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果所有涉及该关系的模型都已定义相关关系,则可以通过调用该through
方法并提供这些关系的名称来流畅地定义“has-many-through”关系。例如,如果Application
模型之间存在一个关系environments
,并且Environment
模型之间存在一个deployments
关系,则可以像这样定义一个连接应用程序和部署的“has-many-through”关系:
1// String based syntax...2return $this->through('environments')->has('deployments');3 4// Dynamic syntax...5return $this->throughEnvironments()->hasDeployments();
虽然Deployment
模型表不包含任何application_id
列,但该hasManyThrough
关系可以通过 访问应用程序的部署$application->deployments
。为了检索这些模型,Eloquent 会检查application_id
中间Environment
模型表上的列。找到相关的环境 ID 后,即可使用它们来查询模型Deployment
表。
关键约定
执行关系查询时,将使用典型的 Eloquent 外键约定。如果您想自定义关系的键,可以将它们作为该hasManyThrough
方法的第三和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:
1class Application extends Model 2{ 3 public function deployments(): HasManyThrough 4 { 5 return $this->hasManyThrough( 6 Deployment::class, 7 Environment::class, 8 'application_id', // Foreign key on the environments table... 9 'environment_id', // Foreign key on the deployments table...10 'id', // Local key on the applications table...11 'id' // Local key on the environments table...12 );13 }14}
或者,如前所述,如果所有涉及该关系的模型上都已定义相关关系,则可以通过调用该through
方法并提供这些关系的名称来流畅地定义“多对多”关系。这种方法的优势在于可以重用现有关系中已定义的关键约定:
1// String based syntax...2return $this->through('environments')->has('deployments');3 4// Dynamic syntax...5return $this->throughEnvironments()->hasDeployments();
范围关系
向模型中添加额外的方法来约束关系是很常见的。例如,你可以featuredPosts
向模型添加一个方法,通过附加约束User
来约束更广泛的关系:posts
where
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasMany; 7 8class User extends Model 9{10 /**11 * Get the user's posts.12 */13 public function posts(): HasMany14 {15 return $this->hasMany(Post::class)->latest();16 }17 18 /**19 * Get the user's featured posts.20 */21 public function featuredPosts(): HasMany22 {23 return $this->posts()->where('featured', true);24 }25}
但是,如果您尝试通过 方法创建模型featuredPosts
,则其featured
属性将不会设置为true
。如果您想通过关系方法创建模型,并指定应添加到通过该关系创建的所有模型的属性,则可以withAttributes
在构建关系查询时使用 方法:
1/**2 * Get the user's featured posts.3 */4public function featuredPosts(): HasMany5{6 return $this->posts()->withAttributes(['featured' => true]);7}
该withAttributes
方法将where
使用给定的属性向查询添加条件,并且还将给定的属性添加到通过关系方法创建的任何模型中:
1$post = $user->featuredPosts()->create(['title' => 'Featured Post']);2 3$post->featured; // true
要指示withAttributes
方法不where
向查询添加条件,您可以将asConditions
参数设置为false
:
1return $this->posts()->withAttributes(['featured' => true], asConditions: false);
多对多关系
多对多关系比hasOne
“与”hasMany
关系稍微复杂一些。多对多关系的一个例子是,一个用户拥有多个角色,并且这些角色也由应用程序中的其他用户共享。例如,一个用户可能被分配了“作者”和“编辑”的角色;然而,这些角色也可能被分配给其他用户。因此,一个用户拥有多个角色,一个角色也拥有多个用户。
表结构
要定义这种关系,需要三个数据库表:users
、roles
和role_user
。 该role_user
表根据相关模型名称的字母顺序派生,包含user_id
和role_id
列。该表用作连接用户和角色的中间表。
请记住,由于一个角色可以属于多个用户,我们不能简单地user_id
在表中添加一列roles
。这意味着一个角色只能属于一个用户。为了支持将角色分配给多个用户,role_user
需要创建该表。我们可以总结如下关系的表结构:
1users 2 id - integer 3 name - string 4 5roles 6 id - integer 7 name - string 8 9role_user10 user_id - integer11 role_id - integer
模型结构
多对多关系的定义方式是编写一个返回该belongsToMany
方法结果的方法。该方法由应用程序所有 Eloquent 模型所使用的基类belongsToMany
提供。例如,让我们在模型上定义一个方法。传递给该方法的第一个参数是相关模型类的名称:Illuminate\Database\Eloquent\Model
roles
User
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsToMany; 7 8class User extends Model 9{10 /**11 * The roles that belong to the user.12 */13 public function roles(): BelongsToMany14 {15 return $this->belongsToMany(Role::class);16 }17}
一旦定义了关系,您就可以使用roles
动态关系属性访问用户的角色:
1use App\Models\User;2 3$user = User::find(1);4 5foreach ($user->roles as $role) {6 // ...7}
由于所有关系也充当查询构建器,因此您可以通过调用roles
方法并继续将条件链接到查询上来向关系查询添加进一步的约束:
1$roles = User::find(1)->roles()->orderBy('name')->get();
为了确定关系中间表的表名,Eloquent 会按字母顺序连接两个相关的模型名称。但是,您可以自由地覆盖此约定。您可以通过向该belongsToMany
方法传递第二个参数来实现:
1return $this->belongsToMany(Role::class, 'role_user');
除了自定义中间表的名称外,你还可以通过向该belongsToMany
方法传递附加参数来自定义表中键的列名。第三个参数是要定义关系的模型的外键名称,而第四个参数是要连接到的模型的外键名称:
1return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
定义关系的逆
要定义多对多关系的“逆”,你应该在关联模型上定义一个方法,该方法也返回该belongsToMany
方法的结果。为了完成我们的用户/角色示例,让我们在模型users
上定义这个方法Role
:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsToMany; 7 8class Role extends Model 9{10 /**11 * The users that belong to the role.12 */13 public function users(): BelongsToMany14 {15 return $this->belongsToMany(User::class);16 }17}
User
如您所见,除了引用模型之外,该关系的定义与其对应的模型完全相同App\Models\User
。由于我们重用了该belongsToMany
方法,因此在定义多对多关系的“逆”关系时,所有常用的表和键自定义选项均可用。
检索中间表列
正如您已经了解的,处理多对多关系需要中间表。Eloquent 提供了一些非常有用的方法与此表交互。例如,假设我们的User
模型与多个Role
模型相关联。访问此关系后,我们可以使用pivot
模型上的属性访问中间表:
1use App\Models\User;2 3$user = User::find(1);4 5foreach ($user->roles as $role) {6 echo $role->pivot->created_at;7}
请注意,我们检索的每个Role
模型都会自动分配一个pivot
属性。该属性包含一个表示中间表的模型。
默认情况下,模型上只存在模型键pivot
。如果中间表包含额外的属性,则必须在定义关系时指定它们:
1return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
如果您希望中间表具有由 Eloquent 自动维护的时间戳,请在定义关系时调用该created_at
方法:updated_at
withTimestamps
1return $this->belongsToMany(Role::class)->withTimestamps();
使用 Eloquent 自动维护的时间戳的中间表需要同时具有created_at
和updated_at
时间戳列。
自定义pivot
属性名称
如前所述,可以通过pivot
属性在模型上访问中间表中的属性。但是,您可以自由自定义此属性的名称,以更好地反映其在应用程序中的用途。
例如,如果您的应用程序包含可能订阅播客的用户,则用户和播客之间可能存在多对多关系。在这种情况下,您可能希望将中间表属性重命名为 ,subscription
而不是pivot
。这可以as
在定义关系时使用方法来完成:
1return $this->belongsToMany(Podcast::class)2 ->as('subscription')3 ->withTimestamps();
一旦指定了自定义中间表属性,您就可以使用自定义名称访问中间表数据:
1$users = User::with('podcasts')->get();2 3foreach ($users->flatMap->podcasts as $podcast) {4 echo $podcast->subscription->created_at;5}
通过中间表列过滤查询
您还可以在定义关系时belongsToMany
使用wherePivot
、、、、、和wherePivotIn
方法过滤关系查询返回的结果:wherePivotNotIn
wherePivotBetween
wherePivotNotBetween
wherePivotNull
wherePivotNotNull
1return $this->belongsToMany(Role::class) 2 ->wherePivot('approved', 1); 3 4return $this->belongsToMany(Role::class) 5 ->wherePivotIn('priority', [1, 2]); 6 7return $this->belongsToMany(Role::class) 8 ->wherePivotNotIn('priority', [1, 2]); 9 10return $this->belongsToMany(Podcast::class)11 ->as('subscriptions')12 ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);13 14return $this->belongsToMany(Podcast::class)15 ->as('subscriptions')16 ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);17 18return $this->belongsToMany(Podcast::class)19 ->as('subscriptions')20 ->wherePivotNull('expired_at');21 22return $this->belongsToMany(Podcast::class)23 ->as('subscriptions')24 ->wherePivotNotNull('expired_at');
该方法wherePivot
会在查询中添加 where 子句约束,但在通过定义的关系创建新模型时不会添加指定的值。如果您需要查询并创建具有特定枢轴值的关系,则可以使用该withPivotValue
方法:
1return $this->belongsToMany(Role::class)2 ->withPivotValue('approved', 1);
通过中间表列排序查询
belongsToMany
您可以使用该方法对关系查询返回的结果进行排序orderByPivot
。在以下示例中,我们将检索用户的所有最新徽章:
1return $this->belongsToMany(Badge::class)2 ->where('rank', 'gold')3 ->orderByPivot('created_at', 'desc');
定义自定义中间表模型
如果您想定义一个自定义模型来表示多对多关系的中间表,可以using
在定义关系时调用该方法。自定义数据透视模型允许您在数据透视模型上定义其他行为,例如方法和强制类型转换。
自定义多对多数据透视模型应该扩展该类,Illuminate\Database\Eloquent\Relations\Pivot
而自定义多态多对多数据透视模型也应该扩展该类Illuminate\Database\Eloquent\Relations\MorphPivot
。例如,我们可以定义一个Role
使用自定义数据RoleUser
透视模型的模型:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsToMany; 7 8class Role extends Model 9{10 /**11 * The users that belong to the role.12 */13 public function users(): BelongsToMany14 {15 return $this->belongsToMany(User::class)->using(RoleUser::class);16 }17}
定义RoleUser
模型时,应该扩展该类Illuminate\Database\Eloquent\Relations\Pivot
:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Relations\Pivot; 6 7class RoleUser extends Pivot 8{ 9 // ...10}
数据透视模型可能无法使用该SoftDeletes
特性。如果您需要软删除数据透视记录,请考虑将数据透视模型转换为真正的 Eloquent 模型。
自定义数据透视模型和递增 ID
如果您已经定义了使用自定义枢轴模型的多对多关系,并且该枢轴模型具有自动递增的主键,则应确保您的自定义枢轴模型类定义设置incrementing
为的属性true
。
1/**2 * Indicates if the IDs are auto-incrementing.3 *4 * @var bool5 */6public $incrementing = true;
多态关系
多态关系允许子模型使用单个关联从属于多种类型的模型。例如,假设您正在构建一个允许用户分享博客文章和视频的应用程序。在这样的应用程序中,一个Comment
模型可能同时属于Post
和Video
模型。
一对一(多态)
表结构
一对一多态关系类似于典型的一对一关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,博客Post
和模型User
可以共享一个多态关系Image
。使用一对一多态关系,您可以拥有一个包含唯一图像的表,这些图像可能与帖子和用户相关联。首先,让我们检查一下表结构:
1posts 2 id - integer 3 name - string 4 5users 6 id - integer 7 name - string 8 9images10 id - integer11 url - string12 imageable_id - integer13 imageable_type - string
注意表中的imageable_id
和列。列将包含帖子或用户的 ID 值,而列将包含父模型的类名。Eloquent使用此列来确定在访问关系时返回哪种“类型”的父模型。在本例中, 列将包含或。imageable_type
images
imageable_id
imageable_type
imageable_type
imageable
App\Models\Post
App\Models\User
模型结构
接下来,让我们检查一下建立这种关系所需的模型定义:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\MorphTo; 7 8class Image extends Model 9{10 /**11 * Get the parent imageable model (user or post).12 */13 public function imageable(): MorphTo14 {15 return $this->morphTo();16 }17}18 19use Illuminate\Database\Eloquent\Model;20use Illuminate\Database\Eloquent\Relations\MorphOne;21 22class Post extends Model23{24 /**25 * Get the post's image.26 */27 public function image(): MorphOne28 {29 return $this->morphOne(Image::class, 'imageable');30 }31}32 33use Illuminate\Database\Eloquent\Model;34use Illuminate\Database\Eloquent\Relations\MorphOne;35 36class User extends Model37{38 /**39 * Get the user's image.40 */41 public function image(): MorphOne42 {43 return $this->morphOne(Image::class, 'imageable');44 }45}
检索关系
定义好数据库表和模型后,你就可以通过模型访问它们之间的关系了。例如,为了检索帖子中的图片,我们可以访问image
动态关系属性:
1use App\Models\Post;2 3$post = Post::find(1);4 5$image = $post->image;
您可以通过访问执行调用的方法名称来检索多态模型的父级morphTo
。在本例中,该方法是模型imageable
上的方法Image
。因此,我们将该方法作为动态关系属性来访问:
1use App\Models\Image;2 3$image = Image::find(1);4 5$imageable = $image->imageable;
imageable
模型上的关系将Image
返回Post
或User
实例,具体取决于哪种类型的模型拥有图像。
关键约定
如果需要,您可以指定多态子模型所使用的“id”和“type”列的名称。如果这样做,请确保始终将关系名称作为该morphTo
方法的第一个参数传递。通常,此值应与方法名称匹配,因此您可以使用 PHP 的__FUNCTION__
常量:
1/**2 * Get the model that the image belongs to.3 */4public function imageable(): MorphTo5{6 return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');7}
一对多(多态)
表结构
一对多多态关系类似于典型的一对多关系;但是,子模型可以使用单个关联属于多种类型的模型。例如,假设您的应用程序的用户可以对帖子和视频进行“评论”。使用多态关系,您可以使用单个comments
表来包含帖子和视频的评论。首先,让我们检查一下构建此关系所需的表结构:
1posts 2 id - integer 3 title - string 4 body - text 5 6videos 7 id - integer 8 title - string 9 url - string1011comments12 id - integer13 body - text14 commentable_id - integer15 commentable_type - string
模型结构
接下来,让我们检查一下建立这种关系所需的模型定义:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\MorphTo; 7 8class Comment extends Model 9{10 /**11 * Get the parent commentable model (post or video).12 */13 public function commentable(): MorphTo14 {15 return $this->morphTo();16 }17}18 19use Illuminate\Database\Eloquent\Model;20use Illuminate\Database\Eloquent\Relations\MorphMany;21 22class Post extends Model23{24 /**25 * Get all of the post's comments.26 */27 public function comments(): MorphMany28 {29 return $this->morphMany(Comment::class, 'commentable');30 }31}32 33use Illuminate\Database\Eloquent\Model;34use Illuminate\Database\Eloquent\Relations\MorphMany;35 36class Video extends Model37{38 /**39 * Get all of the video's comments.40 */41 public function comments(): MorphMany42 {43 return $this->morphMany(Comment::class, 'commentable');44 }45}
检索关系
定义好数据库表和模型后,你就可以通过模型的动态关系属性来访问它们之间的关系了。例如,要访问某篇文章的所有评论,我们可以使用comments
动态属性:
1use App\Models\Post;2 3$post = Post::find(1);4 5foreach ($post->comments as $comment) {6 // ...7}
您还可以通过访问执行调用的方法名称来检索多态子模型的父模型morphTo
。在本例中,该方法是模型commentable
上的方法Comment
。因此,我们将该方法作为动态关系属性来访问,以便访问评论的父模型:
1use App\Models\Comment;2 3$comment = Comment::find(1);4 5$commentable = $comment->commentable;
commentable
模型上的关系将Comment
返回一个Post
或Video
实例,具体取决于评论的父模型类型。
自动为子级添加父级模型
即使使用 Eloquent 预加载,如果您在循环遍历子模型时尝试从子模型访问父模型,也可能会出现“N + 1”查询问题:
1$posts = Post::with('comments')->get();2 3foreach ($posts as $post) {4 foreach ($post->comments as $comment) {5 echo $comment->commentable->title;6 }7}
在上面的例子中,引入了一个“N + 1”查询问题,因为尽管每个Post
模型都预先加载了评论,但 Eloquent 不会自动Post
在每个子Comment
模型上补充父模型的评论。
如果您希望 Eloquent 自动将父模型绑定到其子模型上,您可以chaperone
在定义morphMany
关系时调用该方法:
1class Post extends Model 2{ 3 /** 4 * Get all of the post's comments. 5 */ 6 public function comments(): MorphMany 7 { 8 return $this->morphMany(Comment::class, 'commentable')->chaperone(); 9 }10}
或者,如果您希望在运行时选择自动父级水化,则可以chaperone
在急切加载关系时调用该模型:
1use App\Models\Post;2 3$posts = Post::with([4 'comments' => fn ($comments) => $comments->chaperone(),5])->get();
多之一(多态)
有时,一个模型可能包含多个相关模型,但您希望轻松检索该关系中“最新”或“最旧”的相关模型。例如,一个User
模型可能与多个模型相关Image
,但您希望定义一种便捷的方式来与用户上传的最新图片进行交互。您可以使用morphOne
关系类型结合以下ofMany
方法来实现此目的:
1/**2 * Get the user's most recent image.3 */4public function latestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->latestOfMany();7}
同样,您可以定义一种方法来检索关系的“最旧”或第一个相关模型:
1/**2 * Get the user's oldest image.3 */4public function oldestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->oldestOfMany();7}
默认情况下,latestOfMany
和oldestOfMany
方法会根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。但是,有时您可能希望使用不同的排序条件从更大的关系中检索单个模型。
例如,使用 该ofMany
方法,你可以检索用户最“喜欢”的图片。该ofMany
方法接受可排序列作为其第一个参数,并指定在查询关联模型时应用哪个聚合函数(min
或):max
1/**2 * Get the user's most popular image.3 */4public function bestImage(): MorphOne5{6 return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');7}
可以构建更高级的“多对一”关系。更多信息,请参阅“多对一”文档。
多对多(多态)
表结构
多对多多态关系比“一变一”和“多变一”关系稍微复杂一些。例如,一个Post
模型与Video
另一个模型之间可以共享一个与另一个模型的多态关系Tag
。在这种情况下使用多对多多态关系,可以让你的应用程序拥有一个包含唯一标签的表,这些标签可能与帖子或视频相关联。首先,让我们来看看构建这种关系所需的表结构:
1posts 2 id - integer 3 name - string 4 5videos 6 id - integer 7 name - string 8 9tags10 id - integer11 name - string1213taggables14 tag_id - integer15 taggable_id - integer16 taggable_type - string
在深入研究多态多对多关系之前,阅读有关典型多对多关系的文档可能会对您有所帮助。
模型结构
接下来,我们准备定义模型之间的关系。Post
和Video
模型都将包含一个tags
方法,该方法调用morphToMany
基础 Eloquent 模型类提供的方法。
该morphToMany
方法接受关联模型的名称以及“关系名称”。根据我们分配给中间表的名称及其包含的键,我们将该关系称为“可标记的”:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\MorphToMany; 7 8class Post extends Model 9{10 /**11 * Get all of the tags for the post.12 */13 public function tags(): MorphToMany14 {15 return $this->morphToMany(Tag::class, 'taggable');16 }17}
定义关系的逆
接下来,在Tag
模型上,你应该为其每个可能的父模型定义一个方法。因此,在本例中,我们将定义一个posts
方法和一个videos
方法。这两个方法都应该返回该方法的结果morphedByMany
。
该morphedByMany
方法接受关联模型的名称以及“关系名称”。根据我们分配给中间表的名称及其包含的键,我们将该关系称为“可标记的”:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\MorphToMany; 7 8class Tag extends Model 9{10 /**11 * Get all of the posts that are assigned this tag.12 */13 public function posts(): MorphToMany14 {15 return $this->morphedByMany(Post::class, 'taggable');16 }17 18 /**19 * Get all of the videos that are assigned this tag.20 */21 public function videos(): MorphToMany22 {23 return $this->morphedByMany(Video::class, 'taggable');24 }25}
检索关系
定义好数据库表和模型后,你就可以通过模型访问它们之间的关系了。例如,要访问某篇文章的所有标签,你可以使用tags
动态关系属性:
1use App\Models\Post;2 3$post = Post::find(1);4 5foreach ($post->tags as $tag) {6 // ...7}
你可以通过访问执行调用的方法名,从多态子模型中检索多态关系的父级morphedByMany
。在本例中,该方法是模型上的posts
或方法:videos
Tag
1use App\Models\Tag; 2 3$tag = Tag::find(1); 4 5foreach ($tag->posts as $post) { 6 // ... 7} 8 9foreach ($tag->videos as $video) {10 // ...11}
自定义多态类型
默认情况下,Laravel 将使用完全限定类名来存储相关模型的“类型”。例如,在上面的一对多关系示例中,一个Comment
模型可能属于Post
或Video
,则默认值commentable_type
分别为App\Models\Post
或App\Models\Video
。但是,您可能希望将这些值与应用程序的内部结构分离。
例如,我们可以使用简单的字符串(例如post
和video
)来代替使用模型名称作为“类型”。这样,即使模型重命名,数据库中多态的“类型”列值仍然有效:
1use Illuminate\Database\Eloquent\Relations\Relation;2 3Relation::enforceMorphMap([4 'post' => 'App\Models\Post',5 'video' => 'App\Models\Video',6]);
如果您愿意,您可以在类的方法enforceMorphMap
中调用该方法,或者创建单独的服务提供商。boot
App\Providers\AppServiceProvider
你可以在运行时使用模型的方法确定给定模型的变形别名getMorphClass
。相反,你也可以使用如下方法确定与变形别名关联的完全限定类名Relation::getMorphedModel
:
1use Illuminate\Database\Eloquent\Relations\Relation;2 3$alias = $post->getMorphClass();4 5$class = Relation::getMorphedModel($alias);
当向现有应用程序添加“变形图”时,*_type
数据库中每个仍包含完全限定类的可变形列值都需要转换为其“图”名称。
动态关系
您可以使用该resolveRelationUsing
方法在运行时定义 Eloquent 模型之间的关系。虽然通常不建议在常规应用程序开发中使用,但在开发 Laravel 软件包时偶尔会很有用。
该resolveRelationUsing
方法接受所需的关系名称作为其第一个参数。传递给该方法的第二个参数应该是一个闭包,它接受模型实例并返回有效的 Eloquent 关系定义。通常,你应该在服务提供者的 boot 方法中配置动态关系:
1use App\Models\Order;2use App\Models\Customer;3 4Order::resolveRelationUsing('customer', function (Order $orderModel) {5 return $orderModel->belongsTo(Customer::class, 'customer_id');6});
定义动态关系时,始终向 Eloquent 关系方法提供明确的关键名称参数。
查询关系
由于所有 Eloquent 关系都是通过方法定义的,因此您可以调用这些方法来获取关系实例,而无需实际执行查询来加载相关模型。此外,所有类型的 Eloquent 关系都可以用作查询构建器,允许您在最终对数据库执行 SQL 查询之前继续将约束链接到关系查询上。
例如,想象一个博客应用程序,其中一个User
模型有许多关联Post
模型:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\HasMany; 7 8class User extends Model 9{10 /**11 * Get all of the posts for the user.12 */13 public function posts(): HasMany14 {15 return $this->hasMany(Post::class);16 }17}
您可以查询posts
关系并向关系添加其他约束,如下所示:
1use App\Models\User;2 3$user = User::find(1);4 5$user->posts()->where('active', 1)->get();
您可以在关系中使用 Laravel查询构建器的任何方法,因此请务必浏览查询构建器文档以了解所有可用的方法。
关系后的链接orWhere
子句
如上例所示,您可以在查询关系时自由添加其他约束。但是,将orWhere
子句链接到关系时请务必小心,因为这些orWhere
子句在逻辑上将与关系约束分组在同一级别:
1$user->posts()2 ->where('active', 1)3 ->orWhere('votes', '>=', 100)4 ->get();
上面的示例将生成以下 SQL。如您所见,该or
子句指示查询返回任何投票数大于 100 的帖子。查询不再局限于特定用户:
1select *2from posts3where user_id = ? and active = 1 or votes >= 100
在大多数情况下,您应该使用逻辑组将条件检查分组在括号之间:
1use Illuminate\Database\Eloquent\Builder;2 3$user->posts()4 ->where(function (Builder $query) {5 return $query->where('active', 1)6 ->orWhere('votes', '>=', 100);7 })8 ->get();
上面的示例将生成以下 SQL。请注意,逻辑分组已正确对约束进行了分组,并且查询仍然约束于特定用户:
1select *2from posts3where user_id = ? and (active = 1 or votes >= 100)
关系方法与动态属性
如果您不需要在 Eloquent 关系查询中添加其他约束,则可以像访问属性一样访问该关系。例如,继续使用我们的User
和Post
示例模型,我们可以像这样访问用户的所有帖子:
1use App\Models\User;2 3$user = User::find(1);4 5foreach ($user->posts as $post) {6 // ...7}
动态关系属性执行“延迟加载”,这意味着它们仅在您实际访问时才会加载其关系数据。因此,开发人员经常使用预先加载来预加载他们知道在模型加载后才会访问的关系。预先加载可以显著减少加载模型关系时必须执行的 SQL 查询。
查询关系存在
检索模型记录时,您可能希望根据关系的存在来限制结果。例如,假设您想检索所有至少有一条评论的博客文章。为此,您可以将关系的名称传递给has
andorHas
方法:
1use App\Models\Post;2 3// Retrieve all posts that have at least one comment...4$posts = Post::has('comments')->get();
您还可以指定运算符和计数值来进一步自定义查询:
1// Retrieve all posts that have three or more comments...2$posts = Post::has('comments', '>=', 3)->get();
嵌套has
语句可以使用“点”符号构造。例如,你可以检索所有包含至少一条评论且评论中包含至少一张图片的帖子:
1// Retrieve posts that have at least one comment with images...2$posts = Post::has('comments.images')->get();
如果您需要更多功能,您可以使用whereHas
和orWhereHas
方法在查询中定义额外的查询约束has
,例如检查评论的内容:
1use Illuminate\Database\Eloquent\Builder; 2 3// Retrieve posts with at least one comment containing words like code%... 4$posts = Post::whereHas('comments', function (Builder $query) { 5 $query->where('content', 'like', 'code%'); 6})->get(); 7 8// Retrieve posts with at least ten comments containing words like code%... 9$posts = Post::whereHas('comments', function (Builder $query) {10 $query->where('content', 'like', 'code%');11}, '>=', 10)->get();
Eloquent 目前不支持跨数据库查询关系的存在。关系必须存在于同一个数据库中。
多对多关系存在查询
该whereAttachedTo
方法可用于查询与模型或模型集合有多对多附件的模型:
1$users = User::whereAttachedTo($role)->get();
你也可以为该方法提供一个集合whereAttachedTo
实例。这样,Laravel 就会检索附加到该集合中任意模型的模型:
1$tags = Tag::whereLike('name', '%laravel%')->get();2 3$posts = Post::whereAttachedTo($tags)->get();
内联关系存在查询
如果您想通过在关系查询中附加一个简单的 where 条件来查询关系的存在,您可能会发现使用whereRelation
、orWhereRelation
、whereMorphRelation
和orWhereMorphRelation
方法会更方便。例如,我们可以查询所有包含未批准评论的帖子:
1use App\Models\Post;2 3$posts = Post::whereRelation('comments', 'is_approved', false)->get();
当然,就像调用查询生成器的where
方法一样,您也可以指定一个运算符:
1$posts = Post::whereRelation(2 'comments', 'created_at', '>=', now()->subHour()3)->get();
查询关系缺失
检索模型记录时,您可能希望根据是否存在关系来限制结果。例如,假设您想检索所有没有任何评论的博客文章。为此,您可以将关系的名称传递给doesntHave
andorDoesntHave
方法:
1use App\Models\Post;2 3$posts = Post::doesntHave('comments')->get();
如果您需要更多功能,您可以使用whereDoesntHave
和orWhereDoesntHave
方法为查询添加额外的查询约束doesntHave
,例如检查评论的内容:
1use Illuminate\Database\Eloquent\Builder;2 3$posts = Post::whereDoesntHave('comments', function (Builder $query) {4 $query->where('content', 'like', 'code%');5})->get();
您可以使用“点”符号来针对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子,以及所有有评论且评论均非来自被禁用户的帖子:
1use Illuminate\Database\Eloquent\Builder;2 3$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {4 $query->where('banned', 1);5})->get();
查询 Morph To 关系
要查询“变形为”关系的存在,可以使用whereHasMorph
andwhereDoesntHaveMorph
方法。这些方法接受关系名称作为第一个参数。接下来,这些方法接受您希望包含在查询中的相关模型的名称。最后,您可以提供一个闭包来自定义关系查询:
1use App\Models\Comment; 2use App\Models\Post; 3use App\Models\Video; 4use Illuminate\Database\Eloquent\Builder; 5 6// Retrieve comments associated to posts or videos with a title like code%... 7$comments = Comment::whereHasMorph( 8 'commentable', 9 [Post::class, Video::class],10 function (Builder $query) {11 $query->where('title', 'like', 'code%');12 }13)->get();14 15// Retrieve comments associated to posts with a title not like code%...16$comments = Comment::whereDoesntHaveMorph(17 'commentable',18 Post::class,19 function (Builder $query) {20 $query->where('title', 'like', 'code%');21 }22)->get();
您可能偶尔需要根据相关多态模型的“类型”添加查询约束。传递给该whereHasMorph
方法的闭包可能会接收一个$type
值作为其第二个参数。此参数允许您检查正在构建的查询的“类型”:
1use Illuminate\Database\Eloquent\Builder; 2 3$comments = Comment::whereHasMorph( 4 'commentable', 5 [Post::class, Video::class], 6 function (Builder $query, string $type) { 7 $column = $type === Post::class ? 'content' : 'title'; 8 9 $query->where($column, 'like', 'code%');10 }11)->get();
有时,您可能想要查询“变形为”关系父级的子级。您可以使用whereMorphedTo
和whereNotMorphedTo
方法来实现此目的,它们将自动确定给定模型的正确变形类型映射。这些方法接受关系名称morphTo
作为其第一个参数,并将相关的父模型作为其第二个参数:
1$comments = Comment::whereMorphedTo('commentable', $post)2 ->orWhereMorphedTo('commentable', $video)3 ->get();
查询所有相关模型
除了传递可能的多态模型数组之外,您还可以提供*
通配符值。这将指示 Laravel 从数据库中检索所有可能的多态类型。Laravel 将执行额外的查询以执行此操作:
1use Illuminate\Database\Eloquent\Builder;2 3$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {4 $query->where('title', 'like', 'foo%');5})->get();
聚合相关模型
计数相关模型
有时,你可能希望在不实际加载模型的情况下,统计给定关系的相关模型数量。为此,你可以使用该withCount
方法。该方法会在结果模型上withCount
添加一个属性:{relation}_count
1use App\Models\Post;2 3$posts = Post::withCount('comments')->get();4 5foreach ($posts as $post) {6 echo $post->comments_count;7}
通过将数组传递给withCount
方法,您可以为多个关系添加“计数”,并为查询添加额外的约束:
1use Illuminate\Database\Eloquent\Builder;2 3$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {4 $query->where('content', 'like', 'code%');5}])->get();6 7echo $posts[0]->votes_count;8echo $posts[0]->comments_count;
您还可以为关系计数结果添加别名,允许对同一关系进行多次计数:
1use Illuminate\Database\Eloquent\Builder; 2 3$posts = Post::withCount([ 4 'comments', 5 'comments as pending_comments_count' => function (Builder $query) { 6 $query->where('approved', false); 7 }, 8])->get(); 9 10echo $posts[0]->comments_count;11echo $posts[0]->pending_comments_count;
延迟计数加载
使用该loadCount
方法,您可以在检索父模型之后加载关系计数:
1$book = Book::first();2 3$book->loadCount('genres');
如果需要在计数查询中设置额外的查询约束,可以传递一个以需要计数的关系为键的数组。数组值应该是接收查询构建器实例的闭包:
1$book->loadCount(['reviews' => function (Builder $query) {2 $query->where('rating', 5);3}])
关系计数和自定义选择语句
如果您要withCount
与select
语句结合,请确保在方法withCount
之后调用select
:
1$posts = Post::select(['title', 'body'])2 ->withCount('comments')3 ->get();
其他聚合函数
除了withCount
方法之外,Eloquent 还提供了withMin
、withMax
、withAvg
、withSum
和方法。这些方法会在生成的模型上withExists
添加属性:{relation}_{function}_{column}
1use App\Models\Post;2 3$posts = Post::withSum('comments', 'votes')->get();4 5foreach ($posts as $post) {6 echo $post->comments_sum_votes;7}
如果您希望使用其他名称访问聚合函数的结果,您可以指定自己的别名:
1$posts = Post::withSum('comments as total_comments', 'votes')->get();2 3foreach ($posts as $post) {4 echo $post->total_comments;5}
与方法类似loadCount
,这些方法也有延迟版本。这些额外的聚合操作可以在已经检索到的 Eloquent 模型上执行:
1$post = Post::first();2 3$post->loadSum('comments', 'votes');
如果将这些聚合方法与select
语句组合,请确保在该方法之后调用聚合方法select
:
1$posts = Post::select(['title', 'body'])2 ->withExists('comments')3 ->get();
计算变形关系中的相关模型
如果您希望急切加载“变形为”关系以及该关系可能返回的各种实体的相关模型计数,则可以将该方法与关系的方法with
结合使用。morphTo
morphWithCount
在此示例中,我们假设Photo
模型Post
可以创建ActivityFeed
模型。我们假设ActivityFeed
模型定义了一个名为 的“变形为”关系,parentable
该关系允许我们检索给定实例的父级Photo
或Post
模型ActivityFeed
。此外,我们假设Photo
模型“拥有多个”Tag
模型,并且Post
模型“拥有多个”Comment
模型。
现在,假设我们需要检索ActivityFeed
实例,并立即加载parentable
每个ActivityFeed
实例的父模型。此外,我们还想检索与每张父照片关联的标签数量,以及与每篇父帖子关联的评论数量:
1use Illuminate\Database\Eloquent\Relations\MorphTo;2 3$activities = ActivityFeed::with([4 'parentable' => function (MorphTo $morphTo) {5 $morphTo->morphWithCount([6 Photo::class => ['tags'],7 Post::class => ['comments'],8 ]);9 }])->get();
延迟计数加载
假设我们已经检索了一组ActivityFeed
模型,现在我们想要加载parentable
与Events源关联的各种模型的嵌套关系计数。您可以使用以下loadMorphCount
方法来实现此目的:
1$activities = ActivityFeed::with('parentable')->get();2 3$activities->loadMorphCount('parentable', [4 Photo::class => ['tags'],5 Post::class => ['comments'],6]);
预先加载
当将 Eloquent 关系作为属性访问时,相关模型会被“延迟加载”。这意味着关系数据直到您首次访问该属性时才会真正加载。但是,Eloquent 可以在您查询父模型时“预先加载”关系。预先加载可以缓解“N + 1”查询问题。为了说明 N + 1 查询问题,请考虑一个Book
“属于”其他Author
模型的模型:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 8class Book extends Model 9{10 /**11 * Get the author that wrote the book.12 */13 public function author(): BelongsTo14 {15 return $this->belongsTo(Author::class);16 }17}
现在,让我们检索所有书籍及其作者:
1use App\Models\Book;2 3$books = Book::all();4 5foreach ($books as $book) {6 echo $book->author->name;7}
此循环将执行一个查询来检索数据库表中的所有书籍,然后对每本书执行另一个查询以检索书籍的作者。因此,如果我们有 25 本书,则上面的代码将运行 26 个查询:一个查询原始书籍,另外 25 个查询用于检索每本书的作者。
值得庆幸的是,我们可以使用预先加载将此操作简化为两个查询。构建查询时,可以使用以下with
命令指定哪些关系需要预先加载:
1$books = Book::with('author')->get();2 3foreach ($books as $book) {4 echo $book->author->name;5}
对于此操作,将仅执行两个查询 - 一个查询检索所有书籍,另一个查询检索所有书籍的所有作者:
1select * from books2 3select * from authors where id in (1, 2, 3, 4, 5, ...)
预加载多个关系
有时你可能需要预先加载几种不同的关系。为此,只需将关系数组传递给该with
方法:
1$books = Book::with(['author', 'publisher'])->get();
嵌套预加载
要预先加载某个关联的关系,可以使用“点”语法。例如,让我们预先加载这本书的所有作者及其所有个人联系人:
1$books = Book::with('author.contacts')->get();
或者,您可以通过向方法提供嵌套数组来指定嵌套的预加载关系with
,这在预加载多个嵌套关系时非常方便:
1$books = Book::with([2 'author' => [3 'contacts',4 'publisher',5 ],6])->get();
嵌套预加载morphTo
关系
如果您希望预先加载某个morphTo
关系,以及该关系可能返回的各种实体上的嵌套关系,则可以将该方法与该关系的方法with
结合使用。为了帮助说明此方法,我们考虑以下模型:morphTo
morphWith
1<?php 2 3use Illuminate\Database\Eloquent\Model; 4use Illuminate\Database\Eloquent\Relations\MorphTo; 5 6class ActivityFeed extends Model 7{ 8 /** 9 * Get the parent of the activity feed record.10 */11 public function parentable(): MorphTo12 {13 return $this->morphTo();14 }15}
在此示例中,我们假设Event
、Photo
和Post
模型可以创建ActivityFeed
模型。此外,我们假设Event
模型属于某个Calendar
模型,Photo
模型与模型相关联Tag
,并且Post
模型属于某个Author
模型。
使用这些模型定义和关系,我们可以检索ActivityFeed
模型实例并急切加载所有parentable
模型及其各自的嵌套关系:
1use Illuminate\Database\Eloquent\Relations\MorphTo; 2 3$activities = ActivityFeed::query() 4 ->with(['parentable' => function (MorphTo $morphTo) { 5 $morphTo->morphWith([ 6 Event::class => ['calendar'], 7 Photo::class => ['tags'], 8 Post::class => ['author'], 9 ]);10 }])->get();
预先加载特定列
您可能并不总是需要检索关系中的每一列。因此,Eloquent 允许您指定要检索关系中的哪些列:
1$books = Book::with('author:id,name,book_id')->get();
使用此功能时,您应该始终将该id
列和任何相关的外键列包含在您希望检索的列的列表中。
默认预加载
有时你可能希望在检索模型时始终加载某些关系。为此,你可以$with
在模型上定义一个属性:
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 8class Book extends Model 9{10 /**11 * The relationships that should always be loaded.12 *13 * @var array14 */15 protected $with = ['author'];16 17 /**18 * Get the author that wrote the book.19 */20 public function author(): BelongsTo21 {22 return $this->belongsTo(Author::class);23 }24 25 /**26 * Get the genre of the book.27 */28 public function genre(): BelongsTo29 {30 return $this->belongsTo(Genre::class);31 }32}
如果您想要$with
从单个查询的属性中删除某个项目,您可以使用该without
方法:
1$books = Book::without('author')->get();
如果您想要覆盖$with
单个查询的属性中的所有项目,您可以使用该withOnly
方法:
1$books = Book::withOnly('genre')->get();
限制预加载
有时你可能希望预先加载某个关联,但同时又希望为预先加载查询指定额外的查询条件。你可以将一个关联数组传递给该with
方法来实现这一点,其中数组的键是关联名称,数组值是一个闭包,用于为预先加载查询添加额外的约束:
1use App\Models\User;2use Illuminate\Contracts\Database\Eloquent\Builder;3 4$users = User::with(['posts' => function (Builder $query) {5 $query->where('title', 'like', '%code%');6}])->get();
在此示例中,Eloquent 只会预先加载帖子列中title
包含单词 的帖子code
。您可以调用其他查询构建器方法来进一步自定义预先加载操作:
1$users = User::with(['posts' => function (Builder $query) {2 $query->orderBy('created_at', 'desc');3}])->get();
限制morphTo
关系的预加载
如果您急切地加载关联,Eloquent 将运行多个查询来获取每种类型的关联模型。您可以使用关联的方法morphTo
为每个查询添加额外的约束:MorphTo
constrain
1use Illuminate\Database\Eloquent\Relations\MorphTo; 2 3$comments = Comment::with(['commentable' => function (MorphTo $morphTo) { 4 $morphTo->constrain([ 5 Post::class => function ($query) { 6 $query->whereNull('hidden_at'); 7 }, 8 Video::class => function ($query) { 9 $query->where('type', 'educational');10 },11 ]);12}])->get();
在这个例子中,Eloquent 只会急切加载未被隐藏的帖子和值为type
“educational”的视频。
通过关系存在来约束预加载
有时,您可能需要在基于相同条件加载关系的同时检查关系是否存在。例如,您可能希望只检索具有符合给定查询条件User
的子模型的模型Post
,同时还预先加载匹配的帖子。您可以使用以下withWhereHas
方法实现此目的:
1use App\Models\User;2 3$users = User::withWhereHas('posts', function ($query) {4 $query->where('featured', true);5})->get();
延迟预加载
有时,您可能需要在父模型已检索后立即加载关联模型。例如,如果您需要动态决定是否加载关联模型,这可能会很有用:
1use App\Models\Book;2 3$books = Book::all();4 5if ($someCondition) {6 $books->load('author', 'publisher');7}
如果你需要在预先加载查询中设置额外的查询约束,可以传递一个以你想要加载的关系为键的数组。数组值应该是接收查询实例的闭包实例:
1$author->load(['books' => function (Builder $query) {2 $query->orderBy('published_date', 'asc');3}]);
要仅在尚未加载关系时加载关系,请使用该loadMissing
方法:
1$book->loadMissing('author');
嵌套延迟预加载和morphTo
如果您想要急切加载一种morphTo
关系以及该关系可能返回的各种实体上的嵌套关系,则可以使用该loadMorph
方法。
此方法接受关系名称morphTo
作为其第一个参数,并接受一个包含模型/关系对的数组作为其第二个参数。为了帮助说明此方法,我们考虑以下模型:
1<?php 2 3use Illuminate\Database\Eloquent\Model; 4use Illuminate\Database\Eloquent\Relations\MorphTo; 5 6class ActivityFeed extends Model 7{ 8 /** 9 * Get the parent of the activity feed record.10 */11 public function parentable(): MorphTo12 {13 return $this->morphTo();14 }15}
在此示例中,我们假设Event
、Photo
和Post
模型可以创建ActivityFeed
模型。此外,我们假设Event
模型属于某个Calendar
模型,Photo
模型与模型相关联Tag
,并且Post
模型属于某个Author
模型。
使用这些模型定义和关系,我们可以检索ActivityFeed
模型实例并急切加载所有parentable
模型及其各自的嵌套关系:
1$activities = ActivityFeed::with('parentable')2 ->get()3 ->loadMorph('parentable', [4 Event::class => ['calendar'],5 Photo::class => ['tags'],6 Post::class => ['author'],7 ]);
自动预加载
此功能目前处于测试阶段,旨在收集社区反馈。即使在补丁版本中,此功能的行为和功能也可能会发生变化。
在许多情况下,Laravel 可以自动预加载您访问的关系。要启用自动预加载,您应该在应用程序的 方法Model::automaticallyEagerLoadRelationships
中调用该方法:boot
AppServiceProvider
1use Illuminate\Database\Eloquent\Model;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Model::automaticallyEagerLoadRelationships();9}
启用此功能后,Laravel 将尝试自动加载您访问的任何之前未加载的关系。例如,考虑以下场景:
1use App\Models\User; 2 3$users = User::all(); 4 5foreach ($users as $user) { 6 foreach ($user->posts as $post) { 7 foreach ($post->comments as $comment) { 8 echo $comment->content; 9 }10 }11}
通常,上面的代码会针对每个用户执行查询以检索其帖子,并针对每个帖子执行查询以检索其评论。但是,automaticallyEagerLoadRelationships
启用此功能后,当您尝试访问任何检索到的用户的帖子时,Laravel 将自动以惰性预加载方式加载用户集合中所有用户的帖子。同样,当您尝试访问任何检索到的帖子的评论时,所有最初检索到的帖子的评论都将以惰性预加载方式加载。
如果您不想全局启用自动预加载,您仍然可以通过调用withRelationshipAutoloading
集合上的方法来为单个 Eloquent 集合实例启用此功能:
1$users = User::where('vip', true)->get();2 3return $users->withRelationshipAutoloading();
防止延迟加载
如前所述,预先加载关联关系通常可以显著提升应用程序的性能。因此,如果您愿意,可以指示 Laravel 始终阻止关联的延迟加载。为此,您可以调用preventLazyLoading
Eloquent 基类模型提供的方法。通常,您应该在boot
应用程序AppServiceProvider
类的方法中调用此方法。
该preventLazyLoading
方法接受一个可选的布尔参数,用于指示是否应阻止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,这样即使在生产代码中意外存在延迟加载关系,您的生产环境仍能继续正常运行:
1use Illuminate\Database\Eloquent\Model;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Model::preventLazyLoading(! $this->app->isProduction());9}
防止延迟加载后,Illuminate\Database\LazyLoadingViolationException
当您的应用程序尝试延迟加载任何 Eloquent 关系时,Eloquent 将抛出异常。
您可以使用该方法自定义延迟加载违规的行为handleLazyLoadingViolationsUsing
。例如,使用此方法,您可以指示仅记录延迟加载违规,而不是以异常中断应用程序的执行:
1Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {2 $class = $model::class;3 4 info("Attempted to lazy load [{$relation}] on model [{$class}].");5});
插入和更新相关模型
方法save
Eloquent 提供了便捷的方法将新模型添加到关系中。例如,你可能需要为帖子添加一条新评论。无需手动设置模型post_id
上的属性,Comment
你只需使用关系的方法插入评论即可save
:
1use App\Models\Comment;2use App\Models\Post;3 4$comment = new Comment(['message' => 'A new comment.']);5 6$post = Post::find(1);7 8$post->comments()->save($comment);
请注意,我们没有将comments
关系作为动态属性进行访问。相反,我们调用了该comments
方法来获取关系的实例。该save
方法会自动将适当的post_id
值添加到新Comment
模型中。
如果需要保存多个关联模型,可以使用如下saveMany
方法:
1$post = Post::find(1);2 3$post->comments()->saveMany([4 new Comment(['message' => 'A new comment.']),5 new Comment(['message' => 'Another new comment.']),6]);
和方法会持久化指定的模型实例,但不会将新持久化的模型添加到任何已加载到父模型save
的saveMany
内存关系中。如果您计划在使用save
或saveMany
方法后访问关系,则可能需要使用refresh
方法来重新加载模型及其关系:
1$post->comments()->save($comment);2 3$post->refresh();4 5// All comments, including the newly saved comment...6$post->comments;
递归保存模型和关系
如果您想要保存save
模型及其所有相关关系,可以使用该push
方法。在本例中,Post
模型及其评论和评论作者都将被保存:
1$post = Post::find(1);2 3$post->comments[0]->message = 'Message';4$post->comments[0]->author->name = 'Author Name';5 6$post->push();
该pushQuietly
方法可用于保存模型及其相关关系,而不会引发任何事件:
1$post->pushQuietly();
方法create
除了save
和saveMany
方法之外,你还可以使用create
方法,它接受一个属性数组作为参数,创建一个模型并将其插入数据库。save
和的区别create
在于save
接受一个完整的 Eloquent 模型实例,而create
接受一个普通的 PHP 对象array
。 该方法将返回新创建的模型create
:
1use App\Models\Post;2 3$post = Post::find(1);4 5$comment = $post->comments()->create([6 'message' => 'A new comment.',7]);
您可以使用该createMany
方法创建多个相关模型:
1$post = Post::find(1);2 3$post->comments()->createMany([4 ['message' => 'A new comment.'],5 ['message' => 'Another new comment.'],6]);
和方法可用于创建模型而不分派任何事件createQuietly
:createManyQuietly
1$user = User::find(1); 2 3$user->posts()->createQuietly([ 4 'title' => 'Post title.', 5]); 6 7$user->posts()->createManyQuietly([ 8 ['title' => 'First post.'], 9 ['title' => 'Second post.'],10]);
您还可以使用findOrNew
、、和方法来创建和更新关系模型firstOrNew
。firstOrCreate
updateOrCreate
在使用此create
方法之前,请务必查看批量分配文档。
属于关系
如果您想将子模型分配给新的父模型,可以使用该associate
方法。在本例中,该User
模型定义了belongsTo
与父Account
模型的关系。此associate
方法将在子模型上设置外键:
1use App\Models\Account;2 3$account = Account::find(10);4 5$user->account()->associate($account);6 7$user->save();
要从子模型中删除父模型,可以使用dissociate
方法。此方法将关系的外键设置为null
:
1$user->account()->dissociate();2 3$user->save();
多对多关系
连接/拆卸
Eloquent 还提供了一些方法,使处理多对多关系更加便捷。例如,假设一个用户可以拥有多个角色,一个角色也可以拥有多个用户。您可以使用该attach
方法通过在关系的中间表中插入记录来将角色附加到用户:
1use App\Models\User;2 3$user = User::find(1);4 5$user->roles()->attach($roleId);
当将关系附加到模型时,您还可以传递要插入到中间表中的附加数据数组:
1$user->roles()->attach($roleId, ['expires' => $expires]);
有时可能需要从用户中删除角色。要删除多对多关系记录,请使用该detach
方法。该detach
方法将从中间表中删除相应的记录;但是,两个模型仍将保留在数据库中:
1// Detach a single role from the user...2$user->roles()->detach($roleId);3 4// Detach all roles from the user...5$user->roles()->detach();
为了方便起见,attach
还detach
接受 ID 数组作为输入:
1$user = User::find(1);2 3$user->roles()->detach([1, 2, 3]);4 5$user->roles()->attach([6 1 => ['expires' => $expires],7 2 => ['expires' => $expires],8]);
同步关联
您还可以使用sync
方法来构建多对多关联。该sync
方法接受一个 ID 数组作为参数,并将其放入中间表中。任何不在给定数组中的 ID 都将从中间表中移除。因此,此操作完成后,中间表中将只保留给定数组中的 ID:
1$user->roles()->sync([1, 2, 3]);
您还可以通过 ID 传递其他中间表值:
1$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果您想要为每个同步的模型 ID 插入相同的中间表值,则可以使用该syncWithPivotValues
方法:
1$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
如果您不想从给定数组中分离缺失的现有 ID,则可以使用该syncWithoutDetaching
方法:
1$user->roles()->syncWithoutDetaching([1, 2, 3]);
切换关联
多对多关系还提供了一种toggle
方法来“切换”给定相关模型 ID 的连接状态。如果给定的 ID 当前已连接,则将其断开连接。同样,如果当前已断开连接,则将其连接:
1$user->roles()->toggle([1, 2, 3]);
您还可以通过 ID 传递其他中间表值:
1$user->roles()->toggle([2 1 => ['expires' => true],3 2 => ['expires' => true],4]);
更新中间表上的记录
如果需要更新关系中间表中的现有行,可以使用该updateExistingPivot
方法。该方法接受中间记录外键和要更新的属性数组:
1$user = User::find(1);2 3$user->roles()->updateExistingPivot($roleId, [4 'active' => false,5]);
接触父时间戳
当一个模型定义了与另一个模型的belongsTo
或belongsToMany
关系时,例如Comment
属于 的Post
,在更新子模型时更新父模型的时间戳有时会有所帮助。
例如,当Comment
模型更新时,你可能希望自动“修改”updated_at
所属模型的时间戳Post
,使其设置为当前日期和时间。为此,你可以在子模型中添加一个属性,该属性包含应在子模型更新时更新touches
其时间戳的关系名称:updated_at
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Model; 6use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 8class Comment extends Model 9{10 /**11 * All of the relationships to be touched.12 *13 * @var array14 */15 protected $touches = ['post'];16 17 /**18 * Get the post that the comment belongs to.19 */20 public function post(): BelongsTo21 {22 return $this->belongsTo(Post::class);23 }24}
仅当使用 Eloquent 的方法更新子模型时,父模型时间戳才会更新save
。