被忽略的魔法——php引用之延迟赋值(后期数据延迟绑定)

看到这个主题大家知道我今天要说的是php的变量引用特性,但是延迟赋值又是怎么回事呢?这个主要是我近期优化一些功能时的一个想法,我觉得还算不错,就打算记录下来。看一下下面的伪代码:

// 这段代码有人会说为啥不用联表,因为有些业务需求不用联表的效率是联表的3到20倍
// 我的项目里基本都是此类写法,比之前联表效率提升很多
$a = DB::query("select id from a");
$aid = "";
foreach($a as $v){
	$aid .= $v['id'].',';
}
$aid = substr($aid, 0 , -1);
if($aid){
	$b = DB::query("select * from b where aid in ({$aid})");

	// 此处省略
}

之所以用这段代码举例,因为类似这样的代码很多,大家比较容易理解,带不一定适合用后期延迟赋值,因为这样更容理解,且效率差不错,不过可以和后期延迟赋值形成鲜明对比,让大家更容理解下面的要将的方式。

看完上面的例子我们再看一个复杂的需求,要求数据是获取一人的最近10篇文章列表,且读取每篇文章5条评论,并包含文章发起者和评论人的id,姓名,把是数据打包成指定格式的json返回给客户端,看下面的代码:

// 这种需求用联表获取用户信息远没有搜集用户id做in查询效率高
$data = array();
$article = DB::query("select id,uid,title,content from article where uid={$_GET['uid']} order by id desc limit 10");
foreach($article as $v){
	$uid = $v['uid'];

	$comment = DB::query("select id,uid,content from comment where aid={$v['id']} order by id asc limit 5");

	foreach($comment as $value){
		$uid .= ','.$value['uid'];
	}

	// 这里第二个参数我们要求DB类返回的数组以uid为索引
	$member = DB::query("select uid,username from user where uid in({$uid})", 'uid');

	$commentList = array();
	$data[] = array(
		'id' => $v['id'],
		'title' => $v['title'],
		'content' => $v['content'],
		'uid' => $v['uid'],
		'username' => $member[$v['uid']]['username'],
		'comment' => &$commentList
	);

	foreach($comment as $value){
		$commentList[] = array(
			'id' => $value['id'],
			'content' => $value['content'],
			'uid' => $value['uid'],
			'username' => $member[$value['uid']]['username']
		)
	}
}

echo json_encode($data);
exit;

细心看这段代码就会发现其中$data[][‘comment‘]的值最开始就引用了变量$commentList,之后在后面更改了$commentList的值,同时导致$data[][‘comment‘]值跟着一起发生了改变,这里也是后期延迟赋值,但是比较简单,可以据此了解一下这个实现原理。

我相信大多数人都写过类似的代码,也很少有人觉得这段代码会有问题。我来分析一下这块的逻辑,评论信息因为要每篇文章获取5条,这个没法用简单的sql合并成一条,由于需求只是获取10篇文章,写复杂的sql处理反而不如循环查询的效率(如果你内网延迟较低的情况下),为啥说复杂sql的处理效率慢,如果你考虑的是几千条数据都一样没啥需要注意,但是如果处理的是千万级别的数据量,复杂sql很多情况下不如简单的sql效率高,那么这里就采用循环查询没法继续优化。但是我们看用户信息也在循环里进行查询,这个很不好了,我的项目组里很不希望见到这样的代码的,10篇帖子都是一个人发起的,近50条评论会有很多活跃用户的数据,这样每次循环查询其实查询到重复用户信息的概率非常高,在一个业务逻辑里最好不要从数据库获取重复信息而是复用。如何能让让用户信息达到复用呢?可以在组装最终数据前循环获取文章的评论信息,之后收集用户id,然后再获取用户信息,之后组装最终的数据。这是一种比较简单的解决方案,不是我们今天的重点,下面看一种优雅的处理方式——延迟赋值。

// 这种需求用联表获取用户信息远没有搜集用户id做in查询效率高
$data = array();
$article = DB::query("select id,uid,title,content from article where uid={$_GET['uid']} order by id desc limit 10");
$member = array();
foreach($article as $v){
	$comment = DB::query("select id,uid,content from comment where aid={$v['id']} order by id asc limit 5");
	$commentList = array();
	$data[] = array(
		'id' => $v['id'],
		'title' => $v['title'],
		'content' => $v['content'],
		'uid' => $v['uid'],
		'username' => &$member[$v['uid']]['username'],
		'comment' => &$commentList
	);

	foreach($comment as $value){
		$commentList[] = array(
			'id' => $value['id'],
			'content' => $value['content'],
			'uid' => $value['uid'],
			'username' => &$member[$value['uid']]['username']
		)
	}
}

