模型关联


把数据表的关系对象化 解决了常用的关联场景
关联操作比起数据库联表操作智能和高效 且直观 关联是模型的杀手锏
模型间的关联关系及关联查询
关联的优势主要在查询

多用户博客系统
数据表
城市表city
用户表 user
博客表 blog  博客基础信息
内容表 content 博客 具体内容和扩展信息
分类表 cate
评论表 comment
角色表 role
用户-角色表 auth
关联关系 参照模型  称为主模型 或 当前模型
关联关系对应的模型 是 关联模型
关联关系指定义在主模型中的关联
中间表不一定需要具体的模型
主模型和关联模型间 通过外键关联  外键的命名 有约定 通常是 主模型名称+_id
遵循约定会 简化关联定义
数据表创建模型 模型间的关联关系

关联关系是相对某个参照模型的
博客和内容一对一 hasOne关联 博客模型为参照
content表 blog_id字段
内容和博客 属于belongsTo关联 以内容模型为参照
博客属于某个分类 设计为单个分类  belongsTo关联 以博客模型为参照  blog表有cate_id字段
分类下有多个博客属于hasMany关联 以分类模型为参照
用户发布多个博客 用户和博客间属于hasMany关联 以用户模型为参照  blog表有user_id字段
每个博客有多个评论 博客和评论属hasMany关联 以博客模型为参照
用户有多个角色每个角色有多个用户 用户和角色属belongsToMany关联
多对多关联  用户和角色间间表是用户权限表 中间表会设计user_id和role_id字段
每个城市有多个用户 每个用户有多个博客 城市和博客通过中间模型产生关联
城市和博客之间属于hasManyThrough关联 远程一对多 以城市模型为参照  中间模型是用户

某个用户和某个博客都能发表评论 那么用户、博客和评论之间就形成了多态一对多的关联关系 也就是说用户会有多个评论 morphMany关联 以用户模型为参照  博客会有多个评论 morphMany关联 以博客模型为参照  但评论表只有一个 评论表对于博客和用户来说 不需要定义两个关联关系 而只需要定义一个morphTo关联 以评论模型为参照 即可 评论表的设计就会被改造以满足多态的设计 普遍的设计是会增加一个多态类型的字段来标识属于某个类型 这里就是用户或博客类型
关联的表现方式
面向对象看关联 模型的关联是模型的某个属性 比如用户的档案关联 就应该是下面的情况:
// 用户的档案
$user->profile;
// 用户的档案属性中的手机资料
$user->profile->mobile;
$user 是 User 模型的对象实例
$user->profile 是Profile模型的对象实例
所以具备模型的所有特性而不是数组
进行Profile模型的CURD操作和业务逻辑执行 $user->profile->mobile 表示获取Profile模型对象实例的mobile数据
包括下面的操作也是有效的
// 对查询出来的关联模型数据更新
$user->profile->email = 'myqq@qq.com'
$user->profile->save();
这种关联关系
用Db类无法完成
由模型来完成 模型的关联用法很好的解决了关联的对象化
支持大部分的关联场景和需求
为方便灵活的定义模型的关联关系
框架用方法定义而不是属性定义的方式
每个关联属性对应一个模型的关联方法
关联属性和模型的数据是动态的 并非模型类的实际属性
关联属性在User模型类中定义了一个profile方法
namespace app\index\model;
use think\Model;
class User extends Model{
public function profile() {
return $this->hasOne('Profile');
}
}
访问User模型对象实例 profile属性时 是调用 profile方法来完成关联查询
当获取模型的属性的时候触发模型的获取器
而当获取器在没有检测到模型有对应属性时就检查是否存在关联方法定义
关联方法的判断很简单 关联方法返回的是 think\model\Relation对象
若存在则调用对应关联类的getRelation方法
模型的方法名 为 驼峰命名
系统做了兼容处理 当定义 userProfile 关联方法时 在获取关联属性时
下面两种方式都是有效的
$user->userProfile;
$user->user_profile;
推荐关联属性统一使用后者 和数据表的字段命名规范一致
系统自动获取关联属性的时候采用的也是后者
Model类中getAttr方法的源码  看关联属性获取的具体代码实现
方法赋予了模型神奇的关联特性
hasOne方法背后是强大而复杂的关联实现逻辑
让开发更简单 是因为有众多 简单而又神奇的特性
定义关联方法 关键是 用何种关联关系 不同的关联关系的定义方法和参数
定义关联是在模型类中添加方法 不要和模型的对象属性及其它业务逻辑方法冲突
一般无需参数 在方法中指定关联关系
关联关系包括下面七种
模型方法     关联类型
hasOne         一对一HAS ONE
belongsTo     一对一BELONGS TO
hasMany     一对多 HAS MANY
hasManyThrough     远程一对多 HAS MANY THROUTH
belongsToMany     多对多 BELONGS TO MANY
morphMany     多态一对多 MORPH MANY
morphTo     多态 MORPH TO
关联方法的第一个参数
是要关联的模型名称
当前模型的关联模型必须是已经定义的模型

