简介
数据库表通常相互关联。例如,一篇博客文章可能有许多评论,或者一个订单对应一个下单用户。php Eloquent 让这些关联的管理和使用变得简单,并支持多种常用的关联类型:
一对一
一对多
多对多
远程一对一
远程一对多
多态一对一
多态一对多
多态多对多
定义关联
Eloquent 关联是在 Eloquent 模型类中作为方法定义的。关联同时也是强大的 查询构造器,定义关联提供了强大的链式调用和查询功能。例如,可以在 php post 关联的链式调用中附加一个约束条件:
$user->posts()->where(‘active’, 1)->get();
在深入使用关联之前,让我们先学习如何定义 Eloquent 支持的每种关联类型
一对一
一对一是最基本的数据库关系。例如,一个 php User 模型一个 php Phone 模型关联。定义这个关联,要在 php User 模型写一个 php Phone 方法。在这个 php Phone 方法中调用 php hasOne 方法并返回其结果。php hasOne 方法被定义在 php Illuminate\Database\Eloquent\Model 这个模型基类中:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasOne;class User extends Model{/*** 获取与用户关联的电话。*/public function phone(): HasOne{return $this->hasOne(Phone::class);}}
php hasOne 方法的第一个参数是关联模型的类名。关联一旦定义,就可以使用 Eloquent 的动态属性获得相关记录。动态属性允许访问该关联方法,就像访问模型中定一个属性一样:
$phone = User::find(1)->phone;
Eloquent 基于父模型的名称来确定关联模型的外键名称。在本例中,php Phone模型会被自动假定有个 php user_id 的外键。如果想要覆盖这个约定,可以传递第二个参数给 php hasOne 方法:
return $this->hasOne(Phone::class, 'foreign_key');
另外,Eloquent 假设外键的值是与父模型的主键(Primary Key)相同的。换句话说,Eloquent 将会通过 php Phone 记录的 php user_id 列中查找与用户表的 php id 列相匹配的值。如果你希望使用自定义的主键值,而不是使用 php id 或者模型中的 php $primaryKey 属性,你可以给 php hasOne 方法传递第三个参数:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定义反向关联
现在已经能从 php User 模型访问到 php Phone 模型了。接下来,让我们再在 php Phone 模型上定义一个关联,它能让我们访问到拥有该电话的用户。我们可以使用 php belongsTo 方法来定义反向关联, php belongsTo 方法与 php hasOne 方法相对应:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;class Phone extends Model{/*** 获取拥有该手机的用户。*/public function user(): BelongsTo{return $this->belongsTo(User::class);}}
在调用 php user 方法时,Eloquent 会尝试查找一个 php User 模型,该 php User 模型上的 php id 字段会与 php Phone 模型上的 php user_id 字段相匹配。
Eloquent 通过关联方法(php user)的名称并使用 php _id 作为后缀名来确定外键名称。因此,在本例中,Eloquent 会假设 php Phone 模型有一个 php user_id 字段。但是,如果 php Phone 模型的外键不是 php user_id,这时你可以给 php belongsTo 方法的第二个参数传递一个自定义键名:
/*** 获取拥有该手机的用户。*/public function user(): BelongsTo{return $this->belongsTo(User::class, 'foreign_key');}
如果父模型的主键未使用 php id 作为字段名,或者你想要使用其他的字段来匹配相关联的模型,那么你可以向 php belongsTo 方法传递第三个参数,这个参数是在父模型中自己定义的字段名称:
/*** 获取拥有此电话的用户*/public function user(): BelongsTo{return $this->belongsTo(User::class, 'foreign_key', 'owner_key');}
一对多
当要定义一个模型是其他 (一个或者多个)模型的父模型这种关系时,可以使用一对多关联。例如,一篇博客可以有很多条评论。和其他模型关联一样,一对多关联也是在 Eloquent 模型文件中用一个方法来定义的:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasMany;class Post extends Model{/*** 获取这篇博客的所有评论*/public function comments(): HasMany{return $this->hasMany(Comment::class);}}
注意,Eloquent 将会自动为 php Comment 模型选择一个合适的外键。通常,这个外键是通过使用父模型的「蛇形命名」方式,然后再加上 php _id. 的方式来命名的。因此,在上面这个例子中,Eloquent 将会默认 php Comment 模型的外键是 php post_id 字段。
如果关联方法被定义,那么我们就可以通过 php comments 属性来访问相关的评论 集合。注意,由于 Eloquent 提供了「动态属性」,所以我们就可以像访问模型属性一样来访问关联方法:
use App\Models\Post;$comments = Post::find(1)->comments;foreach ($comments as $comment) {// ...}
由于所有的关系都可以看成是查询构造器,所以你也可以通过链式调用的方式,在 php comments 方法中继续添加条件约束:
$comment = Post::find(1)->comments()->where('title', 'foo')->first();
像 php hasOne 方法一样,你也可以通过将附加参数传递给 php hasMany 方法来覆盖外键和本地键:
// 覆盖外键return $this->hasMany(Comment::class, 'foreign_key');// 覆盖外键和本地键return $this->hasMany(Comment::class, 'foreign_key', 'local_key');
一对多 (反向) / 属于
现在我们可以访问一篇文章的所有评论,下面我们可以定义一个关联关系,从而让我们可以通过一条评论来获取到它所属的文章。这个关联关系是 php hasMany 的反向,可以在子模型中通过 php belongsTo 方法来定义这种关联关系:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;class Comment extends Model{/*** 获取这条评论所属的文章。*/public function post(): BelongsTo{return $this->belongsTo(Post::class);}}
如果定义了这种关联关系,那么我们就可以通过 php Comment 模型中的 php post 「动态属性」来获取到这条评论所属的文章:
use App\Models\Comment;$comment = Comment::find(1);return $comment->post->title;
在上面这个例子中,Eloquent 将会尝试寻找 php Post 模型中的 php id 字段与 php Comment 模型中的 php post_id 字段相匹配。
Eloquent 通过检查关联方法的名称,从而在关联方法名称后面加上 php _ ,然后再加上父模型 (Post)的主键名称,以此来作为默认的外键名。因此,在上面这个例子中,Eloquent 将会默认 php Post 模型在 php comments 表中的外键是 php post_id。
但是,如果你的外键不遵循这种约定的话,那么你可以传递一个自定义的外键名来作为 php belongsTo 方法的第二个参数:
/*** 获取这条评论所属的文章。*/public function post(): BelongsTo{return $this->belongsTo(Post::class, 'foreign_key');}
如果你的父表不使用 php id 作为主键,或者你希望使用不同的列来关联模型,你可以将第三个参数传递给 php belongsTo 方法,指定父表的自定义键:
/*** 获取这条评论所属的文章。*/public function post(): BelongsTo{return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');}
默认模型
当 php belongsTo,php hasOne,php hasOneThrough 和 php morphOne 这些关联方法返回 php null 的时候,你可以定义一个默认的模型返回。该模式通常被称为 空对象模式,它可以帮你省略代码中的一些条件判断。在下面这个例子中,如果 php Post 模型中没有用户,那么 php user 关联关系将会返回一个空的 php App\Models\User 模型:
/*** 获取文章的作者。*/public function user(): BelongsTo{return $this->belongsTo(User::class)->withDefault();}
可以向 php withDefault 方法传递数组或者闭包来填充默认模型的属性。
/*** 获取文章的作者。*/public function user(): BelongsTo{return $this->belongsTo(User::class)->withDefault(['name' => 'Guest Author',]);}/*** 获取文章的作者。*/public function user(): BelongsTo{return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {$user->name = 'Guest Author';});}
查询所属关系
在查询「所属」的子模型时,可以构建 php where 语句来检索相应的 Eloquent 模型:
use App\Models\Post;$posts = Post::where('user_id', $user->id)->get();
但是,你会发现使用 php whereBelongsTo 方法更方便,它会自动确定给定模型的正确关系和外键:
$posts = Post::whereBelongsTo($user)->get();
你还可以向 php whereBelongsTo 方法提供一个 集合 实例。 这样 Laravel 将检索属于集合中任何父模型的子模型:
$users = User::where('vip', true)->get();$posts = Post::whereBelongsTo($users)->get();
默认情况下,Laravel 将根据模型的类名来确定给定模型的关联关系; 你也可以通过将关系名称作为 php whereBelongsTo 方法的第二个参数来手动指定关系名称:
$posts = Post::whereBelongsTo($user, 'author')->get();
一对多检索
有时一个模型可能有许多相关模型,如果你想很轻松的检索「最新」或「最旧」的相关模型。例如,一个 php User 模型可能与许多 php Order 模型相关,但你想定义一种方便的方式来与用户最近下的订单进行交互。 可以使用 php hasOne 关系类型结合 php ofMany 方法来完成此操作:
/*** 获取用户最新的订单。*/public function latestOrder(): HasOne{return $this->hasOne(Order::class)->latestOfMany();}
同样,你可以定义一个方法来检索 「oldest」或第一个相关模型:
/*** 获取用户最早的订单。*/public function oldestOrder(): HasOne{return $this->hasOne(Order::class)->oldestOfMany();}
默认情况下,php latestOfMany 和 php oldestOfMany 方法将根据模型的主键检索最新或最旧的相关模型,该主键必须是可排序的。 但是,有时你可能希望使用不同的排序条件从更大的关系中检索单个模型。
例如,使用 php ofMany 方法,可以检索用户最昂贵的订单。 php ofMany 方法接受可排序列作为其第一个参数,以及在查询相关模型时应用哪个聚合函数(php min 或 php max):
/*** 获取用户最昂贵的订单。*/public function largestOrder(): HasOne{return $this->hasOne(Order::class)->ofMany('price', 'max');}
[!WARNING]
因为 PostgreSQL 不支持对 UUID 列执行php MAX函数,所以目前无法将一对多关系与 PostgreSQL UUID 列结合使用。
转换 一对多关联 为 一对一关联
通常,当使用 php latestOfMany, php oldestOfMany, 或者 php ofMany 方法检索单个模型时,该模型已经有一个 「 has many 」 关联。为了方便,Laravel 允许调用已有「 has many 」关联的 php one方法 , 轻松转换此关系为 「 has one 」关联。
/*** 获取用户的订单。*/public function orders(): HasMany{return $this->hasMany(Order::class);}/*** 获取用户最昂贵的订单。*/public function largestOrder(): HasOne{return $this->orders()->one()->ofMany('price', 'max');}
进阶一对多关联
可以构建更高级的「一对多」关联。例如,一个 php Product 模型可能有许多关联的 php Price 模型,即使在新定价发布后,这些模型也会保留在系统中。此外,产品的新定价数据能够通过 php published_at 列提前发布,以便在未来某日生效。
因此,我们需要检索最新的发布定价。 此外,如果两个价格的发布日期相同,我们优先选择 ID 更大的价格。 为此,我们必须将一个数组传递给 php ofMany 方法,其中包含确定最新价格的可排序列。此外,将提供一个闭包作为 php ofMany 方法的第二个参数。此闭包将负责向关系查询添加额外的发布日期约束:
/*** 获取产品的当前定价。*/public function currentPricing(): HasOne{return $this->hasOne(Price::class)->ofMany(['published_at' => 'max','id' => 'max',], function (Builder $query) {$query->where('published_at', '<', now());});}
远程一对一
「远程一对一」关联定义了与另一个模型的一对一的关联。然而,这种关联是声明的模型通过第三个模型来与另一个模型的一个实例相匹配。
例如,在一个汽车维修的应用程序中,每一个 php Mechanic 模型都与一个 php Car 模型相关联,同时每一个 php Car 模型也和一个 php Owner 模型相关联。虽然维修师(mechanic)和车主(owner)在数据库中并没有直接的关联,但是维修师可以通过 php Car 模型来找到车主。让我们来看看定义这种关联所需要的数据表:
mechanicsid - integername - stringcarsid - integermodel - stringmechanic_id - integerownersid - integername - stringcar_id - integer
既然我们已经了解了远程一对一的表结构,那么我们就可以在 php Mechanic 模型中定义这种关联:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasOneThrough;class Mechanic extends Model{/*** 获取汽车的主人。*/public function carOwner(): HasOneThrough{return $this->hasOneThrough(Owner::class, Car::class);}}
传递给 php hasOneThrough 方法的第一个参数是我们希望访问的最终模型的名称,而第二个参数是中间模型的名称。
或者,如果相关的关联已经在关联中涉及的所有模型上被定义,你可以通过调用 php through 方法和提供这些关联的名称来流式定义一个「远程一对一」关联。例如,php Mechanic 模型有一个 php cars 关联,php Car 模型有一个 php owner 关联,你可以这样定义一个连接维修师和车主的「远程一对一」关联:
// 基于字符串的语法...return $this->through('cars')->has('owner');// 动态语法...return $this->throughCars()->hasOwner();
键名约定
当使用远程一对一进行关联查询时,Eloquent 将会使用约定的外键名。如果你想要自定义相关联的键名的话,可以传递两个参数来作为 php hasOneThrough 方法的第三个和第四个参数。第三个参数是中间表的外键名。第四个参数是最终想要访问的模型的外键名。第五个参数是当前模型的本地键名,第六个参数是中间模型的本地键名:
class Mechanic extends Model{/*** 获取汽车的主人*/public function carOwner(): HasOneThrough{return $this->hasOneThrough(Owner::class,Car::class,'mechanic_id', // 汽车表的外键mechanic_id...'car_id', // 车主表的外键car_id...'id', // 机械师表的本地键...'id' // 汽车表的本地键...);}}
如果所涉及的模型已经定义了相关关系,可以调用 php through 方法并提供关系名来定义「远程一对一」关联。该方法的优点是重复使用已有关系上定义的主键约定:
// 基本语法...return $this->through('cars')->has('owner');// 动态语法...return $this->throughCars()->hasOwner();
远程一对多
「远程一对多」关联是可以通过中间关系来实现远程一对多的。例如,我们正在构建一个像 Laravel Vapor 这样的部署平台。一个 php Project 模型可以通过一个中间的 php Environment 模型来访问许多个 php Deployment 模型。就像上面的这个例子,可以在给定的 environment 中很方便的获取所有的 deployments。下面是定义这种关联关系所需要的数据表:
projectsid - integername - stringenvironmentsid - integerproject_id - integername - stringdeploymentsid - integerenvironment_id - integercommit_hash - string
既然我们已经检查了关系的表结构,现在让我们在 php Project 模型上定义该关系:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasManyThrough;class Project extends Model{/*** Get all of the deployments for the project.*/public function deployments(): HasManyThrough{return $this->hasManyThrough(Deployment::class, Environment::class);}}
php hasManyThrough 方法中传递的第一个参数是我们希望访问的最终模型名称,而第二个参数是中间模型的名称。
或者,所有模型上都定义好了关系,你可以通过调用 php through 方法并提供这些关系的名称来定义「has-many-through」关系。例如,如果 php Project 模型具有 php environments 关系,而 php Environment 模型具有 php deployments 关系,则可以定义连接 project 和 deployments 的「has-many-through」关系,如下所示:
// 基于字符串的语法...return $this->through('environments')->has('deployments');// 动态语法...return $this->throughEnvironments()->hasDeployments();
虽然 php Deployment 模型的表格不包含 php project_id 列,但 php hasManyThrough 关系通过 php $project->deployments 提供了访问项目的部署方式。为了检索这些模型,Eloquent 在中间的 php Environment 模型表中检查 php project_id 列。在找到相关的 environment ID 后,它们被用来查询 php Deployment 模型。
键名约定
在执行关系查询时,通常会使用典型的 Eloquent 外键约定。如果你想要自定义关系键名,可以将它们作为 php hasManyThrough 方法的第三个和第四个参数传递。第三个参数是中间模型上的外键名称。第四个参数是最终模型上的外键名称。第五个参数是本地键,而第六个参数是中间模型的本地键:
class Project extends Model{public function deployments(): HasManyThrough{return $this->hasManyThrough(Deployment::class,Environment::class,'project_id', // 在 environments 表上的外键...'environment_id', // 在 deployments 表上的外键...'id', // 在 projects 表上的本地键...'id' // 在 environments 表上的本地键...);}}
或者,所有模型上都定义好了关系,你可以通过调用 php through 方法并提供这些关系的名称来流畅的定义「has-many-through」关系。这种方法的优点是可以复用现有关系中已定义的键的约束:
// 基于字符串的语法...return $this->through('environments')->has('deployments');// 动态语法...return $this->throughEnvironments()->hasDeployments();
多对多关联
多对多关联比 php hasOne 和 php hasMany 关联略微复杂。一个多对多关系的例子,在应用中一个用户可以拥有多个角色,同时这些角色也可以分配给其他用户。例如,一个用户可是「作者」和「编辑」;但是,这些角色也可以分配给其他用户。所以,一个用户可以拥有多个角色,一个角色可以分配给多个用户。
表结构
要定义这种关联,需要三个数据库表: php users, php roles 和 php role_user。php role_user 表的命名是由关联的两个模型按照字母顺序来的,并且包含了 php user_id 和 php role_id 字段。该表用作链接 php users 和 php roles 的中间表。
特别提醒,由于角色可以属于多个用户,因此我们不能简单地在 php roles 表上放置 php user_id 列。如果这样,这意味着角色只能属于一个用户。为了支持将角色分配给多个用户,需要使用 php role_user 表。我们可以这样总结关系的表结构:
usersid - integername - stringrolesid - integername - stringrole_useruser_id - integerrole_id - integer
模型结构
多对多关联是通过调用 php belongsToMany 方法结果的方法来定义的。php belongsToMany 方法由 php Illuminate\Database\Eloquent\Model 基类提供,所有应用程序的 Eloquent 模型都使用该基类。例如,让我们在 php User 模型上定义一个 php roles 方法。传递给此方法的第一个参数是相关模型类的名称:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany;class User extends Model{/*** 属于用户的角色。*/public function roles(): BelongsToMany{return $this->belongsToMany(Role::class);}}
定义关系后,可以使用 php roles 动态关系属性访问用户的角色:
use App\Models\User;$user = User::find(1);foreach ($user->roles as $role) {// ...}
由于所有的关系也可以作为查询构建器,你可以通过调用 php roles() 方法链式的在查询上添加关系条件:
$roles = User::find(1)->roles()->orderBy('name')->get();
为了确定关系的中间表的表名,Eloquent 会按字母顺序连接两个相关的模型名。你也可以随意覆盖此约定。通过将第二个参数传递给 php belongsToMany 方法来做到这一点:
return $this->belongsToMany(Role::class, 'role_user');
除了自定义连接表的表名,你还可以通过传递额外的参数到 php belongsToMany 方法来定义该表中字段的键名。第三个参数是定义此关联的模型在连接表里的外键名,第四个参数是另一个模型在连接表里的外键名:
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
定义反向关系
要定义多对多的「反向」关联,只需要在关联模型中定义一个方法并返回调用 php belongsToMany 方法的结果。为了完成我们的用户 / 角色例子,让我们在 php Role 模型中定义 php users 方法:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany;class Role extends Model{/*** 属于角色的用户。*/public function users(): BelongsToMany{return $this->belongsToMany(User::class);}}
如你所见,除了引用 php App\Models\User 模型之外,该关系的定义与其对应的 php User 模型完全相同。 由于我们复用了 php belongsToMany 方法,所以在定义多对多关系的「反向」关系时,所有常用的表和键自定义选项都可用。
获取中间表字段
如上所述,处理多对多关系需要一个中间表。 Eloquent 提供了一些非常有用的方式来与这张表进行交互。 假设我们的 php User 模型关联了多个 php Role 模型。在获得这些关联对象后,我们可以使用模型的 php pivot 属性访问中间表的属性:
use App\Models\User;$user = User::find(1);foreach ($user->roles as $role) {echo $role->pivot->created_at;}
需要注意的是,我们获取的每个 php Role 模型对象,都会被自动赋予 php pivot 属性。这些属性包含一个代表中间表的模型。
默认情况下,php pivot 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');
如果你想让中间表自动维护 php created_at 和 php updated_at 时间戳,那么在定义关联时附加上 php withTimestamps 方法即可:
return $this->belongsToMany(Role::class)->withTimestamps();
注意
使用 Eloquent 自动维护时间戳的中间表需要同时具有php created_at和php updated_at时间戳字段。
自定义 pivot 属性名称
如前所述,可以通过 php pivot 属性在模型上访问中间表中的属性。 但是,你可以随意自定义此属性的名称,以更好地反映其在应用程序中的用途。
例如,如果你的应用程序包含可能订阅播客的用户,则用户和播客之间可能存在多对多关系。 如果是这种情况,你可能希望将中间表属性重命名为 php subscription 而不是 php pivot。 这可以在定义关系时使用 php 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;}
通过中间表过滤查询
你还可以在定义关系时使用 php wherePivot、php wherePivotIn、php wherePivotNotIn、php wherePivotBetween、php wherePivotNotBetween、php wherePivotNull 和 php wherePivotNotNull 方法过滤 php 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');
按中间表的列字段对查询结果进行排序
你可以使用 php orderByPivot 方法对 php belongsToMany 关系查询返回的结果进行排序。在以下示例中,我们将检索用户的最新徽章:
return $this->belongsToMany(Badge::class)->where('rank', 'gold')->orderByPivot('created_at', 'desc');
自定义中间表模型
如果你希望为多对多关系的中间表定义一个自定义模型,可以在定义关系时使用 php using 方法。这样可以在模型中定义额外的功能,比如自定义方法和类型转换。
自定义多对多中间表模型应继承 php Illuminate\Database\Eloquent\Relations\Pivot 类,而自定义多对多(多态)中间表模型应继承 php Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,我们在定义 php Role 模型的关联模型时,使用自定义中间表模型 php RoleUser:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsToMany;class Role extends Model{/*** 属于该角色的用户。*/public function users(): BelongsToMany{return $this->belongsToMany(User::class)->using(RoleUser::class);}}
定义 php RoleUser 模型时,应该继承 php Illuminate\Database\Eloquent\Relations\Pivot 类:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Relations\Pivot;class RoleUser extends Pivot{// ...}
注意
Pivot 模型不能使用php SoftDeletestrait。如果你需要中间表模型进行软删除,请考虑将你的中间表模型转换为一个实际的 Eloquent 模型。
自定义中间模型和自增 ID
如果你用一个自定义的中间模型定义了多对多的关系,而且这个中间模型拥有一个自增的主键,你应当确保这个自定义中间模型类中定义了一个 php incrementing 属性且其值为 php true.
/*** 标识 ID 是否自增** @var bool*/public $incrementing = true;
多态关系
多态关联允许子模型使用单个关联属于多种类型的模型。例如,假设你正在构建一个应用程序,允许用户共享博客文章和视频。在这样的应用程序中, php Comment 模型可能同时属于 php Post 和 php Video 模型。
一对一 (多态)
表结构
一对一多态关联类似于典型的一对一关系,但是子模型可以使用单个关联属于多个类型的模型。例如,一个博客 php Post 和一个 php User 可以共享到一个 php Image 模型的多态关联。使用一对一多态关联允许你拥有一个唯一图像的单个表,这些图像可以与帖子和用户关联。首先,让我们查看表结构:
postsid - integername - stringusersid - integername - stringimagesid - integerurl - stringimageable_id - integerimageable_type - string
请注意 php images 表上的 php imageable_id 和 php imageable_type 两列。 php imageable_id 列将包含帖子或用户的 ID 值, 而 php imageable_type 列将包含父模型的类名。php imageable_type 列用于 Eloquent 在访问 php imageable 关联时确定要返回哪种类型的父模型。在本例中,该列将包含 php App\Models\Post 或 php App\Models\User。
模型结构
接下来,让我们看一下构建这种关系所需的模型定义:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo;class Image extends Model{/*** 获取父级可变模型(用户或帖子)。*/public function imageable(): MorphTo{return $this->morphTo();}}use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphOne;class Post extends Model{/*** 获取帖子的图片。*/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{/*** 获取用户的图片。*/public function image(): MorphOne{return $this->morphOne(Image::class, 'imageable');}}
检索关系
一旦你定义了数据库表和模型,你可以通过你的模型访问这些关系。例如,要检索帖子的图片,我们可以访问 php image 动态关系属性:
use App\Models\Post;$post = Post::find(1);$image = $post->image;
你可以通过访问执行对 php morphTo 的调用的方法的名称来检索多态模型的父级。在这种情况下,这是 php Image 模型上的 php imageable 方法。因此,我们将访问该方法作为动态关系属性:
use App\Models\Image;$image = Image::find(1);$imageable = $image->imageable;
php Image 模型上的 php imageable 关系将返回一个 php Post 或 php User 实例,取决于哪种类型的模型拥有该图片。
键约定
如果需要,你可以指定多态子模型使用的「id」和「type」列的名称。如果这样做,请确保始终将关系的名称作为 php morphTo 方法的第一个参数传递。通常,这个值应该与方法名称匹配,因此你可以使用 PHP 的 php __FUNCTION__ 常量:
/*** 获取图片所属的模型。*/public function imageable(): MorphTo{return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');}
一对多(多态)
表结构
一对多多态关系类似于典型的一对多关系;然而,子模型可以使用单个关联属于多种类型的模型。例如,假设你的应用程序的用户可以在帖子和视频上「评论」。使用多态关系,你可以使用单个 php comments 表来包含帖子和视频的评论。首先,让我们看一下构建这种关系所需的表结构:
postsid - integertitle - stringbody - textvideosid - integertitle - stringurl - stringcommentsid - integerbody - textcommentable_id - integercommentable_type - string
模型结构
接下来,让我们看一下构建这种关系所需的模型定义:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo;class Comment extends Model{/*** 获取父级可变评论模型(帖子或视频)。*/public function commentable(): MorphTo{return $this->morphTo();}}use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphMany;class Post extends Model{/*** 获取所有帖子的评论。*/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{/*** 获取所有视频的评论。*/public function comments(): MorphMany{return $this->morphMany(Comment::class, 'commentable');}}
检索关系
一旦你定义了数据库表和模型,你可以通过模型的动态关系属性访问这些关系。例如,要访问帖子的所有评论,我们可以使用 php comments 动态属性:
use App\Models\Post;$post = Post::find(1);foreach ($post->comments as $comment) {// ...}
你也可以通过访问执行对 php morphTo 的调用的方法的名称来检索多态子模型的父模型。在这种情况下,这是 php Comment 模型上的 php commentable 方法。因此,我们将访问该方法作为动态关系属性,以便访问评论的父模型:
use App\Models\Comment;$comment = Comment::find(1);$commentable = $comment->commentable;
php Comment 模型上的 php commentable 关系将返回一个 php Post 或 php Video 实例,取决于评论的父模型是哪种类型。
多个中的一个(多态)
有时,一个模型可能有许多相关模型,但你想要轻松地检索关系中的「最新」或「最旧」相关模型。例如,一个 php User 模型可能与许多 php Image 模型相关联,但你想要定义一种方便的方式与用户最近上传的图片进行交互。你可以使用 php morphOne 关系类型结合 php ofMany 方法来实现这一点:
/*** 获取用户最新的图片。*/public function latestImage(): MorphOne{return $this->morphOne(Image::class, 'imageable')->latestOfMany();}
同样,你可以定义一个方法来检索关系中的「最旧」或第一个相关模型:
/*** 获取用户最早的图片。*/public function oldestImage(): MorphOne{return $this->morphOne(Image::class, 'imageable')->oldestOfMany();}
默认情况下,php latestOfMany 和 php oldestOfMany 方法将基于模型的主键检索最新或最旧的相关模型,这些主键必须是可排序的。然而,有时你可能希望使用不同的排序标准从更大的关系中检索单个模型。
例如,使用 php ofMany 方法,你可以检索用户最「喜欢」的图片。php ofMany 方法接受可排序列作为第一个参数,并在查询相关模型时应用哪种聚合函数(php min 或 php max):
/*** 获取用户最受欢迎的图片。*/public function bestImage(): MorphOne{return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');}
注意
可以构建更复杂的「多个中的一个」关系。有关更多信息,请参阅一个中的多个文档。
多对多(多态)
表结构
多对多多态关系比「morph one」和「morph many」关系稍微复杂一些。例如,php Post 模型和 php Video 模型可以共享与 php Tag 模型的多态关系。在这种情况下使用多对多多态关系,你的应用程序可以拥有一个包含可与帖子或视频关联的唯一标签的单个表。首先,让我们看一下构建这种关系所需的表结构:
postsid - integername - stringvideosid - integername - stringtagsid - integername - stringtaggablestag_id - integertaggable_id - integertaggable_type - string
注意
在深入研究多对多多态关系之前,你可能会从阅读关于典型多对多关系的文档中受益。
模型结构
接下来,我们准备在模型上定义关系。php Post 和 php Video 模型都将包含一个 php tags 方法,该方法调用基础 Eloquent 模型类提供的 php morphToMany 方法。
php morphToMany 方法接受相关模型的名称以及「关系名称」。根据我们分配给中间表名称及其包含的键的名称,我们将将该关系称为「taggable」:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphToMany;class Post extends Model{/*** 获取该帖子的所有标签。*/public function tags(): MorphToMany{return $this->morphToMany(Tag::class, 'taggable');}}
定义关系的反向关系
接下来,在 php Tag 模型上,你应该为每个可能的父模型定义一个方法。因此,在这个例子中,我们将定义一个 php posts 方法和一个 php videos 方法。这两个方法都应该返回 php morphedByMany 方法的结果。
php morphedByMany 方法接受相关模型的名称以及「关系名称」。根据我们分配给中间表名称及其包含的键的名称,我们将将该关系称为「taggable」:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphToMany;class Tag extends Model{/*** 获取分配了该标签的所有帖子。*/public function posts(): MorphToMany{return $this->morphedByMany(Post::class, 'taggable');}/*** 获取分配了该标签的所有视频。*/public function videos(): MorphToMany{return $this->morphedByMany(Video::class, 'taggable');}}
检索关系
一旦定义了数据库表和模型,你可以通过你的模型访问关系。例如,要访问帖子的所有标签,你可以使用 php tags 动态关系属性:
use App\Models\Post;$post = Post::find(1);foreach ($post->tags as $tag) {// ...}
你可以通过访问执行对 php morphedByMany 调用的方法的名称,从多态子模型中检索多态关系的父模型。在这种情况下,就是 php Tag 模型上的 php posts 或 php videos 方法:
use App\Models\Tag;$tag = Tag::find(1);foreach ($tag->posts as $post) {// ...}foreach ($tag->videos as $video) {// ...}
自定义多态类型
默认情况下,Laravel 将使用完全限定的类名来存储相关模型的「类型」。例如,考虑上面的一对多关系示例,其中 php Comment 模型可以属于 php Post 或 php Video 模型,默认的 php commentable_type 分别为 php App\Models\Post 或 php App\Models\Video。然而,你可能希望将这些值与应用程序的内部结构解耦。
例如,我们可以使用简单的字符串(如 php post 和 php video)而不是使用模型名称作为「类型」。通过这样做,即使模型被重命名,我们数据库中的多态「类型」列值也将保持有效:
use Illuminate\Database\Eloquent\Relations\Relation;Relation::enforceMorphMap(['post' => 'App\Models\Post','video' => 'App\Models\Video',]);
你可以在 php App\Providers\AppServiceProvider 类的 php boot 方法中调用 php enforceMorphMap 方法,或者如果你愿意,也可以创建一个单独的服务提供程序。
你可以使用模型的 php getMorphClass 方法在运行时确定给定模型的多态别名。反之,你可以使用 php Relation::getMorphedModel 方法确定与多态别名关联的完全限定类名:
use Illuminate\Database\Eloquent\Relations\Relation;$alias = $post->getMorphClass();$class = Relation::getMorphedModel($alias);
警告
在向现有应用程序添加「morph map」时,数据库中仍包含完全限定类的每个可多态化的php *_type列值都需要转换为其「映射」名称。
动态关系
你可以使用 php resolveRelationUsing 方法在运行时定义 Eloquent 模型之间的关系。虽然在正常应用程序开发中通常不建议使用,但在开发 Laravel 包时偶尔可能会很有用。
php resolveRelationUsing 方法接受所需的关系名称作为其第一个参数。传递给该方法的第二个参数应该是一个接受模型实例并返回有效的 Eloquent 关系定义的闭包。通常,你应该在服务提供程序的 php 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 查询之前继续在关系查询上链约束。
例如,想象一个博客应用程序,其中 php User 模型有许多关联的 php Post 模型:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\HasMany;class User extends Model{/*** 获取用户的所有帖子。*/public function posts(): HasMany{return $this->hasMany(Post::class);}}
你可以查询 php posts 关系并向关系添加额外约束,如下所示:
use App\Models\User;$user = User::find(1);$user->posts()->where('active', 1)->get();
你可以在关系上使用 Laravel 查询构建器的任何方法,因此请务必查阅查询构建器文档,了解所有可用的方法。
在关系之后链式使用 orWhere 子句
如上面的示例所示,你可以在查询关系时添加额外约束。但是,在将 php orWhere 子句链接到关系时要小心,因为 php orWhere 子句将在与关系约束相同级别逻辑分组:
$user->posts()->where('active', 1)->orWhere('votes', '>=', 100)->get();
上面的示例将生成以下 SQL。如你所见,php or 子句指示查询返回具有超过 100 票的 任何 帖子。查询不再受限于特定用户:
select *from postswhere 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 postswhere user_id = ? and (active = 1 or votes >= 100)
关系方法 vs. 动态属性
如果你不需要向 Eloquent 关系查询添加额外约束,可以像访问属性一样访问关系。例如,继续使用我们的 php User 和 php Post 示例模型,你可以这样访问用户的所有帖子:
use App\Models\User;$user = User::find(1);foreach ($user->posts as $post) {// ...}
动态关系属性执行「懒加载」,意味着只有在你实际访问它们时才会加载它们的关系数据。因此,开发人员通常使用预加载来预加载他们知道在加载模型后将被访问的关系。预加载大大减少了必须执行的 SQL 查询,以加载模型的关系。
查询关系存在性
在检索模型记录时,你可能希望根据关系的存在性限制结果。例如,想象一下,你想检索所有至少有一条评论的博客文章。为此,你可以将关系的名称传递给 php has 和 php orHas 方法:
use App\Models\Post;// 检索至少有一条评论的所有帖子...$posts = Post::has('comments')->get();
你还可以指定运算符和计数值以进一步自定义查询:
// 检索至少有三条或更多评论的所有帖子...$posts = Post::has('comments', '>=', 3)->get();
可以使用「点」符号构建嵌套的 php has 语句。例如,你可以检索至少有一条具有至少一张图片的评论的所有帖子:
// 检索至少有一条带有图片的评论的帖子...$posts = Post::has('comments.images')->get();
如果你需要更多功能,可以使用 php whereHas 和 php orWhereHas 方法在 php has 查询上定义额外的查询约束,例如检查评论的内容:
use Illuminate\Database\Eloquent\Builder;// 检索至少有一条包含类似 code% 的单词的评论的帖子...$posts = Post::whereHas('comments', function (Builder $query) {$query->where('content', 'like', 'code%');})->get();// 检索至少有十条包含类似 code% 的单词的评论的帖子...$posts = Post::whereHas('comments', function (Builder $query) {$query->where('content', 'like', 'code%');}, '>=', 10)->get();
警告
目前 Eloquent 不支持跨数据库查询关系存在性。这些关系必须存在于同一个数据库中。
内联关系存在性查询
如果你想要通过一个简单的 where 条件查询关系的存在性,你可能会发现使用 php whereRelation、php orWhereRelation、php whereMorphRelation 和 php orWhereMorphRelation 方法更方便。例如,我们可以查询所有具有未批准评论的帖子:
use App\Models\Post;$posts = Post::whereRelation('comments', 'is_approved', false)->get();
当然,类似于查询构建器的 php where 方法调用,你也可以指定一个操作符:
$posts = Post::whereRelation('comments', 'created_at', '>=', now()->subHour())->get();
查询关系缺失
在检索模型记录时,你可能希望根据关系的缺失限制结果。例如,假设你想要检索所有没有任何评论的博客帖子。为此,你可以将关系的名称传递给 php doesntHave 和 php orDoesntHave 方法:
use App\Models\Post;$posts = Post::doesntHave('comments')->get();
如果你需要更多的功能,你可以使用 php whereDoesntHave 和 php orWhereDoesntHave 方法为你的 php doesntHave 查询添加额外的查询约束,比如检查评论的内容:
use Illuminate\Database\Eloquent\Builder;$posts = Post::whereDoesntHave('comments', function (Builder $query) {$query->where('content', 'like', 'code%');})->get();
你可以使用「点」符号对嵌套关系执行查询。例如,以下查询将检索所有没有评论的帖子;然而,带有来自未被禁止的作者的评论的帖子将包含在结果中:
use Illuminate\Database\Eloquent\Builder;$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {$query->where('banned', 0);})->get();
查询 Morph To 关系
要查询「morph to」关系的存在性,你可以使用 php whereHasMorph 和 php whereDoesntHaveMorph 方法。这些方法将关系的名称作为第一个参数。接下来,方法接受你希望在查询中包含的相关模型的名称。最后,你可以提供一个自定义关系查询的闭包:
use App\Models\Comment;use App\Models\Post;use App\Models\Video;use Illuminate\Database\Eloquent\Builder;// 检索与标题类似于 code% 的帖子或视频关联的评论...$comments = Comment::whereHasMorph('commentable',[Post::class, Video::class],function (Builder $query) {$query->where('title', 'like', 'code%');})->get();// 检索与标题不类似于 code% 的帖子关联的评论...$comments = Comment::whereDoesntHaveMorph('commentable',Post::class,function (Builder $query) {$query->where('title', 'like', 'code%');})->get();
偶尔你可能需要根据多态关联模型的「类型」添加查询约束。传递给 php whereHasMorph 方法的闭包可以接收 php $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 模型
你可以将 php * 作为通配符值,而不是提供可能的多态模型数组。这将指示 Laravel 从数据库中检索所有可能的多态类型。为了执行这个操作,Laravel 将执行额外的查询:
use Illuminate\Database\Eloquent\Builder;$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {$query->where('title', 'like', 'foo%');})->get();
聚合相关模型
计算相关模型数量
有时你可能想要计算给定关系的相关模型数量,而不实际加载这些模型。为了实现这一点,你可以使用 php withCount 方法。php withCount 方法将在结果模型上放置一个 php {relation}_count 属性:
use App\Models\Post;$posts = Post::withCount('comments')->get();foreach ($posts as $post) {echo $post->comments_count;}
通过向 php 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;
延迟计数加载
使用 php loadCount 方法,你可以在已经检索到父模型之后加载关系计数:
$book = Book::first();$book->loadCount('genres');
如果你需要在计数查询上设置额外的查询约束,你可以传递一个以你希望计数的关系为键的数组。数组值应该是接收查询构建器实例的闭包:
$book->loadCount(['reviews' => function (Builder $query) {$query->where('rating', 5);}])
关联计数和自定义选择语句
如果你将 php withCount 与 php select 语句结合使用,请确保在 php select 方法之后调用 php withCount:
$posts = Post::select(['title', 'body'])->withCount('comments')->get();
其他聚合函数
除了 php withCount 方法外,Eloquent 还提供了 php withMin、php withMax、php withAvg、php withSum 和 php withExists 方法。这些方法将在你的结果模型上放置一个 php {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;}
与 php loadCount 方法类似,这些方法的延迟版本也是可用的。这些额外的聚合操作可以在已经检索到的 Eloquent 模型上执行:
$post = Post::first();$post->loadSum('comments', 'votes');
如果你将这些聚合方法与 php select 语句结合使用,请确保在 php select 方法之后调用聚合方法:
$posts = Post::select(['title', 'body'])->withExists('comments')->get();
在 Morph To 关系上计数相关模型
如果你想要预加载「morph to」关系,并且为该关系可能返回的各种实体的相关模型计数,你可以在 php with 方法中结合 php morphTo 关系的 php morphWithCount 方法。
在这个例子中,假设 php Photo 和 php Post 模型可以创建 php ActivityFeed 模型。我们假设 php ActivityFeed 模型定义了一个名为 php parentable 的「morph to」关系,允许我们为给定的 php ActivityFeed 实例检索父 php Photo 或 php Post 模型。此外,让我们假设 php Photo 模型「拥有多个」 php Tag 模型,而 php Post 模型「拥有多个」 php Comment 模型。
现在,让我们想象我们想要检索 php ActivityFeed 实例,并预加载每个 php ActivityFeed 实例的 php parentable 父模型。此外,我们希望检索与每个父照片关联的标签数量,以及与每个父帖子关联的评论数量:
use Illuminate\Database\Eloquent\Relations\MorphTo;$activities = ActivityFeed::with(['parentable' => function (MorphTo $morphTo) {$morphTo->morphWithCount([Photo::class => ['tags'],Post::class => ['comments'],]);}])->get();
延迟计数加载
假设我们已经检索了一组 php ActivityFeed 模型,现在我们想要加载与活动源关联的各种 php parentable 模型的嵌套关系计数。你可以使用 php loadMorphCount 方法来实现这一点:
$activities = ActivityFeed::with('parentable')->get();$activities->loadMorphCount('parentable', [Photo::class => ['tags'],Post::class => ['comments'],]);
预加载
当将 Eloquent 关系视为属性访问时,相关模型是「懒加载」的。这意味着直到你首次访问属性时,关系数据才会被实际加载。然而,Eloquent 可以在查询父模型时「预加载」关系。预加载可以减轻「N + 1」查询问题。为了说明 N + 1 查询问题,考虑一个 php Book 模型,它「belongs to」 一个 php Author 模型:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;class Book extends Model{/*** 获取写作这本书的作者。*/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 次是为每本书的作者。
幸运的是,我们可以使用预加载来将此操作减少到只需两次查询。在构建查询时,你可以使用 php with 方法指定应该预加载哪些关系:
$books = Book::with('author')->get();foreach ($books as $book) {echo $book->author->name;}
对于这个操作,只会执行两次查询 - 一次查询以检索所有书籍,另一次查询以检索所有书籍的作者:
select * from booksselect * from authors where id in (1, 2, 3, 4, 5, ...)
预加载多个关联
有时你可能需要预加载几个不同的关联。要做到这一点,只需将关系数组传递给 php with 方法:
$books = Book::with(['author', 'publisher'])->get();
嵌套预加载
要预加载关联的关联,你可以使用「点」语法。例如,让我们一次性加载所有书籍的作者和作者的个人联系方式:
$books = Book::with('author.contacts')->get();
或者,你可以通过向 php with 方法提供嵌套数组来指定嵌套预加载的关系,当预加载多个嵌套关系时,这种方法可能更方便:
$books = Book::with(['author' => ['contacts','publisher',],])->get();
嵌套预加载 morphTo 关联
如果你想要预加载一个 php morphTo 关联,以及可能由该关联返回的各个实体上的嵌套关联,你可以在 php with 方法中结合使用 php morphTo 关联的 php morphWith 方法。为了帮助说明这种方法,让我们考虑以下模型:
use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo;class ActivityFeed extends Model{/*** 获取活动源记录的父级。*/public function parentable(): MorphTo{return $this->morphTo();}}
在这个例子中,假设 php Event、php Photo 和 php Post 模型可以创建 php ActivityFeed 模型。此外,假设 php Event 模型属于 php Calendar 模型,php Photo 模型与 php Tag 模型相关联,php Post 模型属于 php Author 模型。
使用这些模型定义和关系,我们可以检索 php ActivityFeed 模型实例,并预加载所有 php 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();
警告
当使用此功能时,你应该始终在希望检索的列列表中包括php id列和任何相关的外键列。
默认预加载
有时候在检索模型时,你可能希望总是加载一些关联数据。为了实现这一点,你可以在模型上定义一个 php $with 属性:
namespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;class Book extends Model{/*** 应该始终加载的关联。** @var array*/protected $with = ['author'];/*** 获取撰写本书的作者。*/public function author(): BelongsTo{return $this->belongsTo(Author::class);}/*** 获取本书的流派。*/public function genre(): BelongsTo{return $this->belongsTo(Genre::class);}}
如果你想要在单个查询中从 php $with 属性中移除一个项目,你可以使用 php without 方法:
$books = Book::without('author')->get();
如果你想要覆盖 php $with 属性中的所有项目以进行单个查询,你可以使用 php withOnly 方法:
$books = Book::withOnly('genre')->get();
约束预加载
有时候你可能希望预加载一个关联数据,但同时为预加载查询指定额外的查询条件。你可以通过将一个关联数据数组传递给 php with 方法来实现这一点,其中数组键是关联名称,数组值是一个闭包,该闭包添加额外的约束条件到预加载查询中:
use App\Models\User;use Illuminate\Contracts\Database\Eloquent\Builder;$users = User::with(['posts' => function (Builder $query) {$query->where('title', 'like', '%code%');}])->get();
在这个例子中,Eloquent 只会预加载那些标题列包含单词 php code 的帖子。你可以调用其他 查询构建器 方法来进一步定制预加载操作:
$users = User::with(['posts' => function (Builder $query) {$query->orderBy('created_at', 'desc');}])->get();
对 morphTo 关联的预加载进行约束
如果你正在预加载一个 php morphTo 关联,Eloquent 将运行多个查询以获取每种相关模型。你可以使用 php MorphTo 关联的 php 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 只会预加载未隐藏的帖子和具有「educational」类型值的视频。
根据关联存在性约束预加载
有时你可能需要同时检查关联的存在性并根据相同条件加载关联。例如,你可能希望仅检索符合给定查询条件的子 php Post 模型的 php User 模型,同时预加载匹配的帖子。你可以使用 php withWhereHas 方法来实现:
use App\Models\User;$users = User::withWhereHas('posts', function ($query) {$query->where('featured', true);})->get();
惰性预加载
有时你可能需要在已检索父模型之后预加载关联。例如,如果你需要动态决定是否加载相关模型,则这可能会很有用:
use App\Models\Book;$books = Book::all();if ($someCondition) {$books->load('author', 'publisher');}
如果你需要在预加载查询上设置额外的查询约束,你可以传递一个由你希望加载的关联作为键的数组。数组值应该是接收查询实例的闭包实例:
$author->load(['books' => function (Builder $query) {$query->orderBy('published_date', 'asc');}]);
要仅在尚未加载时加载关联,请使用 php loadMissing 方法:
$book->loadMissing('author');
嵌套预加载和 morphTo
如果你想要预加载一个 php morphTo 关联,以及可能由该关联返回的各种实体上的嵌套关联,你可以使用 php loadMorph 方法。
该方法接受 php morphTo 关联的名称作为第一个参数,以及一个模型/关联对的数组作为第二个参数。为了帮助说明这个方法,让我们考虑以下模型:
use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\MorphTo;class ActivityFeed extends Model{/*** 获取活动源记录的父级。*/public function parentable(): MorphTo{return $this->morphTo();}}
在这个例子中,假设 php Event、php Photo 和 php Post 模型可以创建 php ActivityFeed 模型。此外,假设 php Event 模型属于 php Calendar 模型,php Photo 模型与 php Tag 模型相关联,而 php Post 模型属于 php Author 模型。
使用这些模型定义和关联,我们可以检索 php ActivityFeed 模型实例,并预加载所有 php parentable 模型及其各自的嵌套关联:
$activities = ActivityFeed::with('parentable')->get()->loadMorph('parentable', [Event::class => ['calendar'],Photo::class => ['tags'],Post::class => ['author'],]);
防止懒加载
如前所述,预加载关联通常可以为你的应用程序提供显著的性能优势。因此,如果你希望的话,你可以指示 Laravel 始终阻止关联的懒加载。为了实现这一点,你可以调用基础 Eloquent 模型类提供的 php preventLazyLoading 方法。通常情况下,你应该在应用程序的 php AppServiceProvider 类的 php boot 方法中调用这个方法。
php preventLazyLoading 方法接受一个可选的布尔参数,指示是否应禁止懒加载。例如,你可能希望仅在非生产环境中禁用懒加载,这样即使在生产代码中意外存在懒加载关系,生产环境仍将正常运行:
use Illuminate\Database\Eloquent\Model;/*** 初始化任何应用程序服务。*/public function boot(): void{Model::preventLazyLoading(! $this->app->isProduction());}
在禁止懒加载后,当你的应用程序尝试懒加载任何 Eloquent 关系时,Eloquent 将抛出一个 php Illuminate\Database\LazyLoadingViolationException 异常。
你可以使用 php handleLazyLoadingViolationUsing 方法自定义懒加载违规行为。例如,使用这个方法,你可以指示懒加载违规行为仅被记录,而不是用异常中断应用程序的执行:
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {$class = $model::class;info("Attempted to lazy load [{$relation}] on model [{$class}].");});
插入和更新相关模型
save 方法
Eloquent 提供了方便的方法来将新模型添加到关系中。例如,也许你需要向帖子添加一条新评论。你可以使用关系的 php save 方法来插入评论,而不是手动设置 php Comment 模型上的 php post_id 属性:
use App\Models\Comment;use App\Models\Post;$comment = new Comment(['message' => 'A new comment.']);$post = Post::find(1);$post->comments()->save($comment);
请注意,我们没有将 php comments 关系作为动态属性访问。相反,我们调用了 php comments 方法来获取关系的实例。php save 方法将自动将适当的 php post_id 值添加到新的 php Comment 模型中。
如果需要保存多个相关模型,可以使用 php saveMany 方法:
$post = Post::find(1);$post->comments()->saveMany([new Comment(['message' => 'A new comment.']),new Comment(['message' => 'Another new comment.']),]);
php save 和 php saveMany 方法会持久化给定的模型实例,但不会将新持久化的模型添加到已加载到父模型的任何内存关系中。如果计划在使用 php save 或 php saveMany 方法后访问关系,可以使用 php refresh 方法重新加载模型及其关系:
$post->comments()->save($comment);$post->refresh();// 所有评论,包括新保存的评论...$post->comments;
递归保存模型和关系
如果想要php save你的模型及其所有关联关系,可以使用 php push 方法。在这个例子中,php Post 模型将被保存,以及它的评论和评论的作者:
$post = Post::find(1);$post->comments[0]->message = 'Message';$post->comments[0]->author->name = 'Author Name';$post->push();
php pushQuietly 方法可用于保存模型及其关联关系而不触发任何事件:
$post->pushQuietly();
create 方法
除了 php save 和 php saveMany 方法之外,还可以使用 php create 方法,它接受一个属性数组,创建一个模型,并将其插入数据库。php save 和 php create 之间的区别在于 php save 接受一个完整的 Eloquent 模型实例,而 php create 接受一个普通的 PHP php array。php create 方法将返回新创建的模型:
use App\Models\Post;$post = Post::find(1);$comment = $post->comments()->create(['message' => 'A new comment.',]);
你可以使用 php createMany 方法来创建多个相关模型:
$post = Post::find(1);$post->comments()->createMany([['message' => 'A new comment.'],['message' => 'Another new comment.'],]);
php createQuietly 和 php createManyQuietly 方法可用于创建模型而不触发任何事件:
$user = User::find(1);$user->posts()->createQuietly(['title' => 'Post title.',]);$user->posts()->createManyQuietly([['title' => 'First post.'],['title' => 'Second post.'],]);
你还可以使用 php findOrNew、php firstOrNew、php firstOrCreate 和 php updateOrCreate 方法来在关系上创建和更新模型。
注意
在使用php create方法之前,请确保查看批量赋值文档。
属于关系
如果想将子模型分配给新的父模型,可以使用 php associate 方法。在这个例子中,php User 模型定义了一个到 php Account 模型的 php belongsTo 关系。php associate 方法将在子模型上设置外键:
use App\Models\Account;$account = Account::find(10);$user->account()->associate($account);$user->save();
要从子模型中移除父模型,可以使用 php dissociate 方法。该方法将关系的外键设置为 php null:
$user->account()->dissociate();$user->save();
多对多关系
附加 / 分离
Eloquent 还提供了一些方法,使得处理多对多关系更加方便。例如,假设一个用户可以拥有多个角色,而一个角色也可以拥有多个用户。你可以使用 php attach 方法将一个角色附加到一个用户,通过在关系的中间表中插入一条记录:
use App\Models\User;$user = User::find(1);$user->roles()->attach($roleId);
当向模型附加关系时,你也可以传递一个包含要插入到中间表的附加数据的数组:
$user->roles()->attach($roleId, ['expires' => $expires]);
有时需要从用户中移除一个角色。要移除多对多关系记录,可以使用 php detach 方法。php detach 方法将从中间表中删除适当的记录;但是,两个模型将仍然保留在数据库中:
// 从用户中分离单个角色...$user->roles()->detach($roleId);// 从用户中分离所有角色...$user->roles()->detach();
为了方便起见,php attach 和 php detach 也接受ID数组作为输入:
$user = User::find(1);$user->roles()->detach([1, 2, 3]);$user->roles()->attach([1 => ['expires' => $expires],2 => ['expires' => $expires],]);
同步关联
你还可以使用 php sync 方法构建多对多关联。php sync 方法接受一个ID数组,放置在中间表上。不在给定数组中的任何ID将从中间表中删除。因此,在此操作完成后,只有给定数组中的ID将存在于中间表中:
$user->roles()->sync([1, 2, 3]);
你也可以传递附加的中间表值与ID:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果希望将相同的中间表值与每个同步模型ID一起插入,可以使用 php syncWithPivotValues 方法:
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
如果不想分离缺少在给定数组中的现有ID,可以使用 php syncWithoutDetaching 方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切换关联
多对多关系还提供了一个 php toggle 方法,用于「切换」给定相关模型的附加状态。如果给定的ID当前已附加,它将被分离。同样,如果它当前处于分离状态,它将被附加:
$user->roles()->toggle([1, 2, 3]);
你也可以传递附加表的额外中间值与ID:
$user->roles()->toggle([1 => ['expires' => true],2 => ['expires' => true],]);
更新中间表上的记录
如果你需要更新关系的中间表中的现有行,你可以使用 php updateExistingPivot 方法。该方法接受中间记录外键和要更新的属性数组:
$user = User::find(1);$user->roles()->updateExistingPivot($roleId, ['active' => false,]);
更新父级时间戳
当一个模型定义了一个 php belongsTo 或 php belongsToMany 关系到另一个模型,比如一个 php Comment 属于一个 php Post,有时在更新子模型时更新父模型的时间戳是有帮助的。
例如,当一个 php Comment 模型被更新时,你可能希望自动「触摸」拥有的 php Post 的 php updated_at 时间戳,使其设置为当前日期和时间。为了实现这一点,你可以在子模型中添加一个 php touches 属性,其中包含应在更新子模型时更新其 php updated_at 时间戳的关系名称:
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;class Comment extends Model{/*** 所有要被触摸的关系。** @var array*/protected $touches = ['post'];/*** 获取评论所属的帖子。*/public function post(): BelongsTo{return $this->belongsTo(Post::class);}}
警告
只有当子模型使用 Eloquent 的php save方法进行更新时,父模型的时间戳才会被更新。