Eloquent: 关联
简介
数据库表通常相互关联。 例如,一篇博客文章可能有许多评论,或者一个订单对应一个下单用户。Eloquent 让这些关联的管理和使用变得简单,并支持多种类型的关联:
定义关联
Eloquent 关联在 Eloquent 模型类中以方法的形式呈现。如同 Eloquent 模型本身,关联也可以作为强大的 查询语句构造器 使用,提供了强大的链式调用和查询功能。例如,我们可以在 posts 关联的链式调用中附加一个约束条件:
$user->posts()->where('active', 1)->get();不过在深入使用关联之前,让我们先学习如何定义每种关联类型。
一对一
一对一是最基本的关联关系。例如,一个 User 模型可能关联一个 Phone 模型。为了定义这个关联,我们要在 User 模型中写一个 phone 方法。在 phone 方法内部调用 hasOne 方法并返回其结果:
hasOne('App\Phone');
}
}hasOne 方法的第一个参数是关联模型的类名。一旦定义了模型关联,我们就可以使用 Eloquent 动态属性获得相关的记录。动态属性允许你访问关系方法就像访问模型中定义的属性一样:
$phone = User::find(1)->phone;
Eloquent 会基于模型名决定外键名称。在这种情况下,会自动假设 Phone 模型有一个 user_id 外键。如果你想覆盖这个约定,可以传递第二个参数给 hasOne 方法:
return $this->hasOne('App\Phone', 'foreign_key');另外,Eloquent 假设外键的值是与父级 id (或自定义 $primaryKey) 列的值相匹配的。换句话说,Eloquent 将会在 Phone 记录的 user_id 列中查找与用户表的 id 列相匹配的值。如果您希望该关联使用 id 以外的自定义键名,则可以给 hasOne 方法传递第三个参数:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');定义反向关联
我们已经能从 User 模型访问到 Phone 模型了。现在,让我们再在 Phone 模型上定义一个关联,这个关联能让我们访问到拥有该电话的 User 模型。我们可以使用与 hasOne 方法对应的 belongsTo 方法来定义反向关联:
belongsTo('App\User');
}
}在上面的例子中, Eloquent 会尝试匹配 Phone 模型上的 user_id 至 User 模型上的 id 。它是通过检查关系方法的名称并使用 _id 作为后缀名来确定默认外键名称的。但是,如果 Phone 模型的外键不是 user_id ,那么可以将自定义键名作为第二个参数传递给 belongsTo 方法:
/**
* 获得拥有此电话的用户
*/
public function user(){
return $this->belongsTo('App\User', 'foreign_key');
}如果父级模型没有使用 id 作为主键,或者是希望用不同的字段来连接子级模型,则可以通过给 belongsTo 方法传递第三个参数的形式指定父级数据表的自定义键:
/**
* 获得拥有此电话的用户
*/
public function user(){
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}一对多
『一对多』关联用于定义单个模型拥有任意数量的其它关联模型。例如,一篇博客文章可能会有无限多条评论。正如其它所有的 Eloquent 关联一样,一对多关联的定义也是在 Eloquent 模型中写一个方法:
hasMany('App\Comment');
}
}记住一点,Eloquent 将会自动确定 Comment 模型的外键属性。按照约定,Eloquent 将会使用所属模型名称的 『snake case』形式,再加上 _id 后缀作为外键字段。因此,在上面这个例子中,Eloquent 将假定 Comment 对应到 Post 模型上的外键就是 post_id。
一旦关系被定义好以后,就可以通过访问 Post 模型的 comments 属性来获取评论的集合。记住,由于 Eloquent 提供了『动态属性』 ,所以我们可以像访问模型的属性一样访问关联方法:
$comments = App\Post::find(1)->comments;foreach ($comments as $comment) { //}当然,由于所有的关联还可以作为查询语句构造器使用,因此你可以使用链式调用的方式,在 comments 方法上添加额外的约束条件:
$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();正如 hasOne 方法一样,你也可以在使用 hasMany 方法的时候,通过传递额外参数来覆盖默认使用的外键与本地键:
return $this->hasMany('App\Comment', 'foreign_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');一对多(反向)
现在,我们已经能获得一篇文章的所有评论,接着再定义一个通过评论获得所属文章的关联关系。这个关联是 hasMany 关联的反向关联,需要在子级模型中使用 belongsTo 方法定义它:
belongsTo('App\Post');
}
}这个关系定义好以后,我们就可以通过访问 Comment 模型的 post 这个『动态属性』来获取关联的 Post 模型了:
$comment = App\Comment::find(1); echo $comment->post->title;
在上面的例子中,Eloquent 会尝试用 Comment 模型的 post_id 与 Post 模型的 id 进行匹配。默认外键名是 Eloquent 依据关联名,并在关联名后加上 _ 再加上主键字段名作为后缀确定的。当然,如果 Comment 模型的外键不是 post_id,那么可以将自定义键名作为第二个参数传递给 belongsTo 方法:
/**
* 获取此评论所属文章
*/
public function post(){
return $this->belongsTo('App\Post', 'foreign_key');
}如果父级模型没有使用 id 作为主键,或者是希望用不同的字段来连接子级模型,则可以通过给 belongsTo 方法传递第三个参数的形式指定父级数据表的自定义键:
/**
* 获取此评论所属文章
*/
public function post(){
return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}多对多
多对多关联比 hasOne 和 hasMany 关联稍微复杂些。举个例子,一个用户可以拥有很多种角色,同时这些角色也被其他用户共享。例如,许多用户可能都有 「管理员」 这个角色。要定义这种关联,需要三个数据库表: users,roles 和 role_user。role_user 表的命名是由关联的两个模型按照字母顺序来的,并且包含了 user_id 和 role_id 字段。
多对多关联通过调用 belongsToMany 这个内部方法返回的结果来定义,例如,我们在 User 模型中定义 roles 方法:
belongsToMany('App\Role');
}
}一旦关联关系被定义后,你可以通过 roles 动态属性获取用户角色:
$user = App\User::find(1);
foreach ($user->roles as $role) {
//
}当然,像其它所有关联模型一样,你可以使用 roles 方法,利用链式调用对查询语句添加约束条件:
$roles = App\User::find(1)->roles()->orderBy('name')->get();正如前面所提到的,为了确定关联连接表的表名,Eloquent 会按照字母顺序连接两个关联模型的名字。当然,你也可以不使用这种约定,传递第二个参数到 belongsToMany 方法即可:
return $this->belongsToMany('App\Role', 'role_user');除了自定义连接表的表名,你还可以通过传递额外的参数到 belongsToMany 方法来定义该表中字段的键名。第三个参数是定义此关联的模型在连接表里的外键名,第四个参数是另一个模型在连接表里的外键名:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');定义反向关联
要定义多对多的反向关联, 你只需要在关联模型中调用 belongsToMany 方法。我们在 Role 模型中定义 users 方法:
belongsToMany('App\User');
}
}如你所见,除了引入模型为 App\User 外,其它与在 User 模型中定义的完全一样。由于我们重用了 belongsToMany 方法,自定义连接表表名和自定义连接表里的键的字段名称在这里同样适用。
获取中间表字段
就如你刚才所了解的一样,多对多的关联关系需要一个中间表来提供支持, Eloquent 提供了一些有用的方法来和这张表进行交互。例如,假设我们的 User 对象关联了多个 Role 对象。在获得这些关联对象后,可以使用模型的 pivot 属性访问中间表的数据:
$user = App\User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}需要注意的是,我们获取的每个 Role 模型对象,都会被自动赋予 pivot 属性,它代表中间表的一个模型对象,并且可以像其他的 Eloquent 模型一样使用。
默认情况下,pivot 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:
return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');如果你想让中间表自动维护 created_at 和 updated_at 时间戳,那么在定义关联时附加上 withTimestamps 方法即可:
return $this->belongsToMany('App\Role')->withTimestamps();自定义 pivot 属性名称
如前所述,来自中间表的属性可以使用 pivot 属性访问。但是,你可以自由定制此属性的名称,以便更好的反应其在应用中的用途。
例如,如果你的应用中包含可能订阅的用户,则用户与博客之间可能存在多对多的关系。如果是这种情况,你可能希望将中间表访问器命名为 subscription 取代 pivot 。这可以在定义关系时使用 as 方法完成:
return $this->belongsToMany('App\Podcast')
->as('subscription')
->withTimestamps();一旦定义完成,你可以使用自定义名称访问中间表数据:
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}通过中间表过滤关系
在定义关系时,你还可以使用 wherePivot 和 wherePivotIn 方法来过滤 belongsToMany 返回的结果:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);定义中间表模型
定义自定义中间表模型
如果你想定义一个自定义模型来表示关联关系中的中间表,可以在定义关联时调用 using 方法。自定义多对多中间表模型都必须扩展自 Illuminate\Database\Eloquent\Relations\Pivot 类,自定义多对多(多态)中间表模型必须继承 Illuminate\Database\Eloquent\Relations\MorphPivot 类。例如,
我们在写 Role 模型的关联时,使用自定义中间表模型 UserRole
belongsToMany('App\User')->using('App\UserRole');
}
}当定义 UserRole 模型时,我们要扩展 Pivot 类:
你可以组合使用
using和withPivot从中间表来检索列。例如,通过将列名传递给withPivot方法,就可以从UserRole中间表中检索出created_by和updated_by两列数据。belongsToMany('App\User') ->using('App\UserRole') ->withPivot([ 'created_by', 'updated_by' ]); } }带有递增 ID 的自定义中继模型
如果你用一个自定义的中继模型定义了多对多的关系,而且这个中继模型拥有一个自增的主键,你应当确保这个自定义中继模型类中定义了一个
incrementing属性其值为true:/** * 标识 ID 是否自增。 * * @var bool */ public $incrementing = true;远程一对一关系
远程一对一关联通过一个中间关联模型实现。
例如,如果每个供应商都有一个用户,并且每个用户与一个用户历史记录相关联,那么供应商可以通过用户访问用户的历史记录,让我们看看定义这种关系所需的数据库表:suppliers id - integer users id - integer supplier_id - integer history id - integer user_id - integer虽然
history表不包含supplier_id,但hasOneThrough关系可以提供对用户历史记录的访问,以访问供应商模型。现在我们已经检查了关系的表结构,让我们在Supplier模型上定义相应的方法:hasOneThrough('App\History', 'App\User'); } }传递给
hasOneThrough方法的第一个参数是希望访问的模型名称,第二个参数是中间模型的名称。当执行关联查询时,通常会使用 Eloquent 约定的外键名。如果你想要自定义关联的键,可以通过给
hasOneThrough方法传递第三个和第四个参数实现,第三个参数表示中间模型的外键名,第四个参数表示最终模型的外键名。第五个参数表示本地键名,而第六个参数表示中间模型的本地键名:class Supplier extends Model{ /** * 用户的历史记录。 */ public function userHistory() { return $this->hasOneThrough( 'App\History', 'App\User', 'supplier_id', // 用户表外键 'user_id', // 历史记录表外键 'id', // 供应商本地键 'id' // 用户本地键 ); } }远程一对多关联
远程一对多关联提供了方便、简短的方式通过中间的关联来获得远层的关联。例如,一个
Country模型可以通过中间的User模型获得多个Post模型。在这个例子中,你可以轻易地收集给定国家的所有博客文章。让我们来看看定义这种关联所需的数据表:countries id - integer name - string users id - integer country_id - integer name - string posts id - integer user_id - integer title - string虽然
posts表中不包含country_id字段,但hasManyThrough关联能让我们通过$country->posts访问到一个国家下所有的用户文章。为了完成这个查询,Eloquent 会先检查中间表users的country_id字段,找到所有匹配的用户 ID 后,使用这些 ID,在posts表中完成查找。现在,我们已经知道了定义这种关联所需的数据表结构,接下来,让我们在
Country模型中定义它:&