默认用当前模型的命名空间,若不同请用完整命名空间定义
namespace app\index\model;
use think\Model;
class User extends Model{
public function profile() {  // Profile模型和当前模型的命名空间不一致
return $this->hasOne('app\model\Profile');
}
}
两个模型之间因为参照模型的不同
产生相对的不一定相同的关联关系
且相对的关联关系只有在需要调用的时候才需要定义
每个关联类型的相对关联关系对照
类型     关联关系     相对的关联关系
一对一     hasOne         belongsTo
一对多     hasMany     belongsTo
多对多     belongsToMany     belongsToMany
远程一对多     hasManyThrough     不支持
多态一对多     morphMany     morphTo
关联定义的要点
关联方法用驼峰法命名
关联方法一般无需定义参数
关联调用时 驼峰法和小写+下划线都支持
关联字段 尽可能按规范简化关联定义
关联方法定义可添加额外查询条件
关联方法定义参数说明
七种关联关系的定义及参数
hasOne关联
用法 hasOne('关联模型','外键','主键');
除了关联模型 其它参数可选
关联模型 必须  模型名 或 模型类名
外键 默认外键是当前模型名 不含命名空间 +_id 如 user_id
主键 当前模型主键 一般 自动获取也可 指定
belongsTo关联
用法 belongsTo('关联模型','外键','关联表主键');
除了关联模型 其它参数可选
关联模型 必须 模型名或模型类名
外键 当前模型外键 默认 外键名 是关联模型名 +_id
关联主键 关联模型主键 一般 自动获取也可 指定
hasMany关联
用法 hasMany('关联模型','外键','主键');
除了关联模型 其它参数可选
关联模型 必须  模型名或 模型类名
外键 关联模型外键 默认 外键名 是当前模型名+_id
主键 当前模型主键 一般 自动获取也可 指定
hasManyThrough
用法 hasManyThrough('关联模型','中间模型','外键','中间表关联键','主键');
关联模型 必须  模型名 或 模型类名
中间模型 必须  模型名 或 模型类名
外键 默认的外键名 是当前模型名+_id
中间表关联键 默认的中间表关联键名 是中间模型名+_id
主键 当前模型主键 一般会自动获取也可以指定传入
belongsToMany 关联
用法 belongsToMany('关联模型','中间表','外键','关联键');
关联模型  必须   模型名或 模型类名
中间表 默认 是当前模型名+_+关联模型名
外键 中间表的当前模型外键 默认 外键名 是关联模型名+_id
关联键 中间表的当前模型关联键名 默认 是当前模型名+_id
morphMany关联
用法 morphMany('关联模型','多态字段','多态类型');
关联模型 必须 :模型名或者模型类名
多态字段:多态字段信息定义包含两种方式 字符串的话表示多态字段的前缀 数组则表示实际的多态字段
多态类型:默认是当前模型名
数据表的多态字段一般包含两个字段:多态类型和多态主键
若多态字段使用字符串例如morph 那么多态类型和多态主键字段分别对应morph_type 和 morph_id 若用数组方式定义的话 就改为['morph_type','morph_id']即可

morphTo关联
用法 morphTo('多态字段','多态类型别名 数组 ');
多态字段 定义和morphMany一致
多态类型别名 用于设置特殊的多态类型 比如用数字标识的多态类型
基础方法
关联操作经常会涉及到几个重要的方法 也是关联操作的基础 掌握了这几个方法对于掌握关联 尤其是关联查询 有很大的帮助

方法名     作用
relation     关联查询
with         关联预载入
withCount     关联统计
load         关联延迟预载入
together     关联自动写入
使用方法
load 数据集对象的方法
together 模型类提供的方法
其它几个都是Query类提供的链式方法 在查询方法之前调用
relation和with方法的 区别在于relation是单纯的关联查询
如查询一个用户列表 然后需要关联查询用户的档案数据
使用relation方法 是 先查询用户列表数据 然后每个 用户再单纯查询档案数据
若用户列表数据有10个 那么就会产生11次查询

