Eloquent: 关联
简介
数据库表通常相互关联。例如,一篇博客文章可能有许多评论,或者一个订单对应一个下单用户。Eloquent 让这些关联的管理和使用变得简单,并支持多种常用的关联类型:
定义关联
Eloquent 关联在 Eloquent 模型类中以方法的形式呈现。如同 Eloquent 模型本身,关联也可以作为强大的 查询语句构造器,使用,提供了强大的链式调用和查询功能。例如,我们可以在 posts
关联的链式调用中附加一个约束条件:
$user->posts()->where('active', 1)->get();
不过在深入使用关联之前,让我们先学习如何定义每种关联类型。
一对一
一对一是最基本的数据库关系。 例如,一个 User
模型可能与一个 Phone
模型相关联。为了定义这个关联关系,我们要在 User
模型中写一个 phone
方法, 在 phone
方法中调用 hasOne
方法并返回其结果。hasOne
方法被定义在 Illuminate\Database\Eloquent\Model
这个模型基类中:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 获取与用户相关的电话记录
*/
public function phone()
{
return $this->hasOne(Phone::class);
}
}
hasOne
方法的第一个参数是关联模型的类名。一旦定义了模型关联,我们就可以使用 Eloquent 的动态属性获得相关的记录。动态属性允许你访问该关联方法,就像访问模型中定义的属性一样:
$phone = User::find(1)->phone;
Eloquent 基于父模型(User
)的名称来确定关联模型(Phone
)的外键名称。在本例中,会自动假定 Phone
模型有一个 user_id
的外 键。如果你想重写这个约定,可以传递第二个参数给 hasOne
方法:
return $this->hasOne(Phone::class, 'foreign_key');
另外,Eloquent 假设外键的值是与父模型的主键(Primary Key)相同的。换句话说,Eloquent 将会通过 Phone
记录的 user_id
列中查找与用户表的 id
列相匹配的值。如果你希望使用自定义的主键值,而不是使用 id
或者模型中的 $primaryKey
属性,你可以给 hasOne
方法传递第三个参数:
return $this->hasOne(Phone::class, 'foreign_key', 'local_key');
定义反向关联
我们已经能从 User
模型访问到 Phone
模型了。接下来,让我们再在 Phone
模型上定义一个关联,它能让我们访问到拥有该电话的用户。我们可以使用 belongsTo
方法来定义反向关联, belongsTo
方法与 hasOne
方法相对应:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* 获取拥有此电话的用户
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
在调用 user
方法时,Eloquent 会尝试查找一个 User
模型,该 User
模型上的 id
字段会与 Phone
模型上的 user_id
字段相匹配。
Eloquent 通过关联方法(user
)的名称并使用 _id
作为后缀名来确定外键名称。因此,在本例中,Eloquent 会假设 Phone
模型有一个 user_id
字段。但是,如果 Phone
模型的外键不是 user_id
,这时你可以给 belongsTo
方法的第二个参数传递一个自定义键名:
/**
* 获取拥有此电话的用户
*/
public function user()
{
return $this->belongsTo(User::class, 'foreign_key');
}
如果父模型的主键未使用 id
作为字段名,或者您想要使用其他的字段来匹配相关联的模型,那么您可以向 belongsTo
方法传递第三个参数,这个参数是在父模型中自己定义的字段名称:
/**
* 获取当前手机号的用户
*/
public function user()
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}
一对多
当要定义一个模型是其他 (一个或者多个)模型的父模型这种关系时,可以使用一对多关联。例如,一篇博客可以有很多条评论。和其他模型关联一样,一对多关联也是在 Eloquent 模型文件中用一个方法来定义的:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 获取这篇博客的所有评论
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
}
注意,Eloquent 将会自动为 Comment
模型选择一个合适的外键。通常,这个外键是通过使用父模型的「蛇形命名」方式,然后再加上 _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');
一对多 (反向) / 属于
目前我们可以访问一篇文章的所有评论,下面我们可以定义一个关联关系,从而让我们可以通过一条评论来获取到它所属的文章。这个关联关系是 hasMany
的反向,可以在子模型中通过 belongsTo
方法来定义这种关联关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* 获取这条评论 所属的文章。
*/
public function post()
{
return $this->belongsTo(Post::class);
}
}
如果定义了这种关联关系,那么我们就可以通过 Comment
模型中的 post
「动态属性」来获取到这条评论所属的文章:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
在上面这个例子中,Eloquent 将会尝试寻找 Post
模型中的 id
字段与 Comment
模型中的 post_id
字段相匹配。
Eloquent 通过检查关联方法的名称,从而在关联方法名称后面加上 _
,然后再加上父模型 (Post)的主键名称,以此来作为默认的外键名。因此,在上面这个例子中,Eloquent 将会默认 Post
模型在 comments
表中的外键是 post_id
。
但是,如果您的外键不遵循这种约定的话,那么您可以传递一个自定义的外键名来作为 belongsTo
方法的第二个参数:
/**
* 获取这条评论所属的博客。
*/
public function post()
{
return $this->belongsTo(Post::class, 'foreign_key');
}
如果你的主表(Post 表)不使用 id 来作为它的主键的话,或者你想通过其他列来关联相关模型的话,那么可以传递一个参数来作为 belongsTo 方法的第三个参数,这个参数是主表(Post 表)中想要作为关联关系的字段的名称。
/**
* 获取这条评论所属的博客。
*/
public function post()
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}
默认模型
当 belongsTo
,hasOne
,hasOneThrough
和 morphOne
这些关联方法返回为 null
的时候,你可以定义一个默认的模型来返回。这种模式通常被称为 空对象模式
,它可以帮你省略代码中的一些条件判断。在下面这个例子中,如果 Post 模型中没有用户,那么 user 关联关系将会返回一个空的 App\Models\User 模型:
/**
* 获取这篇博客所属的用户。
*/
public function user()
{
return $this->belongsTo(User::class)->withDefault();
}
如果想要这个默认模型中包含一些属性的话,可以向 withDefault
方法中传递一个数组或者一个闭包:
/**
* 获取博客的作者。
*/
public function user()
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
/**
* 获取作者发布的博客。
*/
public function user()
{
return $this->belongsTo(User::class)->withDefault(function ($user, $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();
默认情况下,Laravel 将根据模型的类名确定与给定模型关联的关系; 你也可以通过将关系名称作为 whereBelongsTo
方法的第二个参数来手动指定关系名称:
$posts = Post::whereBelongsTo($user, 'author')->get();
一对多检索
有时一个模型可能有许多相关模型,如果想检索关系的「最新」或「最旧」相关模型。例如,一个 User
模型可能与许多 Order
模型相关,但您想定义一种方便的方式来与用户最近下的订单进行交互。 可以使用 hasOne
关系类型结合 ofMany
方法来完成此操作:
/**
* 获取用户最新的订单。
*/
public function latestOrder()
{
return $this->hasOne(Order::class)->latestOfMany();
}
同样,你也可以定义一个方法来检索模型关系最早的或第一个相关模型:
/**
* 获取用户最早的订单。
*/
public function oldestOrder()
{
return $this->hasOne(Order::class)->oldestOfMany();
}
latestOfMany
和 oldestOfMany
默认根据模型的主键检索最新或最旧的相关记录,所以模型主键必须是 可排序 的。 但是,有时你可能希望使用不同的排序条件从更大的关系中检索单个模型。
例如,使用 ofMany
方法,可以检索用户最昂贵的订单。ofMany
方法接受可排序列作为其第一个参数,以及在查询相关模型时应用哪个聚合函数(min
或 max
):
/**
* 获取用户最昂贵的订单。
*/
public function largestOrder()
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}