__get、关联、延迟加载,听起来好悬,其实很简单,看下文。
YII的魔术方法 __get
什么是关联 & 延迟加载
昨日拿出大把时间对yii2的get魔术方法以及关联属性进行了一番研究,先分享给大家,我想这也是很多人,尤其初学者比较蒙的一个地方。
我们从一个例子入手,在这里我们需要三张表来说明。
其中 user_group 和 user_job 表在 user 表中靠 group_id 和 job_id 分别关联。
我们要实现这样一个表格
会员ID | 会员名 | 所在组 | 组ID | 所属工作 | 工作ID |
---|---|---|---|---|---|
1 | abei | 学生 | 2 | 程序员 | 1 |
2 | 郑讯 | 学生 | 2 | 0 |
开始啦
我叫小明来实现这个需求,大约过了30分钟,它实现了,代码是这样写的。
// models/User.php
class User extends ActiveRecord {
...
public function group(){
return UserGroup::find()->where(['id'=>$this->group_id])->one();
}
public function job(){
return UserJob::find()->where(['id'=>$this->job_id])->one();
}
...
}
// view index.php
会员ID
会员名
所在组
组ID
所属工作
工作ID
= $u->id;?>
= $u->username;?>
= $u->group()->name;?>
= $u->group()->id;?>
= $u->job()->name;?>
= $u->job()->id;?>
大概用了0.01秒的时间,我发现了一个问题,那就是我希望用 $u->group->name 这样的格式代替 $u->group()->name,这样多么的帅,而且我知道可以通过php的魔术方法来实现它。
回去吧,改进下再给我看。???
在小明修改代码期间,我在这里牢骚一下php的__get方法。
当调用一个未定义的属性时访问此方法 __get( $property ) ,是为在类和他们的父类中没有声明的属性而设计的。
举个例子吧,我们知道一个类
class Man {
public $data = [
'username'=>'abei2017',
'site'=>'nai8.me'
];
// 魔术方法
public function __get($name) {
if(in_array($name,array_keys($this->data))){
return $this->data[$name];
}
return false;
}
}
则当我们调用 $manObject->username的时候,php发现Man类此时并没有$username属性,因此会自动触发__get魔术方法,而此方法是我们自己定义的,最终$manObject->username 等价于 $manObject->data['username'];
看明白了吧,那么对于Yii2的AR类是如何定义其__get方法的那,下面我们来研究一下。
ActiveRecord的__get方法存在于其父类BaseActiveRecord中,我们看看它的实现。
public function __get($name)
{
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name];
} elseif ($this->hasAttribute($name)) {
return null;
} else {
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
return $this->_related[$name];
}
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
return $this->_related[$name] = $value->findFor($name, $this);
} else {
return $value;
}
}
}
这段代码并不难懂,当我们访问一个Model(AR)的对象的属性不存在时,Yii2会做调用__get魔术方法并做三件事情。
看看对象的_arrtibutes数组里是不是有,有则直接返回。
如果没有看看$this->hasAttribute()函数是否为真
否则就进入最后,也是最重要的环节,它首先查看_related数组里是不是有,有则返回,否则调用了父类的魔术方法__get,然后得到一个值。
对于父类的 $value = parent::__get($name); 我们大体看看
public function __get($name) {
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
....
}
看4行代码就可以明白了,我们总结一下,$value = parent::__get($name); 方法在寻找一个叫做getName的方法,如果该方法存在则返回。
说到这里,我想你应该明白我让小明改正的东西了吧。
10分钟过去了
小明给我提交了新的代码,我很高兴他改对了,看看这些改正。
// models/User.php
class User extends ActiveRecord {
...
public function getGroup(){
return UserGroup::find()->where(['id'=>$this->group_id])->one();
}
public function getJob(){
return UserJob::find()->where(['id'=>$this->job_id])->one();
}
...
}
// view index.php
会员ID
会员名
所在组
组ID
所属工作
工作ID
= $u->id;?>
= $u->username;?>
= $u->group->name;?>
= $u->group->id;?>
= $u->job->name;?>
= $u->job->id;?>
结果图
数据关联 & 延迟加载
通过ar的魔术方法规则,我们使用 getXXX 完成了代码的优化,现在可以像访问对象自身属性一样访问关联的模型数据了。
但是,是的,还有但是。
我打开了神器小强yii2-debug ,看了下数据库,我勒个去,这么多次查询。
小明,你是在玩我么?回去改!
等待是漫长的,过了30分钟,我看到了新的代码。
class User extends ActiveRecord {
...
public function getGroup(){
if($this->group_id <= 0){
return false;
}
return $this->hasOne(UserGroup::className(),['id'=>'group_id']);
}
public function getJob(){
if($this->job_id <= 0){
return false;
}
return $this->hasOne(UserJob::className(),['id'=>'job_id']);
}
...
}
我很高兴小明的这次改动又对了,不知道你看懂没?我来给你说下,先看看结果
好棒,数据库查询从9次减少到4次,内存占有量从4M减少到1M,优化的力量。
我们先对比分析一下,在会员表中一共有8次调用关联表的数据,加上对会员自己的一次select,因此我们第一次一共9次数据库的检索。
小明是如何进行优化的
在数据库之前先PHP判断,因为郑讯的job_id为0,因此不用进行数据库检索,这样省掉2次数据库查询
if($this->job_id <= 0){
return false
}
小经验:在我们做数据库查询之前,先用php进行一些判断,这样可以节省很多数据库资源,毕竟php执行个if啥的速度没话说。
另外小明使用了比如hasOne这样的方法,还有比如hasMany的,他们叫做关联方法
但是同样是返回对象,为何使用关联方法就能节省数据库的查询那?
现在跟着我再回头看看上面的AR魔术方法,秘密就在这里,我们一起探索下。
我们来谈之下最后一个分支,大体理解为
首先判断对象中$_related数组中是否含有,如果有直接返回
如果没有调用父类得到属性
如果属性是 ActiveQueryInterface 则存到$_related数组,如果不是直接返回。
秘密就在这里 hasOne、hasMany等关联返回了一个 ActiveQueryInterface 对象,那么发生了什么那?
我们看看上面会员的阿北数据行
会员ID | 会员名 | 所在组 | 组ID | 所属工作 | 工作ID |
---|---|---|---|---|---|
1 | abei | 学生 | 2 | 程序员 | 1 |
当我第一次使用 $u->group->name 获取组名的时候,因为返回对象是 ActiveQueryInterface 接口对象,因此存放到了当前对象的$_related数组中,当我在放问 组ID $u->group->id 时,直接从上次的$_related数组中拿出,并没有走数据库。
以你相对于自己写个查询语句,关联方法的结果是每个记录每个关联属性只查了一次数据库,节省了老多老多资源了。
可能你会问?那么自己写的那个XXX::find()->one() 是啥?它是一个AR,反正不是ActiveQueryInterface,也就无法存到$_related数组。
后来这个方式被很多框架所使用,那就起个名字吧,就叫做 延时加载。
大家开发的时候一定要善于利用它,提高性能必备哈。
这也是小明优化的主要一点,当然,对于遍历使用延迟加载也会遇到性能(n+1)问题,但因不属于本节内容,以后再单独分享。
小明的故事就这样过去了。
有一些将来要说的
本文可能衍生两个问题,以后阿北会进行分享
PHP中到底有多少魔术方法?Yii2在如何使用它们。
数据关联方法全部解密
也欢迎来到我的yii2小站 http://nai8.me
(完)