with方法
虽然最终查询出来的关联数据是一样的 但由于with查询使用的是预载入查询
因此实际只会产生2次查询
而load方法则更先进
先查询出用户列表 然后在需要关联数据的时候使用load方法获取关联数据
适合动态关联的情况 最终也是两次查询 因此称为延迟预载入

由于模型关联的对象化封装机制的优势
relation方法 很少被用到  使用关联惰性查询及关联方法的自定义查询来替代

最常用的with方法 因为最常用因此被内置到模型类的get和all方法的第二个参数
对with方法的用法说明 均适用于get和all方法的第二个参数
withCount用于在不获取关联数据的情况下提供关联数据的统计 在查询一对多或 多对多关联的时候才需要

load 适用于在数据集的延迟预载入关联查询  默认的数据集查询类型系统提供了 load_relation 助手函数 作用是等效的
together 用于一对一的关联自动写入操作 包括新增、更新和删除  提供了更简单的关联写入机制
作用不尽相同 但使用方法类似
四个方法只有一个参数
参数类型包括字符串和数组
且数组方式还支持索引数组以方便完成关联的自定义查询
 
relation方法 关联方法的 查询用法
// 查询用户 Profile关联数据
$users = $user->relation('profile')->select();
// 查询用户的Book关联数据
$users = $user->relation('books')->select();
关联查询 返回 包含User对象实例的数据集
relation 设定的关联查询结果只是数据集中 User模型对象实例的某个关联属性
relation 传入的字符串 是关联定义的方法名 不是关联模型的名称
由于模型方法名使用的都是驼峰法规范
假设定义了 名为userBooks的关联方法
relation方法可用两种方式的关联查询
// 驼峰法的关联方法定义
$users = $user->relation('userBooks')->select();
// 或者使用下面的方式等效
$users = $user->relation('user_books')->select();
第一种传入 驼峰法关联方法名userBooks
第二种 传入小写和下划线的转化名称user_books
两种关联查询用法都会实际定位到关联方法名称userBooks
所以关联方法定义必须使用驼峰法

对于上面的关联查询用法
在获取关联查询数据的时候
支持两种方式
foreach ($users as $user) {
dump($user->userBooks);
}
或者
foreach ($users as $user) {
dump($user->user_books);
}
默认 关联方法获取的是满足关联条件的所有数据

自定义关联查询
//自定义关联查询
$user->relation(['books' => function ($query) {
$query->where('title', 'like', '%think%');
}])->select();
查询用户 标题 包含think的书籍
闭包可以使用查询条件 支持其它的链式方法
比如对关联数据进行排序和指定字段
//自定义关联查询
$user->relation(['books' => function ($query) {
    $query->field('id,name,title,pub_time,user_id')
    ->order('pub_time desc')
    ->whereTime('pub_time', 'year');
}])->select();

field方法指定查询字段
务必包含当前模型的主键及关联模型的关键键 否则关联查询失败
关联方法可同时指定多个关联 即使是不同的关联类型
// 查询用户 Profile和Book关联数据
$users = $user->relation('profile,books')->select();
数组方式等效
// 查询用户的Profile和Book关联数据
$users = $user->relation(['profile','books'])->select();
数组 主要需要使用闭包自定义关联查询的情况 否则用逗号分割的字符串就可以了

together方法不支持闭包 但支持数组定义多个关联方法
关联查询
有两种方式关联数据获取
关联预查询和关联延迟查询
关联预查询 是用relation方法
// 指定User模型的profile关联
$user = User::relation('profile')->find(1);
// profile关联属性也是一个模型对象实例
dump($user->profile);
relation方法 传入关联 方法 名称  多个用逗号分割的字符串或数组
无论是否最终获取profile属性 都会事先进行关联查询 因此称为关联预查询

若关联数据不存在 一对一关联返回 null
一对多关联 返回 空数组或空数据集对象

考虑性能 通常选择关联延迟查询
// 不指定关联
$user = User::get(1);
// 获取profile属性的时候自动进行关联查询
dump($user->profile);
这种方式关联查询是惰性的 只有在获取关联属性时才实际关联查询 称之为关联延迟查询
关联属性的名称一般就是关联 定义 方法的名称 但同时也支持驼峰关联方法的小写+下划线转化名称
关联自定义查询
模型的关联方法除了自动在关联获取自动调用外
查询构造器的链式操作来对待 以完成额外的附加条件或其它自定义查询 一对多的关联关系时候比较多见类似场景
如User模型定义了一个articles的hasMany关联
namespace app\index\model;
use think\Model;
class User extends Model{
public function articles(){
return $this->hasMany('Article');
}
}
普通的关联查询获取全部的关联数据
$user = User::get(1);
$articles = $user->articles;
articles返回类型根据Article模型数据集返回类型设定
若Article模型返回的数据集类型是Collection 那么关联数据集返回Collection对象
若对关联数据筛选 需要查询用户发表的标题里面包含think文章 且按create_time倒序排序 则可用
$user     = User::get(1);
$articles = $user->articles()
->where('title', 'like', '%think%')
->order('create_time desc')
->select();