$uid = array_keys($member);
if($uid){
	$uid = implode(',', $uid);
	$user = DB::query("select uid,username from user where uid in({$uid})", 'uid');
	foreach($member as $uid => $value){
		$member[$uid]['username'] = $user[$uid]['username'];
	}
	unset($member,$user);
}
echo json_encode($data);
exit;

这段代码和之前不太一样了,最明显的就是最下面多了一段代码,而且暂时还不知道究竟是干嘛,好像没啥用,我们一步步的看看,首先在循环文章数据前初始化了一个变量$member = array();之后在循环里少了$uid的赋值,以及循环收集评论人的id,并且查询用户数据的sql也不见了,好像到了最下面那段看不懂的代码地方。仔细找了找还发现&$member[$v[‘uid‘]][‘username‘]和&$member[$value[‘uid‘]][‘username‘]地方多了&引用符号,这就是为啥循环里少了写代码的奥秘。回想一下之前发现$commentList被引用之后在后面进行赋值的,并且改变了$data[][‘comment‘]。这的道理是一样的,先不查询用户信息,只进行一个空的引用,在引用一个不存在的变量时php会先创建这个变量,例如&$member[$v[‘uid‘]][‘username‘],php检测$member是一个数组已经声明,但是$member[$v[‘uid‘]][‘username‘]不存在就在内存创建并且值为null。

当循环完文章数据后打印会发现username的信息都是null,当然之前并没用用户信息,php在引用赋值的时候帮我们给了一个null值。之后通过$uid = array_keys($member);获取所有用户的id信息,

为什么array_keys能获取用户id,因为php在引用的时候帮我们创建了$member数组呀,注意一下这里的uid是不重复的哟,之后我们去user表用in检索用户信息,一定注意这里不能把返回的数据赋值给$member因为之前的数据都是引用$member里的数据,如果这里覆盖了$member,内存里两个变量的地址就不一样了,相当于重新创建了一个数组,我们这里赋值给$user,下面的循环是干什么的,当然是修改之前被引用数据的赋值了,我们循环$member变量把$user[$uid][‘username‘]赋值给$member[$uid][‘username‘],从而改变引用变量的值。在我们把数据绑定到引用变量后千万不要忽略用uset把$member删除了,主要是防止之后的代码里出现操作$member变量的代码,不小心就会把之前绑定好的数据覆盖掉。为啥删除$member之后绑定的数据没有丢失,主要是引用的特性,当多个变量引用一个内存地址时,删除其中一个变量不影响其它变量,除非把所有变量都删除,才会真的删除内存里的数据。php手册是这么解释的“当
unset 一个引用,只是断开了变量名和变量内容之间的绑定。这并不意味着变量内容被销毁了”。unset($user);只是因为$user是一个临时变量,使用完可以直接从内存释放了。

关于这种编程方式我起名为,之所以标题是延迟变量赋值主要是让大家便于理解。Php中引用的作用非常广泛,本文所举的例子也只局部的一种使用方法,用来解决编程中遇到类似业务需求的一种处理方式,当然后期数据延迟绑定的编程方法也有很广的使用,希望大家不要局限在本文例子的场景上。针对本文例子是我们编程中最常用的一种问题,我编写了一个函数用来处理数据延迟绑定,减少每个地方都要编写数据绑定的逻辑。

/**
 * 数据延迟绑定通用方法
 *
 * @access public
 * @param array $bindingVar		待绑定的变量
 * @param array	$data		待绑定的数据
 * @param mixed	$default	默认值
 * @return void
 */
function bindingData(&$bindingVar, $data, $default=''){
	foreach($bindingVar as $key => $tmp){
		foreach($tmp as $k => $v){
			$bindingVar[$key][$k] = isset($data[$key][$k]) ? $data[$key][$k] : $default;
		}
	}

	unset($bindingVar);
}

采用这个函数我们能把之前处理数据绑定的代码部分改成下面这样:

$uid = array_keys($member);
if($uid){
	$user = DB::query("select uid,username from user where uid in({$uid})", 'uid');
	bindingData($member, $user);
	unset($member,$user);
}
时间: 2024-08-25 02:31:52

被忽略的魔法——php引用之延迟赋值(后期数据延迟绑定)的相关文章

引用对象深度赋值

