Yii2 | 论 AR 中魔术方法和延迟加载

378 阅读6分钟

__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
<table class="table">
    <tr>
        <th>会员ID</th>
        <th>会员名</th>
        <th>所在组</th>
        <th>组ID</th>
        <th>所属工作</th>
        <th>工作ID</th>
    </tr>
    <?php foreach ($users as $u):?>
        <tr>
            <td><?= $u->id;?></td>
            <td><?= $u->username;?></td>
            <td><?= $u->group()->name;?></td>
            <td><?= $u->group()->id;?></td>
            <td><?= $u->job()->name;?></td>
            <td><?= $u->job()->id;?></td>
        </tr>
    <?php endforeach;?>
</table>

大概用了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
<table class="table">
    <tr>
        <th>会员ID</th>
        <th>会员名</th>
        <th>所在组</th>
        <th>组ID</th>
        <th>所属工作</th>
        <th>工作ID</th>
    </tr>
    <?php foreach ($users as $u):?>
        <tr>
            <td><?= $u->id;?></td>
            <td><?= $u->username;?></td>
            <td><?= $u->group->name;?></td>
            <td><?= $u->group->id;?></td>
            <td><?= $u->job->name;?></td>
            <td><?= $u->job->id;?></td>
        </tr>
    <?php endforeach;?>
</table>

结果图

数据关联 & 延迟加载

通过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小站 nai8.me

(完)