调用articles()关联方法的动作有
切换当前模型到关联模型对象 Article
且自动传入关联条件 user_id = 1
若是一对多或者多对多关联 并且希望自主条件查询关联数据的话请参考该方式
若改变默认关联查询条件而不在外部查询时指定 可直接在定义关联时添加额外条件
namespace app\index\model;
use think\Model;
class User extends Model{
public function articles(){
    return $this->hasMany('Article')->where('title', 'like', '%think%')->order('create_time desc');
}
}
关联方法的查询条件自动作为关联查询的条件带入
关联查询出来的数据就是包含额外条件
$user = User::get(1);
$articles = $user->articles;
若需外部调用时追加额外条件 关联查询包含了关联方法里面定义的和额外追加的条件
$user     = User::get(1);
$articles = $user->articles()
->where('name', 'think')
->field('id,name,title')
->select();
若担心基础关联条件定义影响其它查询 可以单独定义多个关联关系 各自独立使用互不影响
namespace app\index\model;
use think\Model;
class User extends Model{
public function articles(){
    return $this->hasMany('Article');
}
public function articlesLike($title){
    return $this->hasMany('Article')
        ->where('title', 'like', '%' . $title . '%')
        ->field('id,name,title')
        ->order('create_time desc');
}    
}
articlesLike 作为自定义关联查询专用  且需 传入title参数
$user = User::get(1);
$articles = $user->articlesLike('think')->select();
用法错误
$user = User::get(1);
$articles = $user->articlesLike;
带有参数的关联定义方法不能直接用于关联属性获取 只能用于链式关联自定义查询

关联约束
hasMany关联关系 系统提供根据关联数据条件查询当前模型数据的关联约束方法
has和hasWhere两个方法
has用于查询关联数据的记录数作为当前模型的查询依据 默认存在一条数据即可
// 查询有评论数据的文章
$list = Article::has('comments')->select();
可指定关联数据的数量查询
// 查询评论超过3个的文章
$list = Article::has('comments', '>', 3)->select();
has方法的第二个参数支持>、>=、<、<= 以及 = 第三个参数是一个整数
若需要复杂的关联查询约束条件的话 可以使用hasWhere方法 例如:
// 查询评论状态正常的文章
$list = Article::hasWhere('comments', ['status' => 1])->select();
或者直接使用闭包查询 然后在闭包里面使用链式方法查询:
// 查询最近一周包含think字符的评论的文章
$list = Article::hasWhere('comments', function ($query) {
    $query->whereTime('create_time', 'week')
    ->where('content', 'like', '%think%');
})->select();
闭包方式查询 若查询的关联模型字段同时存在当前模型和关联模型的话 要加上关联模型的名称作为别名
// 查询最近一周包含think字符的评论的文章
$list = Article::hasWhere('comments', function ($query) {
    $query->whereTime('Comment.create_time', 'week')
    ->where('content', 'like', '%think%');
})->select();
has支持hasWhere所有用法

