## 关联关系
通常我们所说的关联关系包括下面三种:
~~~
一对一关联 :ONE_TO_ONE,包括HAS_ONE 和 BELONGS_TO
一对多关联 :ONE_TO_MANY,包括HAS_MANY 和 BELONGS_TO
多对多关联 :MANY_TO_MANY
~~~
关联关系必然有一个参照表,例如:
- 有一个员工档案管理系统项目,这个项目要包括下面的一些数据表:基本信息表、员工档案表、部门表、项目组表、银行卡表(用来记录员工的银行卡资料)。
- 这些数据表之间存在一定的关联关系,我们以员工基本信息表为参照来分析和其他表之间的关联:
- 每个员工必然有对应的员工档案资料,所以属于HAS_ONE关联;
- 每个员工必须属于某个部门,所以属于BELONGS_TO关联;
- 每个员工可以有多个银行卡,但是每张银行卡只可能属于一个员工,因此属于HAS_MANY关联;
- 每个员工可以同时在多个项目组,每个项目组同时有多个员工,因此属于MANY_TO_MANY关联;
- 分析清楚数据表之前的关联关系后,我们才可以进行关联定义和关联操作。
## 关联定义
ThinkPHP可以很轻松的完成数据表的关联CURD操作,目前支持的关联关系包括下面四种:
**HAS_ONE**、**BELONGS_TO**、**HAS_MANY**和**MANY_TO_MANY**。
一个模型根据业务模型的复杂程度可以同时定义多个关联,不受限制,所有的关联定义都统一在模型类的 $_link 成员变量里面定义,并且可以支持动态定义。要支持关联操作,模型类必须继承`Think\Model\RelationModel`类,关联定义的格式是:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'关联1' => array(
'关联属性1' => '定义',
'关联属性N' => '定义',
),
'关联2' => array(
'关联属性1' => '定义',
'关联属性N' => '定义',
),
'关联3' => HAS_ONE, // 快捷定义
...
);
}
~~~
下面我们首先来分析下各个关联方式的定义:
### HAS_ONE
HAS_ONE关联表示当前模型拥有一个子对象,例如,每个员工都有一个人事档案。我们可以建立一个用户模型UserModel,并且添加如下关联定义:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'Profile'=> self::HAS_ONE,
);
}
~~~
上面是最简单的方式,表示其遵循了系统内置的数据库规范,完整的定义方式是:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'Profile'=>array(
'mapping_type' => self::HAS_ONE,
'class_name' => 'Profile',
// 定义更多的关联属性
……
),
);
}
~~~
关联HAS_ONE支持的关联属性有:
**mapping_type :关联类型**
这个在HAS_ONE 关联里面必须使用HAS_ONE 常量定义。
**class_name :要关联的模型类名**
例如,class_name 定义为Profile的话则表示和另外的Profile模型类关联,这个Profile模型类是无需定义的,系统会自动定位到相关的数据表进行关联。
**mapping_name :关联的映射名称,用于获取数据用**
该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。如果mapping_name没有定义的话,会取class_name的定义作为mapping_name。如果class_name也没有定义,则以数组的索引作为mapping_name。
**foreign_key : 关联的外键名称**
外键的默认规则是当前数据对象名称_id,例如: UserModel对应的可能是表think_user (注意:think只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关联的时候显式定义 foreign_key 。
**condition : 关联条件**
关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过定义关联的condition属性。
**mapping_fields : 关联要查询的字段**
默认情况下,关联查询的关联数据是关联表的全部字段,如果只是需要查询个别字段,可以定义关联的mapping_fields属性。
**as_fields :直接把关联的字段值映射成数据对象中的某个字段**
这个特性是ONE_TO_ONE 关联特有的,可以直接把关联数据映射到数据对象中,而不是作为一个关联数据。当关联数据的字段名和当前数据对象的字段名称有冲突时,还可以使用映射定义。
### BELONGS_TO
Belongs_to 关联表示当前模型从属于另外一个父对象,例如每个用户都属于一个部门。我们可以做如下关联定义。
~~~
'Dept' => self::BELONGS_TO
~~~
完整方式定义为:
~~~
'Dept' => array(
'mapping_type' => self::BELONGS_TO,
'class_name' => 'Dept',
'foreign_key' => 'userId',
'mapping_name' => 'dept',
// 定义更多的关联属性
……
),
~~~
关联BELONGS_TO定义支持的关联属性有:
| 属性 | 描述 |
|-----|-----|
| class_name | 要关联的模型类名 |
| mapping_name | 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。 |
| foreign_key | 关联的外键名称 |
| mapping_fields | 关联要查询的字段 |
| condition | 关联条件 |
| parent_key | 自引用关联的关联字段 默认为parent_id 自引用关联是一种比较特殊的关联,也就是关联表就是当前表。 |
| as_fields | 直接把关联的字段值映射成数据对象中的某个字段 |
### HAS_MANY
HAS_MANY 关联表示当前模型拥有多个子对象,例如每个用户有多篇文章,我们可以这样来定义:
~~~
'Article' => self::HAS_MANY
~~~
完整定义方式为:
~~~
'Article' => array(
'mapping_type' => self::HAS_MANY,
'class_name' => 'Article',
'foreign_key' => 'userId',
'mapping_name' => 'articles',
'mapping_order' => 'create_time desc',
// 定义更多的关联属性
……
),
~~~
关联HAS_MANY定义支持的关联属性有:
| 属性 | 描述 |
|-----|-----|
| class_name | 要关联的模型类名 |
| mapping_name | 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。 |
| foreign_key | 关联的外键名称 |
| parent_key | 自引用关联的关联字段 默认为parent_id |
| condition | 关联条件 关联查询的时候会自动带上外键的值,如果有额外的查询条件,可以通过定义关联的condition属性。 |
| mapping_fields | 关联要查询的字段 默认情况下,关联查询的关联数据是关联表的全部字段,如果只是需要查询个别字段,可以定义关联的mapping_fields属性。 |
| mapping_limit | 关联要返回的记录数目 |
| mapping_order | 关联查询的排序 |
外键的默认规则是当前数据对象名称_id,例如:UserModel对应的可能是表think_user (注意:think只是一个表前缀,可以随意配置) 那么think_user表的外键默认为 user_id,如果不是,就必须在定义关联的时候定义 foreign_key 。
### MANY_TO_MANY
MANY_TO_MANY 关联表示当前模型可以属于多个对象,而父对象则可能包含有多个子对象,通常两者之间需要一个中间表类约束和关联。例如每个用户可以属于多个组,每个组可以有多个用户:
~~~
'Group' => self::MANY_TO_MANY
~~~
完整定义方式为:
~~~
'Group' => array(
'mapping_type' => self::MANY_TO_MANY,
'class_name' => 'Group',
'mapping_name' => 'groups',
'foreign_key' => 'userId',
'relation_foreign_key' => 'groupId',
'relation_table' => 'think_group_user' //此处应显式定义中间表名称,且不能使用C函数读取表前缀
)
~~~
MANY_TO_MANY支持的关联属性定义有:
| 属性 | 描述 |
|-----|-----|
| class_name | 要关联的模型类名 |
| mapping_name | 关联的映射名称,用于获取数据用 该名称不要和当前模型的字段有重复,否则会导致关联数据获取的冲突。 |
| foreign_key | 关联的外键名称 外键的默认规则是当前数据对象名称_id |
| relation_foreign_key | 关联表的外键名称 默认的关联表的外键名称是表名_id |
| mapping_limit | 关联要返回的记录数目 |
| mapping_order | 关联查询的排序 |
| relation_table | 多对多的中间关联表名称 |
多对多的中间表默认表规则是:**`数据表前缀_关联操作的主表名_关联表名`**
如果think_user 和 think_group 存在一个对应的中间表,默认的表名应该是 如果是由group来操作关联表,中间表应该是 think_group_user,如果是从user表来操作,那么应该是think_user_group,也就是说,多对多关联的设置,必须有一个Model类里面需要显式定义中间表,否则双向操作会出错。 中间表无需另外的id主键(但是这并不影响中间表的操作),通常只是由 user_id 和 group_id 构成。 默认会通过当前模型的getRelationTableName方法来自动获取,如果当前模型是User,关联模型是Group,那么关联表的名称也就是使用 user_group这样的格式,如果不是默认规则,需要指定relation_table属性。
**3.2.2版本**开始,relation_table定义支持简化写法,例如:
~~~
'relation_table'=>'__USER_GROUP__'
~~~
## 关联查询
由于性能问题,新版取消了自动关联查询机制,而统一使用relation方法进行关联操作,relation方法不但可以启用关联还可以控制局部关联操作,实现了关联操作一切尽在掌握之中。
~~~
$User = D("User");
$user = $User->relation(true)->find(1);
~~~
输出$user结果可能是类似于下面的数据:
~~~
array(
'id' => 1,
'account' => 'ThinkPHP',
'password' => '123456',
'Profile' => array(
'email' => 'liu21st@gmail.com',
'nickname' => '流年',
),
)
~~~
我们可以看到,用户的关联数据已经被映射到数据对象的属性里面了。其中Profile就是关联定义的mapping_name属性。
如果我们按照下面的方式定义了as_fields属性的话,
~~~
protected $_link = array(
'Profile'=>array(
'mapping_type' => self::HAS_ONE,
'class_name' => 'Profile',
'foreign_key' => 'userId',
'as_fields' => 'email,nickname',
),
);
~~~
查询的结果就变成了下面的结果
~~~
array(
'id' => 1,
'account' => 'ThinkPHP',
'password' => 'name',
'email' => 'liu21st@gmail.com',
'nickname' => '流年',
)
~~~
email和nickname两个字段已经作为user数据对象的字段来显示了。
如果关联数据的字段名和当前数据对象的字段有冲突的话,怎么解决呢?
我们可以用下面的方式来变化下定义:
~~~
'as_fields' => 'email,nickname:username',
~~~
表示关联表的nickname字段映射成当前数据对象的username字段。
默认会把所有定义的关联数据都查询出来,有时候我们并不希望这样,就可以给relation方法传入参数来控制要关联查询的。
~~~
$User = D("User");
$user = $User->relation('Profile')->find(1);
~~~
关联查询一样可以支持select方法,如果要查询多个数据,并同时获取相应的关联数据,可以改成:
~~~
$User = D("User");
$list = $User->relation(true)->Select();
~~~
如果希望在完成的查询基础之上 再进行关联数据的查询,可以使用
~~~
$User = D("User");
$user = $User->find(1);
// 表示对当前查询的数据对象进行关联数据获取
$profile = $User->relationGet("Profile");
~~~
事实上,除了当前的参考模型User外,其他的关联模型是不需要创建的。
## 关联操作
除了关联查询外,系统也支持关联数据的自动写入、更新和删除
### 关联写入
~~~
$User = D("User");
$data = array();
$data["account"] = "ThinkPHP";
$data["password"] = "123456";
$data["Profile"] = array(
'email' =>'liu21st@gmail.com',
'nickname' =>'流年',
);
$result = $User->relation(true)->add($data);
~~~
这样就会自动写入关联的Profile数据。
同样,可以使用参数来控制要关联写入的数据:
~~~
$result = $User->relation("Profile")->add($data);
~~~
> 当MANY_TO_MANY时,不建议使用关联插入。
### 关联更新
数据的关联更新和关联写入类似
~~~
$User = D("User");
$data["account"] = "ThinkPHP";
$data["password"] = "123456";
$data["Profile"] = array(
'email' =>'liu21st@gmail.com',
'nickname' =>'流年',
);
$result = $User-> relation(true)->where(array('id'=>3))->save($data);
~~~
Relation(true)会关联保存User模型定义的所有关联数据,如果只需要关联保存部分数据,可以使用:
~~~
$result = $User->relation("Profile")->save($data);
~~~
这样就只会同时更新关联的Profile数据。
关联保存的规则:
**HAS_ONE**: 关联数据的更新直接赋值
**HAS_MANY**: 的关联数据如果传入主键的值 则表示更新 否则就表示新增
**MANY_TO_MANY**: 的数据更新是删除之前的数据后重新写入
### 关联删除
~~~
//删除用户ID为3的记录的同时删除关联数据
$result = $User->relation(true)->delete("3");
// 如果只需要关联删除部分数据,可以使用
$result = $User->relation("Profile")->delete("3");
~~~
- 序言
- 基础
- 获取ThinkPHP
- 环境要求
- 目录结构
- 入口文件
- 自动生成
- 模块
- 控制器
- 开发规范
- 配置
- 配置格式
- 配置加载
- 读取配置
- 动态配置
- 扩展配置
- 批量配置
- 架构
- 模块化设计
- URL模式
- 多层MVC
- CBD模式
- 命名空间
- 自动加载
- 应用模式
- 项目编译
- 系统流程
- 路由
- 路由定义
- 规则路由
- 正则路由
- 静态路由
- 闭包支持
- 实例说明
- 控制器
- 控制器定义
- 前置和后置操作
- Action参数绑定
- 伪静态
- URL大小写
- URL生成
- AJAX返回
- 跳转和重定向
- 输入变量
- 请求类型
- 空操作
- 空控制器
- 插件控制器
- 操作绑定到类
- 模型
- 模型定义
- 模型实例化
- 字段定义
- 连接数据库
- 切换数据库
- 分布式数据库支持
- 连贯操作
- WHERE
- TABLE
- ALIAS
- DATA
- FIELD
- ORDER
- LIMIT
- PAGE
- GROUP
- HAVING
- JOIN
- UNION
- DISTINCT
- LOCK
- CACHE
- COMMENT
- RELATION
- USING
- fetchSql
- TOKEN
- STRICT
- INDEX
- 命名范围
- CURD操作
- 数据创建
- 数据写入
- 数据读取
- 数据更新
- 数据删除
- ActiveRecord
- 字段映射
- 查询语言
- 查询方式
- 表达式查询
- 快捷查询
- 区间查询
- 组合查询
- 统计查询
- SQL查询
- 动态查询
- 子查询
- 自动验证
- 自动完成
- 参数绑定
- 虚拟模型
- 模型分层
- 视图模型
- 关联模型
- 高级模型
- Mongo模型
- 视图
- 模板定义
- 模板主题
- 模板赋值
- 模板渲染
- 获取模板地址
- 获取内容
- 模板引擎
- 模板
- 变量输出
- 系统变量
- 使用函数
- 默认值输出
- 使用运算符
- 标签库
- 模板继承
- 修改定界符
- 三元运算
- 包含文件
- 内置标签
- Volist标签
- Foreach标签
- For标签
- Switch标签
- 比较标签
- 范围判断标签
- IF标签
- Present标签
- Empty标签
- Defined标签
- Assign标签
- Define标签
- 标签嵌套
- import标签
- 使用PHP代码
- 原样输出
- 模板注释
- 模板布局
- 模板替换
- 调试
- 调试模式
- 异常处理
- 日志记录
- 页面Trace
- Trace方法
- 变量调试
- 性能调试
- 错误调试
- 模型调试
- 缓存
- 数据缓存
- 快速缓存
- 查询缓存
- 静态缓存
- 安全
- 输入过滤
- 表单合法性检测
- 表单令牌
- 防止SQL注入
- 目录安全文件
- 保护模板文件
- 上传安全
- 防止XSS攻击
- 其他安全建议
- 扩展
- 类库扩展
- 驱动扩展
- 缓存驱动
- 数据库驱动
- 日志驱动
- Session驱动
- 存储驱动
- 模板引擎驱动
- 标签库驱动
- 行为扩展
- 标签扩展
- Widget扩展
- 应用模式
- 部署
- PATH_INFO支持
- URL重写
- 模块部署
- 域名部署
- 入口绑定
- 替换入口
- 专题
- SESSION支持
- Cookie支持
- 多语言支持
- 数据分页
- 文件上传
- 验证码
- 图像处理
- RESTFul
- RPC
- SAE
- IP获取和定位
- 附录
- 常量参考
- 配置参考
- 升级指导
- 鸣谢