Для нашего проекта, как учетной системы, характерно производить изменения в других объектах после сохранения текущего, например, проведение документа по регистрам после сохранения. Суть в том, что после сохранения объекта в транзакции 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;
}
// ...
}
Это позволило во всем остальном коде не задумываться, что модель необходимо восстанавливать после отката транзакций, а так же заставляет рекурсивно бэкапировать все участвующие модели в сохранении текущей модели.
Может показаться, что проблема и ее решение не стоит внимания, но как показала практика, что если не учесть этого сразу при разработке, то можно долго искать причину непонятных багов.
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:
- Massacres That Matter - Part 1 - 'Responsibility To Protect' In Egypt, Libya And Syria
- Massacres That Matter - Part 2 - The Media Response On Egypt, Libya And Syria
- National demonstration: No attack on Syria - Saturday 31 August, 12 noon, Temple Place, London, UK
Комментариев нет:
Отправить комментарий