yii2 batch insert with ActiveRecord

2019-02-20 02:30发布

问题:

I want to insert multiple record in my table using yii2 ActiveRecord. I already know that I can use this code

$connection->createCommand()->batchInsert('user', ['name', 'age'], [
    ['Tom', 30],
    ['Jane', 20],
    ['Linda', 25],
])->execute();

but by this approach my model validations are not executing. and I already have read this question ActiveRecord batch insert (yii2)

but also by doing validation in a tricky way, consider I want to fill created_at and updated_at columns using ActiveRecords events.

just like this

public function beforeSave($insert)
{
    if (parent::beforeSave($insert)) {
        if($insert)
            $this->created_at = date('Y-m-d H:i:s');
        $this->updated_at = date('Y-m-d H:i:s');
        return true;
    } else {
        return false;
    }
}

回答1:

I think is not good idea to use beforeSave events (and similar stuff) because it will trigger for each model. However you want save multiple models at once. I recommend to use bulk methods.

In similar cases I use usually following "bulk" approach (code not tested, just for example):

namespace common\components;

class Model extends yii\base\Model {

    /**
     * Saves multiple models.
     *
     * @param ActiveRecord[] $models
     * @return bool
     */
    public static saveMultiple($models){

        if(count($models) > 0){

            $firstModel      = reset($models);
            $columnsToInsert = $firstModel->attributes();   // here you can remove excess columns. for example PK column.
            $modelsToInsert  = [];
            $rowsToInsert    = [];

            foreach($models as $model){
                if ($this->beforeSave(true)) {
                    $modelsToInsert[] = $model;
                }
            }

            foreach($modelsToInsert as $model){
                $rowsToInsert[] = array_values($model->attributes);     // here you can remove excess values
            }

            $numberAffectedRows = \Yii::$app->db->createCommand()
                ->batchInsert($firstModel->tableName(), $columnsToInsert, $rowsToInsert)
                ->execute();

            $isSuccess = ($numberAffectedRows === count($models));

            if($isSuccess){
                $changedAttributes = array_fill_keys($columnsToInsert, null); 

                foreach($modelsToInsert as $model){
                    $model->afterSave(true, $changedAttributes);
                }
            }

            return $isSuccess;

        } else {

            return true;
        }

    }

}

This class can be used:

use common\components\Model;

/**
 * @var SomeActiveRecord[] $models Array that contains array of active records (type SomeActiveRecord)
 */ 

// ...

if (Model::validateMultiple($models)){

    if(!Model::saveMultiple($models)){
        // ... some error handling
    }

} else {

    foreach($models as $model){
        if($model->hasErrors()){

            $errors = $model->getFirtsErrors();
            // ... some error handling

        }
    }

}

Additionally, for more convenient working with multiple models can be developed special Collection class that implements \ArrayAccess and \Iterator interfaces. This collection can iterated as simple array, however it contains special methods for bulk operations. Something like this:

foreach($modelCollection as $model){
    // ...
}

$modelCollection->validate();   // works similar to common\components\Model::validateMultiple()
$modelCollection->save();       // works similar to common\components\Model::saveMultiple()