被忽略的魔法——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);}