关联预载入
关联查询只是为了方便 实际的应用 查询多个数据若数据较多 关联查询产生的性能开销大
比如查询用户Profile关联数据 若有100个用户数据 就会产生100+1次查询
是N+1查询问题 关联预载入功能提供更好的性能
关联查询的预查询载入功能 解决N+1次查询的问题
查询若有3个记录 会执行4次查询
$list = User::all([1, 2, 3]);
foreach ($list as $user) {// 获取用户关联的profile模型数据
    dump($user->profile);
}
若用关联预查询功能
一对一关联 默认只有一次查询
一对多关联 就变成2次查询
有效提高性能
关联预载入用with方法指定需要预载入的关联
用法和relation方法类似
$list = User::with('profile')->select([1, 2, 3]);
foreach ($list as $user) {// 获取用户关联的profile模型数据
dump($user->profile);
}
关联的预载入查询不是惰性的
连同数据查询一起完成 由于封装的合并查询 性能远优于普通的关联惰性查询
所以整体的查询性能是非常乐观
鉴于预载入查询的重要性 模型的get和all方法的第二个参数可直接传入预载入参数
预载入查询和前面是等效
$list = User::all([1, 2, 3], 'profile');
foreach ($list as $user) {// 获取用户关联的profile模型数据
dump($user->profile);
}
嵌套预载入
若关联模型本身还需要进行关联预载入的话
可在当前模型预载入查询的时候直接指定 理论上嵌套是任意级别的 但实际上估计不会有这么复杂的关联设计
假设Profile模型还关联了一个名片模型 cards关联方法  嵌套预载入查询
$list = User::all([1, 2, 3], 'profile.cards');
foreach ($list as $user) {// 获取用户关联数据
    dump($user->profile->cards);
}
一对一关联的JOIN方式不支持嵌套预载入
预载入条件限制
可在预载入时通过闭包指定额外的条件限制 不要在闭包里面执行任何的查询
$list = User::with(['articles' => function ($query) {
    $query->where('title', 'like', '%think%')
    ->field('id,name,title')
    ->order('create_time desc');
}])->select([1, 2, 3]);
foreach ($list as $user) {// 获取用户关联的profile模型数据
dump($user->profile);
}
一对一预载入查询的条件限制 field方法要改为withField方法 否则会产生字段混淆

延迟预载入
根据查询出来的数据决定是否使用关联预载入
关联查询本身就能解决这个问题 因为关联查询是惰性的
不过用预载入的理由也很明显 性能具有优势
延迟预载入仅针对多个数据的查询 因为单个数据的查询用延迟预载入和关联惰性查询没有任何区别
所以不需要用延迟预载入
若数据集查询返回的是数据集对象 可调用数据集对象的load实现延迟预载入
// 查询数据集
$list = User::all([1, 2, 3]);// 延迟预载入
$list->load('cards');
foreach ($list as $user) {// 获取用户关联的card模型数据
    dump($user->cards);
}
若数据集查询返回的是数组 系统提供load_relation助手函数完成同样的功能
// 查询数据集
$list = User::all([1, 2, 3]);// 延迟预载入
$list = load_relation($list, 'cards');
foreach ($list as $user) {// 获取用户关联的card模型数据
dump($user->cards);
}

关联统计
不需要获取关联数据 只希望获取关联数据的统计 关联统计仅针对一对多或者多对多的关联关系
可用withCount方法进行制定关联的统计
$list = User::withCount('cards')->select([1, 2, 3]);
foreach ($list as $user) {// 获取用户关联的card关联统计
echo $user->cards_count;
}

关联统计在模型的对象属性中自动添加以“关联方法名+_count”为名称的动态属性保存相关的关联统计数据
若要对关联统计进行条件过滤 可用
$list = User::withCount(['cards' => function ($query) {
    $query->where('status', 1);
}])->select([1, 2, 3]);
foreach ($list as $user) {// 获取用户关联的card关联统计
echo $user->cards_count;
}
一对一关联关系用关联统计是无效的
一般用exists查询来判断是否存在关联数据

关联输出
关联属性的输出和模型的输出转换一样
用模型的toArray方法同时输出关联属性 对象
$user = User::get(1,'profile');
$data = $user->toArray();
dump($data);
$data = $user->toJson();
dump($data);
使用关联预载入查询和手动获取了关联属性 延迟关联查询 的情况 toArray和toJson方法都会包含关联数据
调用visible和hidden方法对当前模型及关联模型的属性进行输出控制
$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile.email'])->toArray();
返回的data数据不包含用户模型的name属性及关联profile模型的email属性

隐藏多个关联属性
$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile' => ['email', 'address']])->toArray();
模型的visible方法 用于设置需要输出的属性 的用户和hidden一致
同时调用visible和hidden方法 visible优先 所以下面的profile关联属性输出会包含email和sex
$user = User::get(1, 'profile');
$data = $user->visible(['profile' => ['email', 'sex']])->hidden(['name', 'profile' => ['email', 'address']])->toArray();
没有关联查询 也可以在输出时追加关联属性

$user = User::get(1);
$user->append(['profile'])->toArray();
用toArray方法时候才进行profile关联数据获取并转换输出
对于数据集查询 若返回类型是数据集对象仍然支持调用visible、hidden和append方法 若不是数据集对象的话可以先用collection助手函数转换为数据集对象
$users = User::all();
$data  = $users->hidden(['name', 'profile' => ['email', 'address']])->toArray();

关联实例
关联查询、自定义条件查询、关联 及嵌套 预载入、延迟预载入、关联约束和关联统计

