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

AlgoCipherX
• 阅读 6133

__get、关联、延迟加载,听起来好悬,其实很简单,看下文。

  • YII的魔术方法 __get

  • 什么是关联 & 延迟加载

昨日拿出大把时间对yii2的get魔术方法以及关联属性进行了一番研究,先分享给大家,我想这也是很多人,尤其初学者比较蒙的一个地方。

我们从一个例子入手,在这里我们需要三张表来说明。

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

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

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

其中 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>

结果图

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

数据关联 & 延迟加载

通过ar的魔术方法规则,我们使用 getXXX 完成了代码的优化,现在可以像访问对象自身属性一样访问关联的模型数据了。

但是,是的,还有但是。

我打开了神器小强yii2-debug ,看了下数据库,我勒个去,这么多次查询。

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

小明,你是在玩我么?回去改!

等待是漫长的,过了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']);
    }
    ...    
}

我很高兴小明的这次改动又对了,不知道你看懂没?我来给你说下,先看看结果

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

好棒,数据库查询从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魔术方法,秘密就在这里,我们一起探索下。

Yii2 | 论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

(完)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java多线程编程之单例模式
延迟加载:“懒汉模式”延迟加载是指在调用getInstance()方法时创建实例。常见的方法是在getInstance()方法中实例化new。实现代码如下:!(https://oscimg.oschina.net/oscnet/0b194956e9fd68db32050dd6439225bb86a.png)但是因为ge
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
AlgoCipherX
AlgoCipherX
Lv1
相恨不如潮有信,相思始觉海非深。
文章
5
粉丝
0
获赞
0