Yii2框架那些折磨人的坑

說點閒話

距離上次寫博客,已經有一年了。在動手寫以前,老是帶着深深的罪惡感。被它折磨許久,終於,仍是,動手了。php

值得慶祝的一件事:最近開始健身了。天天動感單車45分鐘,游泳45分鐘,真的是(生)爽(不)到(如)爆(死)。html

好了,扯淡完畢,步入正題。mysql


 

ActiveRecord被莫名寫入?

準備知識

  1. 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();              //保存,會發現此行的其它字段都被寫成默認值了。

總結問題

這個例子的問題在於:jquery

  1. 我從數據庫中取出了一行,也就是代碼中的$room,可是隻取出了id字段,而其餘字段天然就是默認值。
  2. 當我$room->save()的時候,那些是默認值的字段也被保存到數據庫裏去了。what!?
  3. 也就是說,當你想節約資源,不取出全部字段的時候,必定要注意不能保存,不然,不少數據會被莫名修改成默認值。

解決方法

然而,咱們有什麼解決辦法呢?提供幾種思路:程序員

  1. 本身時刻注意,避免未徹底取出的ActiveRecord的保存。
  2. 修改或繼承ActiveRecord, 使得,當此對象由find()新建,且字段沒有徹底取出,調用save()方法,拋出異常。
  3. 修改或繼承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();
}

問題總結

這段代碼的問題在於:sql

  1. 你們知道$transaction的存在乎義是保證整段數據庫存儲代碼要麼全成功,要麼全失敗。
  2. 顯然,在這個例子中,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格式無效」的錯誤。而參考官方文檔,發現這種格式是容許的官方文檔bootstrap

啊啊啊。各類試錯,最後發現若是改爲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的內存。這樣只會治標不治本。

總結

一、從開發速度方面,藉助於gii腳手架,能夠快速生成代碼,也就是說搭建一個能夠增刪改查的系統可能一行代碼都不用寫,並且集成了jquery和bootstrap,特效和樣式基本也不須要寫了,這對於設計和審美能力廣泛較差的後端程序員來講簡直是一大福利。不過在先後端徹底的分離的趨勢下,Yii2先後端的耦合的仍是有些重了。

二、從代碼的可讀性方面,Yii不會爲了刻板地遵守某種設計模式而對代碼進行過分的設計。基本上類在IDE裏不借助第三方組件是能夠跳轉閱讀源碼的。這點上Yii要比Laravel略勝一籌。

三、從開源生態圈方面,Yii由於人少,稍微偏門一點的資料就不多,須要強大的谷歌能力和閱讀英文文檔的能力。

不能否認,Yii是一個優秀的開發框架,值得PHP開發者上手學習,踩坑的過程也是一種成長與積累。最後祝願PHP小夥伴們都健健康康,事業有成。

 

END

相關文章
相關標籤/搜索