关联查询实例
关联类型的新增、更新和删除等
理解模型和对象概念 关联的新增、更新和删除
数据表结构如下
city
id - integer
name - string
user
id - integer
name - integer
email - string
city_id - integer

role
id - integer
name - string
auth
user_id - integer
role_id - integer
add_time - dateTime

blog
id - integer
name - string
title - string
cate_id - integer
user_id - integer

content
id - integer
blog_id - integer
data - text
cate
id - integer
name - string
title - string

comment
id - integer
content - text
commentable_id - integer
commentable_type - string

模型类
City模型
namespace app\index\model;
use think\Model;
class City extends Model{
    
public function users(){
    return $this->hasMany('User');
}    
    
public function blog(){
    return $this->hasManyThrough('Blog', 'User');
}    
}
User模型
namespace app\index\model;
use think\Model;
class User extends Model{

public function roles(){
    return $this->belongsToMany('Role', 'auth');
}
    
public function blogs(){
    return $this->hasMany('Blog');
}    

public function comments(){
    return $this->morphMany('Comment', 'commentable');
}    
}
Role模型
namespace app\index\model;
use think\Model;
class Role extends Model{

public function users(){
return $this->belongsToMany('User', 'auth');
}
}
Blog模型
namespace app\index\model;
use think\Model;
class Blog extends Model{

public function user(){
return $this->belongsTo('User');
}
    
public function content(){
return $this->hasOne('Content');
}    
    
public function cate(){
return $this->belongsTo('Cate');
}    

public function comments(){
    return $this->morphMany('Comment', 'commentable');
}    
}
Content模型
namespace app\index\model;
use think\Model;
class Content extends Model{

public function blog(){
return $this->belongsTo('Blog');
}
}
Cate模型
namespace app\index\model;
use think\Model;
class Cate extends Model{

public function blogs(){
return $this->hasMany('Blog');
}
}
Comment模型
namespace app\index\model;
use think\Model;
class Comment extends Model{

public function commentable(){
return $this->morphTo();
}
}

不同关联方法的参数说明参考关联定义
auth数据表不需要创建模型
多对多关联 中间表是不需要关注
一对一关联
一对一关联包含hasOne和belongsTo两种关联关系定义
系统对一对一关联尤其是hasOne做了强化 用博客模型和内容模型之间的关联为例
普通情况的关联操作
[ 新增 ]
$blog        = new Blog;
$blog->name  = 'think';
$blog->title = 'think5关联实例';
if ($blog->save()) {
    $content       = new Content;
    $content->data = '实例内容';
    $blog->content()->save($content);
}
支持使用数组方式新增数据
$data = [
'name'  => 'think',
'title' => 'think5关联实例',
];
$blog    = Blog::create($data);
$content = [
'data' => '实例内容',
];
$blog->content()->save($content);

[ 查询 ]
普通关联查询
$blog = Blog::get(1);
echo $blog->content->data;
预载入关联查询
$blog = Blog::get(1,'content');
echo $blog->content->data;
数据集查询
$blogs = Blog::with('content')->select();
foreach ($blogs as $blog) {
dump($blog->content->data);
}
默认一对一关联查询也是使用2次查询 若希望获取更好的性能 可修改关联定义
    
public function content(){// 修改关联查询方式为JOIN查询方式
return $this->hasOne('Content')->setEagerlyType(0);
}   
修改后 关联查询从原来默认的IN查询改为JOIN查询 减少一次查询 指定的关联表字段field方法必须改为withField方法
[ 更新 ]
// 查询
$blog = Blog::get(1);// 更新当前模型
$blog->title = '更改标题';
$blog->save();
// 更新关联模型
$blog->content->data = '更新内容';
$blog->content->save();
[ 删除 ]
// 查询
$blog = Blog::get(1);
// 删除当前模型
$blog->delete();
// 删除关联模型
$blog->content->delete();
为了更简单的使用一对一关联的写入操作 系统提供关联自动写入功能
比较下面的代码就会发现写入操作和之前的写法更简洁了
[ 新增 ]
$blog          = new Blog;
$blog->name    = 'think';
$blog->title   = 'think5关联实例';
$blog->content = ['data' => '实例内容'];
$blog->together('content')->save();
当然 还可以更加对象化
$blog          = new Blog;
$blog->name    = 'think';
$blog->title   = 'think5关联实例';
$content       = new Content;
$content->data = '实例内容';
$blog->content = $content;
$blog->together('content')->save();
甚至可以把关联属性合并到主模型进行赋值后写入
$blog        = new Blog;
$blog->name  = 'think';
$blog->title = 'think5关联实例';
$blog->data  = '实例内容';
$blog->together(['content' => ['data']])->save();
若不想这么麻烦每次调用together方法 也可以直接在模型类中定义relationWrite属性 但必须是数组方式 不过考虑到模型的独立操作的可能性 并不建议
[ 查询 ]
关联查询支持把关联模型的属性直接附加到当前模型
$blog = Blog::get(1);
$blog->appendRelationAttr('content', 'data');
echo $blog->data;
若不想每次都附加操作的话 可以修改Blog模型的关联定义如下:
    
