Yii2 Activerecord 在重定向之前未保存并显示在 "view" 中

Yii2 Activerecord not saved before redirect and shown in "view"

Yii2 框架。当我在另一个 ActiveRecord 的 AFTER_INSERT_EVENT 中保存多个 ActiveRecords 时,数据库中的值更新速度不够快,因此在重定向查看数据时显示旧值。

更具体地说:标准 XAMPP 环境 PHP 7.2.9。我做了一个特征,可以很容易地在模型中添加具有历史记录的额外属性(现有属性或新属性)。该特征用于 ActiveRecord。

注意函数TL_save中的sleep(5)。这解决了问题,但这不是正确的解决方案。 如何确保在再次读取之前更新所有内容? 我想避免在行上使用锁,因为这需要先更改 table 才能使用。有办法解决吗?交易 - 我试过了,但可能不正确,因为它没有效果。重新加载视图页面也可以解决问题,但同样:不是很优雅:-)

另外:我应该在 GitHub 上分享这个代码吗?我以前没有这样做过,也不太确定它是否真的对其他人有任何价值。

trait TimelineTrait
{   
    private $timelineConfig;

    public function timelineInit($config)
    {
        $std = [
            'attributes' => [],  // required
            '_oldAttributes'=>[],
            'datetime'=> date('Y-m-d H:i:s'),
            'validationRule'=>'safe',
            'table'=>$this->tableName(),
            'onlyDirty'=>true, // using !=, not !==
            'events'=>[
                self::EVENT_AFTER_INSERT=>[$this, 'TL_EventAfterInsert'],
                self::EVENT_AFTER_UPDATE=>[$this, 'TL_EventAfterUpdate'],
                self::EVENT_AFTER_FIND=>[$this, 'TL_EventAfterFind'],
                self::EVENT_AFTER_DELETE=>[$this, 'TL_EventAfterDelete'],
            ],
            'TimelineClass'=>Timeline::class,
            /*
                Must have the following attributes
                id integer primary key auto increment not null,
                table varchar(64) not null,
                table_id integer not null,
                attribute varchar(64) not null,
                datetime datetime not null
                value text (can be null)
            */
        ];
        $this->timelineConfig = array_replace_recursive($std, $config);
        foreach($this->timelineConfig["events"]??[] as $trigger=>$handler)
            $this->on($trigger, $handler);
    }

    public function __get($attr)
    {        
        $cfg = &$this->timelineConfig;
        if (in_array($attr, array_keys($cfg["attributes"])))
            return $cfg["attributes"][$attr];
        else
            return parent::__get($attr);
    }

    public function __set($attr, $val)
    {        
        $cfg = &$this->timelineConfig;        
        if (in_array($attr, array_keys($cfg["attributes"]))) {
            $cfg["attributes"][$attr] = $val;            
        } else
            parent::__set($attr, $val);
    }

    public function attributes()
    {
        return array_merge(parent::attributes(), $this->timelineConfig["attributes"]);
    }

    public function rules()
    {
        $temp  = parent::rules();
        $temp[] = [array_keys($this->timelineConfig["attributes"]), $this->timelineConfig["validationRule"]];
        return $temp;
    }

    public function TL_EventAfterInsert($event) 
    {
        $this->TL_save($event, true);
    }

    public  function TL_EventAfterUpdate($event)
    {
        $this->TL_save($event, false);
    }

    private function TL_save($event, $insert) 
    {
        $cfg = &$this->timelineConfig;    
        if ($cfg["onlyDirty"]) 
            $cfg["_oldAttributes"] = $this->TL_attributesOnTime();        
       
        foreach($cfg["attributes"] as $attr=>$val) {
            $a = [
                'table'=>$cfg["table"], 
                'table_id'=>$this->id,
                'attribute'=>$attr,
                'datetime'=>$cfg["datetime"],
            ];

            if ($insert)
                $model=null;
            else 
                $model = Timeline::find()->where($a)->one();

            $isNew = empty($model); // this exact attribute does not exist on timeline already

            if ($isNew) 
                $model = new $cfg["TimelineClass"]($a);

            $model->value = $val;
           
            if (!$cfg["onlyDirty"] 
                || $cfg["onlyDirty"] && $model->value!=($cfg["_oldAttributes"][$attr]??\uniqid('force_true'))) {
                
                $ok = $model->save();
                if (!$ok) $this->addErrors($attr, $model->getErrorSummary());
            } 
        }
        sleep(5);        
    }

    public function TL_EventAfterFind($event)
    {
        $cfg = &$this->timelineConfig;
        $data = $this->TL_attributesOnTime();
        foreach($data as $attr=>$val)
            $cfg["attributes"][$attr] = $val;

        $cfg["_oldAttributes"] = $cfg["attributes"];
    }

    private function TL_attributesOnTime()
    {
        $cfg = &$this->timelineConfig;
        $timelineTable = $cfg["TimelineClass"]::tableName();        
        
        $sql = "SELECT t1.* FROM $timelineTable AS t1
                LEFT JOIN (SELECT * FROM $timelineTable WHERE `table`=:table AND table_id=:table_id AND datetime<=:datetime) AS t2
                ON (t1.table=t2.table and t1.table_id=t2.table_id and t1.datetime<t2.datetime AND t1.attribute=t2.attribute)
                WHERE t2.id IS NULL AND t1.datetime<:datetime AND t1.table=:table AND t1.table_id=:table_id
                ";
        $params = [
            'table'=>$cfg["table"],
            'table_id'=>$this->id,
            ':datetime'=>$cfg["datetime"],
        ];        
        $data = \Yii::$app->db->createCommand($sql,$params)->queryAll();
        $data = ArrayHelper::map($data,'attribute','value');        
        return $data;
    }

    public function TL_EventAFterDelete($event) 
    {
        $cfg = &$this->timelineConfig;
        $cfg["TimelineClass"]::deleteAll([
            'table'=>$cfg["table"],
            'table_id'=>$event->sender->id
        ]);
    }

}

使用示例:

<?php
namespace app\models;

class KeyTime extends Key
{
    use \app\behaviors\TimelineTrait;

    public function init()
    {
        parent::init();
        $this->timelineInit([
            'attributes'=>[
                // default values for attributes
                'keyid'=>'historic id', // this is existing attribute in Key model
                'label'=>'mylabel', // label and color does not exist in Key model
                'color'=>'red',
            ],   
        ]);
    }
}

动作更新

   public function actionUpdate($id)
    {
        $model = $this->findModel($id);

        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        }

        return $this->render('update', [
            'model' => $model,
        ]);
    }

在打开 microtime(true) 多次“闪烁”后,我找到了它有时与 sleep(1) 一起工作的原因。

答案在TL_attributesOnTime中。 $sql 中的最后一行是

WHERE t2.id IS NULL AND t1.datetime<:datetime AND t1.table=:table AND t1.table_id=:table_id

……但应该是……

WHERE t2.id IS NULL AND t1.datetime<=:datetime AND t1.table=:table AND t1.table_id=:table_id

注意 < 更改为 <= 否则,当记录在填充的同一秒内保存时,它不会被包括在内。 希望它能帮助到别人。