在业务系统中,数据模型之间往往存在复杂的关联关系,ThinkPHP 的模型关联功能正是为此提供的一套优雅的ORM解决方案。通过定义模型间的关联,开发者能够以面向对象的方式便捷地处理跨表数据查询,极大简化了复杂数据关系的操作难度。本文作为模型关联系列文章的首篇,将系统学习一对一、一对多、多对多这三种基础关联关系,以及远程一对一等扩展关联的用法。本篇文章记录这些核心关联类型的学习过程。
一、模型关联
通过模型关联操作把数据表的关联关系对象化,解决了大部分常用的关联场景,封装的关联操作比起常规的数据库联表操作更加智能和高效,并且直观。
从面向对象的角度来看关联的话,模型的关联其实应该是模型的某个属性,比如用户的档案关联,就应该是下面的样子:
php
// 获取用户模型实例
$user = User::find(1);
// 获取用户的档案
$user->profile;
// 获取用户的档案中的手机号
$user->profile->mobile;
为了更方便和灵活的定义模型的关联关系,框架选择了方法定义而不是属性定义的方式,每个关联属性对应了一个模型的(关联)方法,这个关联属性和模型的数据一样是动态的,并非模型类的实体属性。
例如上面的关联属性就是在 User 模型类中定义了一个 profile 方法(mobile 属性是 Profile 模型的属性):
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
一个模型可以定义多个不同的关联,只需增加不同的关联方法即可。
同时,我们还必须定义一个 Profile 模型(即使是一个空模型)。
php
<?php
namespace app\model;
use think\Model;
class Profile extends Model
{
}
关联方法返回的是不同的关联对象,例如这里的 profile 方法返回的是一个 HasOne 关联对象(think\model\relation\HasOne)实例。
当访问 User 模型对象实例的 profile 属性的时候,实际上是调用了 profile 方法来完成关联查询。
可以简单的理解为关联定义就是在模型类中添加一个方法(注意不要和模型的对象属性以及其它业务逻辑方法冲突 ),一般情况下无需任何参数,并在方法中指定一种关联关系,比如上面的 hasOne 关联关系,支持的关联关系包括下面8种:
|----------------|----------|
| 模型方法 | 关联类型 |
| hasOne | 一对一 |
| belongsTo | 一对一 |
| hasMany | 一对多 |
| hasOneThrough | 远程一对一 |
| hasManyThrough | 远程一对多 |
| belongsToMany | 多对多 |
| morphMany | 多态一对多 |
| morphOne | 多态一对一 |
| morphTo | 多态 |
关联方法的第一个参数就是要关联的模型名称,也就是说当前模型的关联模型必须也是已经定义好的一个模型。
可以使用完整命名空间定义,例如:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
两个模型之间因为参照模型的不同就会产生相对的但不一定相同的关联关系,并且相对的关联关系只有在需要调用的时候才需要定义,下面是每个关联类型的相对关联关系对照:
|--------|----------------|---------------|
| 类型 | 关联关系 | 相对的关联关系 |
| 一对一 | hasOne | belongsTo |
| 一对多 | hasMany | belongsTo |
| 多对多 | belongsToMany | belongsToMany |
| 远程一对多 | hasManyThrough | 不支持 |
| 多态一对一 | morphOne | morphTo |
| 多态一对多 | morphMany | morphTo |
例如,Profile 模型中就可以定义一个相对的关联关系。
php
<?php
namespace app\model;
use think\Model;
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
在进行关联查询的时候,操作是一样的,只是当前模型不同。
php
// 获取档案实例
$profile = Profile::find(1);
// 获取档案所属的用户名称
echo $profile->user->name;
如果是数据集查询的话,关联获取的用法如下:
php
// 获取档案实例
$profiles = Profile::where('id', '>', 1)->select();
foreach($profiles as $profile) {
// 获取档案所属的用户名称
echo $profile->user->name;
}
如果需要对关联模型进行更多的查询约束,可以在关联方法的定义后面追加额外的查询链式方法(但切忌不要滥用,并且不要使用实际的查询方法)。例如:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function book()
{
return $this->hasMany(Book::class)->order('pub_time');
}
}
模型关联支持调用模型的方法。
1、一对一关联
定义一对一关联,例如,每个用户都有一个个人资料,我们定义 User 模型如下:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
hasOne 方法的参数包括:
hasOne('关联模型类名', '外键', '主键');
除了关联模型外,其它参数都是可选的。
- 关联模型(必须):关联模型类名。
- 外键 :默认的外键规则是当前模型名(不含命名空间,下同)+ _id ,例如 user_id。
- 主键:当前模型主键,默认会自动获取也可以指定传入。
定义好关联之后,就可以使用下面的方法获取关联数据:
php
$user = User::find(1);
// 输出 Profile 关联模型的 email 属性
echo $user->profile->email;
默认情况下, 使用 user_id 作为外键关联,如果不是的话则需要在关联定义的时候指定,例如:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class, 'uid');
}
}
有一点需要注意的是,关联方法的命名规范是驼峰法,而关联属性则一般是小写+下划线 的方式,系统在获取的时候会自动转换对应,读取 user_profile 关联属性则对应的关联方法应该是 userProfile。
hasWhere 方法可以根据关联条件来查询当前模型对象数据。例如:
php
// 查询用户昵称是think的用户
// 注意第一个参数是关联方法名(不是关联模型名)
$users = User::hasWhere('profile', ['nickname'=>'think'])->select();
// 可以使用闭包查询
$users = User::hasWhere('profile', function($query) {
$query->where('nickname', 'like', 'think%');
})->select();
注意 :如果 hasWhere 需要和 where 同时使用的话,hasWhere 必须在前面,否则 where 不会生效。
默认情况下,关联查询的数据是不包含软删除数据的,如果需要包含软删除数据,可以在关联定义的时候使用 withTrashed() 方法。
php
$users = User::with(['profile' => function($query) {
$query->withTrashed();
}])->select();
1.1、关联保存
如果当前对象的关联模型没有关联数据,则调用 save() 方法时会保存关联模型的数据。例如:
php
$user = User::find(1);
// 如果还没有关联数据,则进行新增
$user->profile()->save(['email' => 'test@126.com']);
系统会自动把当前模型的主键传入 Profile 模型。
如果当前对象已有关联数据,则此时调用 save() 方法会进行数据的更新。例如:
php
$user = User::find(1);
$user->profile->email = 'thinkphp';
$user->profile->save();
1.2、定义相对关联
可以在 Profile 模型中定义一个相对的关联关系,例如:
php
<?php
namespace app\model;
use think\Model;
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
belongsTo 的参数包括:
belongsTo('关联模型', '外键', '关联主键');
除了关联模型外,其它参数都是可选。
- 关联模型(必须):关联模型类名。
- 外键 :当前模型外键,默认的外键名规则是关联模型名+ _id。
- 关联主键:关联模型主键,一般会自动获取也可以指定传入。
接下来就可以根据档案资料来获取用户模型的信息:
php
$profile = Profile::find(1);
// 输出 User 关联模型的属性
echo $profile->user->name;
1.3、绑定属性到父模型
可以在定义关联的时候使用 bind 方法绑定属性到父模型。例如:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class)->bind(['nickname', 'email']);
}
}
或者指定绑定属性别名
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class)->bind([
'truename' => 'nickname', // 把关联模型的 nickname 字段绑定到当前模型的 truename 属性
'email']);
}
}
然后使用关联预载入查询的时候,可以使用
php
$user = User::with('profile')->find(1);
// 直接输出 Profile 关联模型的绑定属性
echo $user->email;
echo $user->truename;
也可以使用 bindAttr 方法动态绑定关联属性:
php
$user = User::find(1)->bindAttr('profile',['email','nickname']);
// 输出 Profile 关联模型的 email 属性
echo $user->email;
echo $user->nickname;
同样支持指定属性别名
php
$user = User::find(1)->bindAttr('profile',[
'email',
'truename' => 'nickname',
]);
// 输出Profile关联模型的email属性
echo $user->email;
echo $user->truename;
1.4、关联自动写入
可以使用 together 方法更方便的进行关联自动写入操作。
示例 写入
php
$blog = new Blog;
$blog->name = '测试';
$blog->title = '测试标题';
$content = new Content;
$content->data = '示例内容';
$blog->content = $content;
$blog->together(['content'])->save();
示例 更新
php
// 查询
$blog = Blog::find(1);
$blog->title = '更改标题';
$blog->content->data = '更新内容';
// 更新当前模型及关联模型
$blog->together(['content'])->save();
示例 删除
php
// 查询
$blog = Blog::with('content')->find(1);
// 删除当前及关联模型
$blog->together(['content'])->delete();
2、一对多关联
一对多关联的情况是比较常见的,使用 hasMany 方法定义,参数包括:
hasMany('关联模型', '外键', '主键');
除了关联模型外,其它参数都是可选。
- 关联模型(必须):关联模型类名。
- 外键 :关联模型外键,默认的外键名规则是当前模型名+ _id。
- 主键:当前模型主键,一般会自动获取也可以指定传入。
例如一篇文章可以有多个评论
php
<?php
namespace app\model;
use think\Model;
class Article extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
}
同样地,也可以定义外键的名称。
接下来就可以通过下面的方式获取关联数据:
php
$article = Article::find(1);
// 获取文章的所有评论
dump($article->comments);
// 也可以进行条件搜索
dump($article->comments()->where('status', 1)->select());
可以使用 has 方法或 hasWhere 方法根据关联条件来查询当前模型对象数据。例如:
php
// 查询评论数超过3个的文章
$list = Article::has('comments', '>', 3)->select();
// 查询评论状态正常的文章
$list = Article::hasWhere('comments', ['status' => 1])->select();
如果需要更复杂的关联条件查询,可以像下面这样操作:
php
$where = Comment::where('status', 1)->where('content', 'like', '%think%');
$list = Article::hasWhere('comments', $where)->select();
注意 :如果 hasWhere 需要和 where 同时使用的话,hasWhere 必须在前面。
2.1、关联新增
我们可以对关联数据进行新增,例如:
php
$article = Article::find(1);
// 增加一个关联数据
$article->comments()->save(['content' => 'test']);
// 批量增加关联数据
$article->comments()->saveAll([
['content' => 'thinkphp'],
['content' => 'onethink']
]);
2.2、定义相对关联
同定义一对一相对关联一样,一对多相对关联也是使用 belongsTo 方法。例如,在 Comment 模型定义相对应的关联:
php
<?php
namespace app\model;
use think\Model;
class Comment extends Model
{
public function article()
{
return $this->belongsTo(Article::class);
}
}
2.3、关联删除
使用 together 方法方便的进行关联自动操作。例如:
php
// 在删除文章的同时删除下面的评论
$article = Article::with('comments')->find(1);
$article->together(['comments'])->delete();
3、远程一对多
远程一对多关联用于定义有跨表的一对多关系,例如:
- 每个城市有多个用户
- 每个用户有多个话题
- 城市和话题之间并无关联
远程一对多使用 hasManyThrough 方法定义,参数如下:
hasManyThrough('关联模型', '中间模型', '外键', '中间表关联键', '当前模型主键', '中间模型主键');
- 关联模型(必须):关联模型类名。
- 中间模型(必须):中间模型类名。
- 外键 :默认的外键名规则是当前模型名+ _id。
- 中间表关联键 :默认的中间表关联键名的规则是中间模型名+ _id。
- 当前模型主键:一般会自动获取也可以指定传入。
- 中间模型主键:一般会自动获取也可以指定传入。
下面给出的是需要用到的表。因为只是用来演示,所以表的结构都很简单。
sql
CREATE TABLE `city`(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50)
);
CREATE TABLE `user`(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50),
city_id INT
);
CREATE TABLE `topic`(
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(50),
STATUS INT,
user_id INT
);
现在可以直接通过远程一对多关联获取每个城市的多个话题,City 模型定义如下:
php
<?php
namespace app\model;
use think\Model;
class City extends Model
{
public function topics()
{
return $this->hasManyThrough(Topic::class, User::class);
}
}
远程一对多关联,需要同时存在 Topic 和 User 模型,当前模型和中间模型的关联关系可以是一对一或者一对多。
接下来我们就可以通过下面的方式获取关联数据了。
php
$city = City::find(1);
// 获取同城的所有话题
dump($city->topics);
// 也可以进行条件搜索
dump($city->topics()->where('topic.status',1)->select());
条件搜索的时候,需要带上模型名作为前缀。
3.1、根据关联条件查询
同样的,远程一对多也可以使用 hasWhere 方法根据关联条件来查询当前模型。例如:
php
$list = City::hasWhere('topics', ['status' => 1])->select();
更复杂的查询条件可以使用
php
$where = Topic::where('status', 1)->where('title', 'like', '%think%');
$list = City::hasWhere('topics', $where)->select();
注意 :如果 hasWhere 需要和 where 同时使用的话,hasWhere 必须在前面。
4、远程一对一
远程一对一关联用于定义有跨表的一对一关系,例如:
- 每个用户有一个档案
- 每个档案有一个档案卡
- 用户和档案卡之间并无关联
远程一对一使用 hasOneThrough 方法定义,参数如下:
hasOneThrough('关联模型', '中间模型', '外键', '中间表关联键', '当前模型主键', '中间模型主键');
- 关联模型(必须):关联模型类名。
- 中间模型(必须):中间模型类名。
- 外键 :默认的外键名规则是当前模型名+ _id。
- 中间表关联键 :默认的中间表关联键名的规则是中间模型名+ _id。
- 当前模型主键:一般会自动获取也可以指定传入。
- 中间模型主键:一般会自动获取也可以指定传入。
下面给出的是需要用到的表。因为只是用来演示,所以表的结构都很简单。
sql
CREATE TABLE `user`(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50)
) COMMENT '用户';
CREATE TABLE `profile`(
id INT PRIMARY KEY AUTO_INCREMENT,
`full_name` VARCHAR(50),
`user_id` INT
) COMMENT '档案';
CREATE TABLE `card`(
id INT PRIMARY KEY AUTO_INCREMENT,
`card_no` VARCHAR(30),
`profile_id` INT
) COMMENT '档案卡';
现在可以直接通过远程一对一关联获取每个用户的档案卡,User 模型定义如下:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public function card()
{
return $this->hasOneThrough(Card::class, Profile::class);
}
}
接下来我们就可以通过下面的方式获取关联数据了。
php
$user = User::find(1);
// 获取用户档案卡卡号
echo $user->card->card_no;
5、多对多关联
多对多关联使用 belongsToMany 方法定义,参数如下:
belongsToMany('关联模型', '中间表', '外键', '关联键');
- 关联模型(必须):关联模型类名。
- 中间表 :默认规则是当前模型名+_+关联模型名(可以指定模型名)。
- 外键 :中间表的当前模型外键,默认的外键名规则是关联模型名+ _id。
- 关联键 :中间表的当前模型关联键名,默认规则是当前模型名+ _id。
例如,用户和角色就是一种多对多的关系。下面给出的是需要用到的表。因为只是用来演示,所以表的结构都很简单。
sql
CREATE TABLE `user`(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255)
) COMMENT '用户';
CREATE TABLE `role`(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
) COMMENT '角色';
CREATE TABLE `access`(
id INT PRIMARY KEY AUTO_INCREMENT,
`user_id` int,
`role_id` int
) COMMENT '中间表';
User 模型定义如下:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function roles()
{
return $this->belongsToMany(Role::class, 'access');
}
}
中间表名无需添加表前缀,并支持定义中间表模型,例如:
php
public function roles()
{
return $this->belongsToMany(Role::class, Access::class);
}
中间表模型类必须继承 think\model\Pivot,例如:
php
<?php
namespace app\model;
use think\model\Pivot;
class Access extends Pivot
{
}
接下来可以通过下面的方式获取关联数据了。
php
$user = User::find(1);
// 获取用户的所有角色
$roles = $user->roles;
foreach ($roles as $role) {
// 输出用户的角色名
echo $role->name;
// 获取中间表模型
dump($role->pivot);
}
关联新增
php
$user = User::find(1);
// 给用户增加管理员权限 会自动写入角色表和中间表数据
$user->roles()->save(['name'=>'管理员']);
// 批量授权
$user->roles()->saveAll([
['name'=>'管理员'],
['name'=>'操作员'],
]);
只新增中间表数据(角色已经提前创建完成),可以使用:
php
$user = User::find(1);
// 仅增加管理员权限(假设管理员的角色ID是1)
$user->roles()->save(1);
// 或者
$role = Role::find(1);
$user->roles()->save($role);
// 批量增加关联数据
$user->roles()->saveAll([1, 2, 3]);
单独更新中间表数据,可以使用:
php
$user = User::find(1);
// 增加关联的中间表数据
$user->roles()->attach(1);
// 删除中间表数据
$user->roles()->detach([1, 2, 3]);
attach 方法的返回值是一个 Pivot 对象实例。