public function content(){
return $this->hasOne('Content')->bind('data');
}   
现在就可以直接使用
$blog = Blog::get(1, 'content');
echo $blog->data;
数据集的用法基本上类似
[ 更新 ]
采用关联自动更新
$blog          = Blog::get(1);
$blog->title   = '更改标题';
$blog->content = ['data' => '更新内容'];// 更新当前模型及关联模型
$blog->together('content')->save();
更加对象化的写法
$blog                = Blog::get(1);
$blog->title         = '更改标题';
$blog->content->data = '更新内容';// 更新当前模型及关联模型
$blog->together('content')->save();
一样可以支持关联属性合并到主模型操作
$blog        = Blog::get(1);
$blog->title = '更改标题';
$blog->data  = '更新内容';// 更新当前模型及关联模型
$blog->together(['content' => 'data'])->save();
在关联方法中使用bind方法把关联属性绑定到当前模型并不会影响关联写入 必须使用数组方式来明确告知当前模型哪些属性是关联的绑定属性
[ 删除 ]
关联自动删除的操作很简单
$blog = Blog::get(1);// 删除当前及关联模型
$blog->together('content')->delete();

一对多关联
包括hasMany和belongsTo两种关联关系
以用户和博客模型为例
一对多关联查询为主 关联写入比起单独模型的操作没有优势
建议一对多的关联写入仍然由各个独立模型完成

查询某个用户的博客
$user = User::get(1);// 用户的所有博客
dump($user->blogs);// 条件搜索
dump($user->blogs()->where('cate_id', 1)->select());
若需要对关联数据进行额外的条件查询、更新和删除操作就可以使用blogs方法
反过来 若需要查询博客所属的用户信息 可以使用
$blog = Blog::get(1);
dump($blog->user->name);

远程一对多
远程一对多的作用是跨过中间模型操作查询另外一个远程模型的关联数据
而这个远程模型通常和当前模型是没有任何关联
一个用户发表了多个博客
一个城市有多个用户
假设城市和博客之间没有直接关联
若需要获取某个城市下面的所有博客 利用已经掌握的关联概念是可以实现的 只是需要通过两次关联操作来获取 代码看起来类似下面:
$city  = City::getByName('shanghai');
$blogs = [];
foreach ($city->users as $user) {
    $blogs[$user->id] = $user->blogs()->order('id desc')->limit(100)->select();
}
// 然后对博客数据进行额外组装处理
思路清晰 但麻烦 还要对数据进行组装 且不便于统一排序和限制
例如希望一共取出100个博客数据就不好办
为了简化引入了远程一对多的关联关系来更好的解决 在City模型中已经定义了blogs关联
$city  = City::getByName('shanghai');
$blogs = $city->blogs()
->order('id desc')
->limit(100)
->select();
看起来直观很多 且对博客数据的自定义查询也相当方便
无论性能还是功能更佳
不需要对用户模型查询操作

本实例的假设前提是城市和博客模型之间没有任何直接关联
架构的优化对于代码的优化来说有时候更有效

多对多关联
多对多关联较前面两种关联来说复杂很多 但越是复杂越能体现出模型关联的优势
用户和角色模型来看下如何操作多对多关联
多对多关联关系必然会有一个中间表 最少必须包含两个字段
auth表就包含了user_id 和 role_id 建议对这两个字段设置联合唯一索引
但中间表仍然可以包含额外的数据

中间表不需要创建模型 auth表没有对应模型
多对多关联关系创建一个虚拟的中间表模型 也称之为枢纽模型
Pivot 对中间表的所有操作只需要对该模型进行操作即可
一般情况无需关注中间表的存在就轻松完成多对多关联操作
 
多对多的关联写入操方式
用户和角色数据独立写入 通过关联完成中间表的写入
用户数据独立写入 通过关联完成角色数据和中间表数据写入
角色数据独立写入 通过关联完成用户数据和中间表数据写入 多对多关联相互之间操作是等同的  
 
