說點閑話
距離上次寫博客,已經(jīng)有一年了。在動手寫之前,總是帶著深深的罪惡感。被它折磨許久,終于,還是,動手了。
值得慶祝的一件事:最近開始健身了。每天動感單車45分鐘,游泳45分鐘,真的是(生)爽(不)到(如)爆(死)。
好了,扯淡完畢,步入正題。
ActiveRecord被莫名寫入?
準備知識
ActiveRecord的基本用法。如果不理解,可參考這里。
代碼現(xiàn)場
/**
* @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(); //保存,會發(fā)現(xiàn)此行的其它字段都被寫成默認值了。
總結(jié)問題
這個例子的問題在于:
- 我從數(shù)據(jù)庫中取出了一行,也就是代碼中的$room,但是只取出了id字段,而其他字段自然就是默認值。
- 當(dāng)我$room->save()的時候,那些是默認值的字段也被保存到數(shù)據(jù)庫里去了。what!?
- 也就是說,當(dāng)你想節(jié)約資源,不取出所有字段的時候,一定要注意不能保存,否則,很多數(shù)據(jù)會被莫名修改為默認值。
解決方法
然而,我們有什么解決辦法呢?提供幾種思路:
- 自己時刻注意,避免未完全取出的ActiveRecord的保存。
- 修改或繼承ActiveRecord, 使得,當(dāng)此對象由find()新建,且字段沒有完全取出,調(diào)用save()方法,拋出異常。
- 修改或繼承ActiveRecord,使得,當(dāng)此對象由find()新建,且字段沒有完全取出,調(diào)用save()方法時,只保存取出過的字段,其他字段被忽略。
你的Transaction生效了嗎?
代碼現(xiàn)場
/**
* @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()階段已經(jīng)返回false,不會進行數(shù)據(jù)庫存儲的步驟,所以也不會拋出異常。
$b->save(); //name字段合法,可以正常保存。
$transaction->commit(); //提交后,發(fā)現(xiàn)$a保存失敗,而$b保存成功。
}
catch (Exception $e)
{
Yii::error($e->getTraceAsString(),__METHOD__);
$transaction->rollBack();
}
問題總結(jié)
這段代碼的問題在于:
- 大家知道$transaction的存在意義是保證整段數(shù)據(jù)庫存儲代碼要么全成功,要么全失敗。
- 顯然,在這個例子中,transaction并沒有達到我們想要的效果:$a因為validate()都沒過,所以$transation->commit()的時候并不會報錯。
解決方法
在$transation塊內(nèi),所有的save()都要判斷下返回值,如果為false,則直接拋出異常。
'Y-m-d'不被識別?
代碼現(xiàn)場
OcRenterBill extends ActiveRecord
{
public function rules()
{
return [
['start_time','date','format'=>'Y-m-d'],
];
}
}
$a = new OcRenterBill();
$a = '2015-09-12';
$a->save(); //會報錯,說格式不對
問題總結(jié)
如果一開始,Yii框架就報錯,這個還不算坑??拥氖俏以贛ac上開發(fā)時,這個可以完全正常的工作,而發(fā)布到線上環(huán)境(Ubuntu)后,就彈出“屬性start_time格式無效”的錯誤。而參考官方文檔,發(fā)現(xiàn)這種格式是允許的官方文檔。
啊啊啊。各種試錯,最后發(fā)現(xiàn)如果改成php:Y-m-d,世界就清凈了。所以,如果你遇到這種問題,感激我吧。
內(nèi)存泄露
代碼現(xiàn)場
public static function actionTest() {
$total = 10;
var_dump('開始內(nèi)存'.memory_get_usage());
while($total){
$ret=User::findOne(['id'=>910002]);
var_dump('end內(nèi)存'.memory_get_usage());
unset($ret);
$total--;
}
}
上面代碼的內(nèi)存一直在增長, 按照原本想法來看, 變量被釋放了,內(nèi)存就算增長也不會一直增長。因為每循環(huán)一次內(nèi)存都會被釋放。
分析問題 上面這段代碼涉及到了數(shù)據(jù)庫的操作,而我們知道,數(shù)據(jù)庫的很多地方都能引起內(nèi)存泄漏。 所以先屏蔽數(shù)據(jù)庫相關(guān)操作, 我手寫了一個原生的數(shù)據(jù)庫查詢操作, 發(fā)現(xiàn)內(nèi)存正常,沒有問題。
$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('開始內(nèi)存'.memory_get_usage());
while($total){
$ret= new User();
var_dump('end內(nèi)存'.memory_get_usage());
unset($ret);
$total--;
}
}
內(nèi)存還是一直增長。 這時候我測試了一個其他的yii2類 發(fā)覺內(nèi)存不增長了。 這就可以聯(lián)想到是在new 對象的時候yii2內(nèi)部自己執(zhí)行了什么操作,然后導(dǎo)致內(nèi)存泄漏。 什么方法是new 的時候就執(zhí)行的呢。。。 對的 構(gòu)造方法 __construct 。 然后 我一步一步的從model 查到object 發(fā)覺都沒有能引起泄漏的地方。
這個時候我們不妨換個思路, 既然是yii2框架下出現(xiàn)的泄漏, 那肯定就是yii2獨有的功能, 那什么功能是yii2獨有的,又是在new 對象的時候就會執(zhí)行的呢?
行為(Behavior) 發(fā)覺我的模型類里面果然有用了行為
public function behaviors()
{
return [
TimestampBehavior::class,
];
}
最普通不過的代碼。 我們知道 行為最后調(diào)用的地方是 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;
}
我們觀察這段代碼,發(fā)覺他把自己傳進去了$behavior->attach($this); 最后調(diào)用的是 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);
}
}
問題總結(jié)
這個時候答案已經(jīng)呼之欲出, Yii2為了實現(xiàn)行為這一功能, 把自身this傳進去,以便能注冊事件、觸發(fā)事件、解除事件。 這就導(dǎo)致了一個循環(huán)引用的問題。 所以導(dǎo)致對象refcount一直不為0 一直回收不了。
接下來就好辦了。將查詢換成原始的連接試試。果然,內(nèi)存上升的非常慢了,可以說這才是正常現(xiàn)象?,F(xiàn)在的內(nèi)存也就是50m左右,cpu也穩(wěn)定在7%左右。
代碼優(yōu)化后,再跑腳本,1分鐘左右吧,腳本就跑完了。重點是不會再報出內(nèi)存錯誤了。所以,以后考慮問題還是要深入。敢于質(zhì)疑。以后如果遇到這種內(nèi)存錯誤,一定要先檢查自己的代碼是不是有內(nèi)存泄漏的地方。不要想著先設(shè)置php的內(nèi)存。這樣只會治標(biāo)不治本。
總結(jié)
1、從開發(fā)速度方面,借助于gii腳手架,可以快速生成代碼,也就是說搭建一個可以增刪改查的系統(tǒng)可能一行代碼都不用寫,而且集成了jquery和bootstrap,特效和樣式基本也不需要寫了,這對于設(shè)計和審美能力普遍較差的后端程序員來說簡直是一大福利。不過在前后端完全的分離的趨勢下,Yii2前后端的耦合的還是有些重了。
2、從代碼的可讀性方面,Yii不會為了刻板地遵照某種設(shè)計模式而對代碼進行過度的設(shè)計?;旧项愒贗DE里不借助第三方組件是可以跳轉(zhuǎn)閱讀源碼的。這點上Yii要比Laravel略勝一籌。
3、從開源生態(tài)圈方面,Yii因為人少,稍微偏門一點的資料就很少,需要強大的谷歌能力和閱讀英文文檔的能力。
不可否認,Yii是一個優(yōu)秀的開發(fā)框架,值得PHP開發(fā)者上手學(xué)習(xí),踩坑的過程也是一種成長與積累。最后祝愿PHP小伙伴們都健健康康,事業(yè)有成。
您可能感興趣的文章:- 從零開始學(xué)YII2框架(一)通過Composer安裝Yii2框架
- 從零開始學(xué)YII2框架(五)快速生成代碼工具 Gii 的使用
- 從零開始學(xué)YII2框架(六)高級應(yīng)用程序模板
- 從零開始學(xué)YII2框架(三)擴展插件yii2-gird
- 從零開始學(xué)YII2框架(二)通過 Composer 安裝擴展插件
- 從零開始學(xué)YII2框架(四)擴展插件yii2-kartikgii