var cloneObj = function(obj){ var str, newobj = obj.constructor === Array ? [] : {}; if(typeof obj !== 'object'){ return; } else if(window.JSON){ str = JSON.stringify(obj), //系列化对象 newobj = JSON.parse(str); //还原 } else { for(var i in obj){ newobj[i]

《Java开发手册》学习进程之对象引用与接口引用间的赋值和强制类型转换问题

对象引用之间: 子类引用可以赋值给父类引用. 父类引用需要在强制转换之后才能赋值给子类引用. 对于对象引用的强制转换,只要被转换的引用类型与转换后的目标类型之间是派生或被派生的关系,就可以通过编译.如果没有这些关系而去强制转换,则编译报错. 即使编译通过,如果被转换的引用指向的对象类型与转换后的目标类型之间不相符或不兼容(即被转换的引用指向的对象类型不能转换为除自身或者自身父类的其他类型,同下),则运行出错. 接口引用之间: 子接口引用可以赋值给父接口引用. 父接口引用需要在强制类型转换之后才能

实时检查MySQL数据库延迟状况复制中断数据延迟

脚本编写思路: (1)根据show slave statusG;l列Seconds_Behind_Master: (2)Seconds_Behind_Master= NULL判断io或sql进程哪个停止或者二者都停止 (3)Seconds_Behind_Master= 0复制正常 (4)Seconds_Behind_Master>0 说明主从延迟 上述(2).(4)会发出邮件报警,正常时忽略,脚本10s采集数据一次. #!/bin/bash ###########################

C++11:右值引用和转移赋值

1.首先认识左值和右值的定义: 左值:表达式可以引用到一个对象,并且这个对象是一块内存空间并可以检测和存储,这个表示即是左值. 右值:直接引用了一个存储在内存地址中的数据. 右值最大限度只能被一个常量引用: const int &a = 1; 规则:临时变量是右值,且可以改变: T().set().get() T为临时变量,set()设置新值,get()获取更改后的值. 2.首先写一个MyString类: #include<vector> class MyString { privat

iOS 设置 延迟执行 与 取消延迟执行 方法 以及对 run loop 初步认识

之前开发过程中经常会有需求会使用 NSObject中的"performSelector:withObject:afterDelay:"做方法延迟执行的处理, 但是 还没有什么地方需要实现 取消 这个延迟执行方法"cancelPreviousPerformRequestsWithTarget:".(具体可参见系统库文件 NSOject里面两个方法的声明). 但是 我们应该知道在什么条件下,合理使用 延迟 与 取消延迟. 延迟 和 取消延迟 应该 在同一个 事件处理循环

VS+SqlServe 在引用了webservice后刷新数据时提示:已超过传入消息(65536)的最大消息大小配额若要增加配额请使用相应绑定元素上 MaxReceivedMessageSize 属性

使用了VS建立了webservice 后,在VS项目中添加了引用,可是在今天从sqlserve中取数据时, 突然提示:已超过传入消息(65536)的最大消息大小配额若要增加配额请使用相应绑定元素上 MaxReceivedMessageSize 属性 这让我那叫一个郁闷啊,之前都是一直好用,为什么突然就不好用了????????? 后来根据提示分析和上网查资料分析,猜测可能是数据量问题,再去找如何绑定元素上 MaxReceivedMessageSize属性,经过了几个小时的处理终于找到解决方案了:

TCP Nagle算法以及延迟确认(即延迟回复ACK)的学习

TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认.为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据. (一个连TCP接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据). Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块.(尤其在广域网中)(减少大量小包的发送) Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段.所谓"小段",指的是小于M

MySQL延迟问题和数据刷盘策略

一.MySQL复制流程官方文档流程图如下: 1.绝对的延时,相对的同步 2.纯写操作,线上标准配置下,从库压力大于主库,最起码从库有relaylog的写入. 二.MySQL延迟问题分析 1.主库DML请求频繁 原因:主库并发写入数据,而从库为单线程应用日志,很容易造成relaylog堆积,产生延迟. 解决思路:做sharding,打散写请求.考虑升级到MySQL 5.7+,开启基于逻辑时钟的并行复制. 2.主库执行大事务 原因:类似主库花费很长时间更新了一张大表,在主从库配置相近的情况下,从库也

为什么类的拷贝构造参数加引用、重载赋值函数的返回值和参数加引用

class string { public: string(const char *str=NULL); string(const string& str);     //copy构造函数的参数为什么是引用呢? string& operator=(const string & str); //赋值函数为什么返回值是引用呢?参数为什么是引用呢? ~string(); }; 下面我就给大家解释一下: class String1 { public: String1(const char*