說點閑話
距離上次寫博客,已經有一年了,在動手寫之前,總是帶著深深的罪惡感,被它折磨許久,終于,還是,動手了,
值得慶祝的一件事:最近開始健身了,每天動感單車45分鐘,游泳45分鐘,真的是(生)爽(不)到(如)爆(死),
好了,扯淡完畢,步入正題,
ActiveRecord被莫名寫入?
準備知識
ActiveRecord的基本用法,如果不理解,可參考這里,
代碼現場
/** * @property integer $id * @property string $name * @property string $detail * @property double $price * @property integer $area **/ class OcRoom extends ActivieRecord { ... } $room = OcRoom::find() //先取出一個物件, ->select(['id']) //只取出'id'列 ->where(['id'=>20]) ->one(); $room->save(); //保存,會發現此行的其它欄位都被寫成默認值了,
總結問題
這個例子的問題在于:
- 我從資料庫中取出了一行,也就是代碼中的
$room,但是只取出了id欄位,而其他欄位自然就是默認值, - 當我
$room->save()的時候,那些是默認值的欄位也被保存到資料庫里去了,what!? - 也就是說,當你想節約資源,不取出所有欄位的時候,一定要注意不能保存,否則,很多資料會被莫名修改為默認值,
解決方法
然而,我們有什么解決辦法呢?提供幾種思路:
- 自己時刻注意,避免未完全取出的
ActiveRecord的保存, - 修改或繼承
ActiveRecord, 使得,當此物件由find()新建,且欄位沒有完全取出,呼叫save()方法,拋出例外, - 修改或繼承
ActiveRecord,使得,當此物件由find()新建,且欄位沒有完全取出,呼叫save()方法時,只保存取出過的欄位,其他欄位被忽略,
你的Transaction生效了嗎?
代碼現場
/** * @property integer $id * @property string $name **/ class OcRoom extends ActiveRecord { public function rules() { return [['name','string','min'=>2,'max'=>10]]; } ... } class OcHouse extends ActiveRecord { public function rules() { return [['name','string','max'=>10]]; } ... } $a = new OcRoom(); $a->name = ''; //name為空字串,不滿足rules()條件, $b = new OcHouse(); $b->name = '我的房間'; //name合法,可以保存, $transaction = Yii::$app->db->beginTransaction(); try{ $a->save(); //name欄位不合法,無法驗證通過,在validate()階段已經回傳false,不會進行資料庫存盤的步驟,所以也不會拋出例外, $b->save(); //name欄位合法,可以正常保存, $transaction->commit(); //提交后,發現$a保存失敗,而$b保存成功, } catch (Exception $e) { Yii::error($e->getTraceAsString(),__METHOD__); $transaction->rollBack(); }
問題總結
這段代碼的問題在于:
- 大家知道
$transaction的存在意義是保證整段資料庫存盤代碼要么全成功,要么全失敗, - 顯然,在這個例子中,
transaction并沒有達到我們想要的效果:$a因為validate()都沒過,所以$transation->commit()的時候并不會報錯,
解決方法
在$transation塊內,所有的save()都要判斷下回傳值,如果為false,則直接拋出例外,
'Y-m-d'不被識別?
代碼現場
OcRenterBill extends ActiveRecord { public function rules() { return [ ['start_time','date','format'=>'Y-m-d'], ]; } } $a = new OcRenterBill(); $a = '2015-09-12'; $a->save(); //會報錯,說格式不對
問題總結
如果一開始,Yii框架就報錯,這個還不算坑,坑的是我在Mac上開發時,這個可以完全正常的作業,而發布到線上環境(Ubuntu)后,就彈出“屬性start_time格式無效”的錯誤,而參考官方檔案,發現這種格式是允許的官方檔案,
啊啊啊,各種試錯,最后發現如果改成php:Y-m-d,世界就清凈了,所以,如果你遇到這種問題,感激我吧,
記憶體泄露
代碼現場
public static function actionTest() { $total = 10; var_dump('開始記憶體'.memory_get_usage()); while($total){ $ret=User::findOne(['id'=>910002]); var_dump('end記憶體'.memory_get_usage()); unset($ret); $total--; } }
上面代碼的記憶體一直在增長, 按照原本想法來看, 變數被釋放了,記憶體就算增長也不會一直增長,因為每回圈一次記憶體都會被釋放,
分析問題 上面這段代碼涉及到了資料庫的操作,而我們知道,資料庫的很多地方都能引起記憶體泄漏, 所以先屏蔽資料庫相關操作, 我手寫了一個原生的資料庫查詢操作, 發現記憶體正常,沒有問題,
$dsn = "mysql:dbname=test;host=localhost"; $db_user = 'root'; $db_pass = 'admin'; //查詢 $sql = "select * from buyer"; $res = $pdo->query($sql); foreach($res as $row) { echo $row['username'].'<br/>'; }
這時候答案呼之欲出--- 是yii2框架搞了鬼
定位問題 既然知道了是yii2 框架的問題那就可以進一步縮小問題,
public static function actionTest() { $total = 10; var_dump('開始記憶體'.memory_get_usage()); while($total){ $ret= new User(); var_dump('end記憶體'.memory_get_usage()); unset($ret); $total--; } }
記憶體還是一直增長, 這時候我測驗了一個其他的yii2類 發覺記憶體不增長了, 這就可以聯想到是在new 物件的時候yii2內部自己執行了什么操作,然后導致記憶體泄漏, 什么方法是new 的時候就執行的呢,,, 對的 構造方法 __construct , 然后 我一步一步的從model 查到object 發覺都沒有能引起泄漏的地方,
這個時候我們不妨換個思路, 既然是yii2框架下出現的泄漏, 那肯定就是yii2獨有的功能, 那什么功能是yii2獨有的,又是在new 物件的時候就會執行的呢?
行為(Behavior) 發覺我的模型類里面果然有用了行為
public function behaviors() { return [ TimestampBehavior::class, ]; }
最普通不過的代碼, 我們知道 行為最后呼叫的地方是 yii\base\Component->attachBehaviors 最后定位到
private function attachBehaviorInternal($name, $behavior) { if (!($behavior instanceof Behavior)) { $behavior = Yii::createObject($behavior); } if (is_int($name)) { $behavior->attach($this); $this->_behaviors[] = $behavior; } else { if (isset($this->_behaviors[$name])) { $this->_behaviors[$name]->detach(); } $behavior->attach($this); $this->_behaviors[$name] = $behavior; } return $behavior; }
我們觀察這段代碼,發覺他把自己傳進去了$behavior->attach($this); 最后呼叫的是 yii\base\Behavior->attach
public function attach($owner) { $this->owner = $owner; foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } }
問題總結
這個時候答案已經呼之欲出, Yii2為了實作行為這一功能, 把自身this傳進去,以便能注冊事件、觸發事件、解除事件, 這就導致了一個回圈參考的問題, 所以導致物件refcount一直不為0 一直回收不了,
接下來就好辦了,將查詢換成原始的連接試試,果然,記憶體上升的非常慢了,可以說這才是正常現象,現在的記憶體也就是50m左右,cpu也穩定在7%左右,
代碼優化后,再跑腳本,1分鐘左右吧,腳本就跑完了,重點是不會再報出記憶體錯誤了,所以,以后考慮問題還是要深入,敢于質疑,以后如果遇到這種記憶體錯誤,一定要先檢查自己的代碼是不是有記憶體泄漏的地方,不要想著先設定php的記憶體,這樣只會治標不治本,
總結
1、從開發速度方面,借助于gii腳手架,可以快速生成代碼,也就是說搭建一個可以增刪改查的系統可能一行代碼都不用寫,而且集成了jquery和bootstrap,特效和樣式基本也不需要寫了,這對于設計和審美能力普遍較差的后端程式員來說簡直是一大福利,不過在前后端完全的分離的趨勢下,Yii2前后端的耦合的還是有些重了,
2、從代碼的可讀性方面,Yii不會為了刻板地遵照某種設計模式而對代碼進行過度的設計,基本上類在IDE里不借助第三方組件是可以跳轉閱讀原始碼的,這點上Yii要比Laravel略勝一籌,
3、從開源生態圈方面,Yii因為人少,稍微偏門一點的資料就很少,需要強大的谷歌能力和閱讀英文檔案的能力,
不可否認,Yii是一個優秀的開發框架,值得PHP開發者上手學習,踩坑的程序也是一種成長與積累,最后祝愿PHP小伙伴們都健健康康,事業有成,
END
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/96366.html
標籤:PHP
上一篇:針對windows系統如何解決openssl_pkey_export(): cannot get key from parameter 1這個問題
下一篇:如何搭建屬于自己的Web服務器
