结合之前的设计,我们已经有了用户数据的增删查改,接下来我们利用一对一关联实现为用户增加个人喜好。在数据库中创建tp_hobby数据表如下:
接下来是具体的代码实现:
首先在app/model目录下新建一个hobby.php文件,在里面直接继承think/model即可。接下来,在User.php中写入如下方法:
public function hobby(){
return $this->hasOne(Hobby::class,'user_id','id');
}
解释:一般这里的函数名和被正向关联的数据表一致,这个是为了可读性,其实修改函数名同样可行。在hasOne函数的三个参数中,第一个参数是关联的数据模型(一般一个模型对应一个数据表),也可以写成’Hobby’,上述增加::class的写法“更加优雅”,第二个参数是被关联的数据表的键值(被称为“外键”),第三个参数是发起关联的数据表的键值(被称为“内键”),第二个参数和第三个参数是可省略的。
接下来将用户喜好关联到实际的数据表中去。借用bootstrap提供的tooltip插件,我们希望用户喜好能够在用户昵称右边显示,因此修改表格UI,将view/user/index.html用户昵称处的td改成如下代码:
<td class="text-center">
<span class="tooltip-toggle" data-toggle="tooltip"
data-placement="right" title="{$obj.hobby.content}">{$obj.username}</span>
</td>
并在{block name=“js”}{/block}代码块中增加如下方法:
//tooltip需要绑定js触发
$('[data-toggle="tooltip"]').tooltip();
结果发现报错如下:
结果一波分析,猜测是数据表设计存在问题,因为这里出现了null提示,在数据表中用户喜好是可以为空的,因此修改用户喜好处的title内容,做一个为空检测,代码如下:
title="{$obj.hobby.content ? $obj.hobby.content: ''}"
数据关联展示只是一部分,接下来希望能够实现数据的关联新增。首先,在create.html页面中增加个人喜好:
然后,在app/controller/User.php文件中找到save方法,在其中调用Usermodel的create方法之后写入如下代码:
UserModel::find($id)->hobby()-save([
'content' =>$data['hobby']
]);
解释:通过id找到对应的数据字段,然后跳转到关联数据表对应字段新增数据字段,新增用户关联成功结果展示如下:
接下来尝试进行数据的关联删除,在delete方法中将Usermodel调用destroy方法部分改写成如下代码:
UserModel::with(['hobby'])->find($id)->together(['hobby'])->delete()
注意:经过尝试发现不写with()方法关联查询数据表,实际上不能达到关联删除的目的,只会删除主表,副表数据依然会保留(部分文章介绍关联查询时没写with,其实是错误的)。
接下来就是关联修改了,通过尝试发现,关联修改是无法直接实现的。下面来详细分析修改的各种写法的效果。
(1)第一种代码,能够实现数据表的关联修改,但是关联修改的方式其实是通过先修改主表,之后修改副表实现的:
UserModel::find($id)->update($data)&&UserModel::find($id)->hobby->save($data)
(2)第二种代码,代码如下:
UserModel::find($id)->hobby->save()
解释:上述代码是先找到对应的关联副表,然后修改数据,最终只能完成修改副表。需要补充的是如果hobby写成hobby(),系统不会报错,实现的效果也将是一样的。但是需要明确的是hobby关联与hobby()方法两者对于数据的处理是不一样的,eg. 如果后面接的是如下代码,二者的区别才会真正显示出来:
->save(["content"=>"test"])
表现的效果为,前者将会在对应$id处修改成save中的对应的hobby(也就是test,之前表单中的内容会被掩盖掉),而hobby()则不一样,hobby()将会新增数据,其执行过程为先修改然后将save中的数据增加到副表中。但是由于唯一键的限制,在副表中会由于id重复出错,报如下bug:
这种执行过程也就解释了上面hobby与hobby()效果一致。
(3)第三种代码,代码如下:
UserModel::find($id)->update($data)
UserModel::find($id)->save($data)
UserModel::find($id)->together(['hobby'])->save($data)
解释:update必须含有至少一个参数,save方法的底层其实就是没有数据便增加有数据就修改(这里的id是固定存在的,因此save也是修改),together参数需要为数组,最终实现的效果是只能修改主表,因为这个方式副表关联无效;需要说明的是第三行代码有些文章是认为可以实现关联修改,但是实际上并不能够修改副表。
同样的,反向关联也是如此,比如新建一个grade类,利用该类反向绑定用户列表,对应的数据表结构如下:
新增数据如下:
在需要进行反向关联的model类中使用增加如下方法:
public function Grade(){
return $this->belongsTo('User','user_id','id');
}
之后在controller目录下新建Grade.php并增加如下方法进行数据测试:
public function index()
{
$user=UserGrade::find(1)->grade;
return json($user);
}
成功展示了id=1对应的user_id在被反向关联的数据表tp_user中蜡笔小新的数据,结果展示如下:
这里发现json展示不太美观,于是想起来了json美化插件-JSON-handle,Chrome一般无法直接访问插件市场,安装JSON-handle教程,美化后的json如下:
注:(1)这里使用反向查询的时候用的是grade而不是grade(),可以理解成直接使用类名表示使用的是属性,而后者表示的使用对应的方法;
(2)主副表存在的意义是很多用户关联数据比如上面的喜好等并不是常用信息,如果只设计一个表,那么用户登录每次都需要遍历这个数据表,这样影响整个数据读取验证的效率,显得有些臃肿,因此通过副表存放非常用信息能够提高整个程序的处理效率。
其实一对多关联和一对一关联有很多类似的地方。为了满足这个需求,在hobby中设置多个hobby对应同一个人,比如给蜡笔小新多设置一个hobby,得到的数据表如下:
接下来修改原来的model中的user与hobby之间的对应的关系,修改成一对多的模式:
public function hobby(){
return $this->hasMany('hobby','user_id','id');
}
测试在controller/User.php中加入如下测试代码:
$user=UserModel::find(1)->hobby;
return json($user);
得到运行结果如图:
从图中可以看出通过绑定了一对多的关联模型,数据便能一次性获取与主表id对应的所有附表信息,同时也能使用where等进行筛选。相反,如果不使用一对多模型,则只能取出第一个从副表中找到的数据。
通常,对于一对多关联模型有如下函数处理方法:
(1)关联查询,数据获取;
return json(UserModel::find(1)->hobby);
return json(UserModel::find(1)->hobby()->select());
解释:和前面一样,这里的hobby是一种属性,而hobby()应该理解成一种方法。
(2)关联查询,数据where
筛选;
return json(Usermodel::find(1)->hobby()->where('id','>=',10)->select());
注:需要补充说明的是,这里并不建议使用第一种方法获取数据,因为第一种数据的二级筛选下标与根据原来的数据下标一致,比如在原来的附表中,主表中id=1的用户有多个喜好,对应了一个喜好数组,但是通过条件查询,第一种方法会将这个和附条件的数据连同它们在整个副表数组中的下标也抽离出来(对应的效果如下图所示),这样不便于数据的直接使用。
return(UserModel::has('hobby','>=',2)->select());
解释:这里的has方法是根据附表中可查数据条数作为依据,返回主表信息。第一个参数是其关联的副表名称,第二个参数是查询条件,第三个数据是数据条数。
(4)关联查询,数据haswhere
筛选;
return(UserModel::haswhere('hobby',['status'=>0])->select());
解释:haswhere方法是通过副表数据来返回主表信息,第一个参数是副表表名,第二个参数是查询条件。补充一点这里的select返回数组下面从0开始的,并且使用find默认返回符合条件的第一个,使用select返回所有数据。
(5)其他方法,参照上面一对一关联模型即可。
多对多关联最经典的实例便是权限控制,比如在一个用户列表中,一个用户可以对应多个管理员权限,而一种管理员权限又能够反过来对应多个用户。
接下来,就拿权限管理为例,总结多对多关联的使用方式。我们知道对于后台而言,往往只有特定用户才会拥有后台的账户,因此我们新建一个具有后台权限的用户表tp_auth,而后台管理员往往有多个,并且不同的种类。因此新建一个tp_role,用来标定这些不同的管理员用户类别。这样就完了吗?不,需要将不同的身份和后台管理员绑定起来,还需要建立一个tp_access表,数据实例展示如下:
同样的,一个表对应一个model类,方便之后对数据进行操作,因此在controller/model目录下新建auth.php、role.php和access.php三个文件,auth和role直接继承model类,但这个access需要继承Pivot(译为枢纽,中心的意思)类,pivot类其实也是继承了think/model,只是在构造函数上面稍微修改了一点,方便表之间更好地协调工作。接下来在controller\model\auth.php文件中新增方法如下:
//多对多继承
public function roles(){
return $this->belongsToMany(Role::class,Access::class,'role_id','auth_id');
}
解释:多对多关联的发起对象可以选择不同,那么之后的对应关系也需要相应改变,这里使用的auth作为关联的发起对象,role为关联对象,接下来的第二个参数是中间模型,第三个参数和第四个参数分别是【“外键”,“关联键”】,使用如下代码进行测试:
public function many(){
$user=UserAuth::find(2);
$role=$user->roles;
return json($role);
}
得到的结果如下:
同样的,我们可以将权限role作为关联的发起项,首先找到权限,然后通过权限找到具有这些权限的用户(当然实际可能不会这么做)。
接下来使用户数据新增部分,对于用户数据的新增,和上面提到的一对一的数据新增一致。接着使用权限管理的多对多模型,增加管理员种类代码如下:
public function addKind(){
UserAuth::find(1)->roles()->save(['type'=>'广告管理员','uri'=>'All']);
}
程序运行后的数据表结果如下:
解释:这种在save方法内部继续设置新增数据的,AuthModel会首先找到id=1的数据段,然后通过roles()关联到关联模型,执行save方法里面的存储数据,实现同步新增。实际上这种应用基本不会出现的,管理员种类如果新增一般是直接在数据表中新增,save会先匹配主表和附表中的字段,如果有,就会新增到对应的id数据字段中,如果没有就会舍弃。
对于直接新增管理员身份,使用如下方法:
UserAuth::find(1)->roles()->save(RoleModel::find(3));
注:这里直接写save(3)也是可以的,但是不太清晰,因此加上角色模型的find方法。同样的使用saveAll([1,2,3])同时增加多个身份也是可以的。此外,可以使用attach方法代替save方法,并且attach相比于save方法还能再传一个参数,eg.attach(RoleModel::find(3),[‘details’=>‘修改access表中的details字段’])。