Для нашего проекта, как учетной системы, характерно производить изменения в других объектах после сохранения текущего, например, проведение документа по регистрам после сохранения. Суть в том, что после сохранения объекта в транзакции ActiveRecord будет считать, что все изменения прошли успешно, хотя это не гарантировано, ведь последующие изменения могут вызвать Exception, а он в свою очередь к откату транзакции. В нашем случае, это грозит тем, что при ошибочном создании записи, экземпляр ActiveRecord уже будет иметь статус существующей записи (флаг isNewRecord == false) или для новой записи уже будет присвоен primaryKey. Если вы при рендере опирались на эти атрибуты (как мы в нашем проекте), то в результате получите ошибочное представление.
/** * Creates a new model. */ public function actionCreate() { /** @var BaseActiveRecord $model */ $model = new $this->modelClass('create'); $this->performAjaxValidation($model); $model->attributes = Yii::app()->request->getParam($this->modelClass, array()); if (Yii::app()->request->isPostRequest && !Yii::app()->request->isAjaxRequest) { $transaction = $model->getDbConnection()->beginTransaction(); try { $model->save(); $transaction->commit(); $url = array('update', 'id' => $model->primaryKey); $this->redirect($url); } catch (Exception $e) { $transaction->rollback(); } } $this->render('create', array('model' => $model)); }
Это, практически, код по урокам Yii. За одним лишь исключением — сохранение объекта в БД обернуто в транзакцию.
Что делать? Нужно после rollback() восстановить исходное состояние ActiveRecord. В нашем случае нужно было еще восстанавливать все ActiveRecord`ы, измененные внутри исходной модели.
Для начала обращаемся к всемирному разуму, вдруг, мы изобретаем велосипед. На Гитхабе эта проблема уже обсуждалась. Разработчики сказали, что решать на уровне фреймворка у них в планах этого нет, так как это ресурсоемко. Их можно понять для большинства проектов достаточно предварительной валидации модели. Нам не хватает — пишем свое решение проблемы.
Расширяем класс CDbTransaction.
/** * Class DbTransaction * Stores models states for restoring after rollback. */ class DbTransaction extends CDbTransaction { /** @var BaseActiveRecord[] models with stored states */ private $_models = array(); /** * Checks if model state is already stored. * @param BaseActiveRecord $model * @return boolean */ public function isModelStateStoredForRollback($model) { return in_array($model, $this->_models, true); } /** * Stores model state for restoring after rollback. * @param BaseActiveRecord $model */ public function storeModelStateForRollback($model) { if (!$this->isModelStateStoredForRollback($model)) { $model->storeState(false); $this->_models[] = $model; } } /** * Rolls back a transaction. * @throws CException if the transaction or the DB connection is not active. */ public function rollback() { parent::rollback(); foreach ($this->_models as $model) { $model->restoreState(); } $this->_models = array(); } }
Добавляем в класс BaseActiveRecord (расширение CActiveRecord, в нашем проекте он уже существовал) методы restoreState(), hasStoredState() и storeState().
abstract class BaseActiveRecord extends CActiveRecord { /** @var array сохраненное состояние модели */ protected $_storedState = array(); /** * Проверка наличия сохраненного состояния модели * @return boolean */ public function hasStoredState() { return $this->_storedState !== array(); } /** * Сохранение состояния модели * @param boolean $force флаг принудительного сохранения * @return void */ public function storeState($force = false) { if (!$this->hasStoredState() || $force) { $this->_storedState = array( 'isNewRecord' => $this->isNewRecord, 'attributes' => $this->getAttributes(), ); } } /** * Восстановаление состояния модели * @return void */ public function restoreState() { if ($this->hasStoredState()) { $this->isNewRecord = $this->_storedState['isNewRecord']; $this->setAttributes($this->_storedState['attributes'], false); $this->_storedState = array(); } } }
Как видно из кода мы бэкапируем только флаг isNewRecord и текущие атрибуты (в том числе primaryKey). Теперь остается только поправить наш первый фрагмент кода для того чтобы запомнить состояние модели до сохранения.
/** * Creates a new model. */ public function actionCreate() { /** @var BaseActiveRecord $model */ $model = new $this->modelClass('create'); $this->performAjaxValidation($model); $model->attributes = Yii::app()->request->getParam($this->modelClass, array()); if (Yii::app()->request->isPostRequest && !Yii::app()->request->isAjaxRequest) { $transaction = $model->getDbConnection()->beginTransaction(); // Сохраняем состояние объекта $transaction->storeModelStateForRollback($model); try { $model->save(); $transaction->commit(); $url = array('update', 'id' => $model->primaryKey); $this->redirect($url); } catch (Exception $e) { $transaction->rollback(); } } $this->render('create', array('model' => $model)); }
В своем проекте мы пошли чуть дальше — перенесли $transaction->storeModelStateForRollback($model) в метод save() самого BaseActiveRecord.
abstract class BaseActiveRecord extends CActiveRecord { // ... /** * Сохранение экземпляра модели (с поддержкой транзакционности) * @param boolean $runValidation необходимость выполнения валидации перед сохранением * @param array $attributes массив атрибутов для сохранения * @throws Exception|UserException * @return boolean результат операции */ public function save($runValidation = true, $attributes = null) { /** @var DbTransaction $transaction */ $transaction = $this->getDbConnection()->getCurrentTransaction(); $isExternalTransaction = ($transaction !== null); if ($transaction === null) { $transaction = $this->getDbConnection()->beginTransaction(); } $transaction->storeModelStateForRollback($this); $exception = null; try { $result = parent::save($runValidation, $attributes); } catch (Exception $e) { $result = false; $exception = $e; } if ($result) { if (!$isExternalTransaction) { $transaction->commit(); } } else { if (!$isExternalTransaction) { $transaction->rollback(); } throw $exception; } return $result; } // ... }
Это позволило во всем остальном коде не задумываться, что модель необходимо восстанавливать после отката транзакций, а так же заставляет рекурсивно бэкапировать все участвующие модели в сохранении текущей модели.
Может показаться, что проблема и ее решение не стоит внимания, но как показала практика, что если не учесть этого сразу при разработке, то можно долго искать причину непонятных багов.
ссылка на оригинал статьи http://habrahabr.ru/post/198100/
Добавить комментарий