通过关联单独完成中间表数据更新及删除
多对多的关联写入操作
需要两个方法
除非模型独立操作 一般不需用save方法
 
方法     描述
attach     附加关联的一个中间表数据
detach     解除关联的一个或多个中间表数据
首先完成第一种方式 仅仅操作中间表数据
$user = User::get(1);
$role = Role::getByName('admin');// 查询角色
$user->roles()->attach($role->id);// 增加用户-角色数据
若中间表有额外数据需要写入
$user = User::get(1);// 查询用户
$role = Role::getByName('admin');// 查询角色
// 传入中间表的额外属性
$user->roles()->attach($role->id, ['add_time' => '2017-1-18']);
attach方法很智能 第一个参数识别包括数字、字符串、数组和模型实例并做出不同的处理
参数类型     作用描述
数字或字符串 要附加中间表的关联模型主键
索引数组     首先写入关联模型 然后附加中间表
普通数组     附加多个关联数据的主键
模型实例     附加关联模型
若要添加的角色尚未创建 则添加用户-角色数据
$user = User::get(1);// 查询用户
// 增加用户-角色数据 并同时创建新的角色
$user->roles()->attach([// 添加一个编辑角色
'name' => 'editor',
]);
获取新增的角色表自增主键ID 最新版本的attach方法返回的是一个 Pivot 模型对象
$user = User::get(1);// 查询用户
// 增加用户-角色数据 并同时创建新的角色
$pivot = $user->roles()->attach([// 添加一个编辑角色
'name' => 'editor',
], ['add_time' => '2017-1-31']);
// 获取中间表的数据
echo $pivot->role_id;
echo $pivot->user_id;
echo $pivot->add_time;

下面则表示给用户添加多个角色授权:
$user = User::get(1);
// 给用户授权多个角色 根据角色主键
$user->roles()->attach([1, 2, 3], ['add_time' => '2017-1-31']);
解除一个用户的角色
$user = User::get(1);
$role = Role::getByName('admin');
$user->roles()->detach($role->id);// 删除中间表数据
可以同时解除用户的多个角色权限
$user = User::get(1);// 删除中间表数据
$user->roles()->detach([1, 2, 3]);

解除用户的所有角色可以用
$user = User::get(1);// 删除中间表数据
$user->roles()->detach();

解除用户的权限同时删除这个角色
$user = User::get(1);
$role = Role::getByName('test');// 查询角色
// 删除中间表数据以及关联表数据
$user->roles()->detach($role->id,true);

每个关联模型数据还有一个额外的枢纽模型数据
$user = User::get(1);// 查询用户

$roles = $user->roles;// 获取用户的角色
foreach ($roles as $role) {// 输出用户的角色名
echo $role->name;// 获取中间表模型
dump($role->pivot);
}

多态一对多
多态关联允许一个模型在单个关联定义方法中从属一个以上其它模型
如用户可评论书和文章 但评论表通常都是同一个数据表的设计
多态一对多关联关系 是为了满足类似的使用场景而设计
多态一对多关联涉及关联查询 关联写入不建议通过关联操作完成 确保用各自的模型独立完成数据写入
多态一对多的多态表设计很重要
如评论表因为需要保存多个模型的评论数据 可以设计成多态关联
获取博客的评论数据
$blog = Blog::get(1);
foreach ($blog->comments as $comment) {
    dump($comment);
}
评论筛选过滤
$blog     = Blog::get(1);
$comments = $blog->comments()
->where('content', 'like', '%think%')
->order('id desc')
->limit(20)
->select();
foreach ($comments as $comment) {
    echo $comment->content;
}
评论模型操作
$comment = Comment::get(1);
$commentable = $comment->commentable;
Comment 模型的 commentable 关联返回 Blog 或 User 模型的对象实例
取决于评论所属模型的类型
若多态类型字段保存的数据并非是模型名称之类的
而是采用数字保存 提高存储和查询性能  比如1表示博客 2表示用户
关联定义方法需要对应修改为:
Blog模型

public function comments(){
return $this->morphMany('Comment', 'commentable', 1);
}   
User模型

public function comments(){
return $this->morphMany('Comment', 'commentable', 2);
}   
Comment模型

public function commentable(){
return $this->morphTo(null, [
    '1' => 'Blog',
    '2' => 'User',
]);
}
若模型用不同的命名空间 使用完整的命名空间方式定义

public function commentable(){
return $this->morphTo(null, [
    '1' => 'app\model\Blog',
    '2' => 'app\model\User',
]);
}

模型关联的概念