YiiのActiveRecordおよびロールバックトランザクション

管理会計のスタートアッププロジェクトを開発する際に遭遇した問題についてお話ししたいと思います。



会計システムとしてのプロジェクトでは、現在のオブジェクトを保存した後に他のオブジェクトに変更を加えるのが一般的です。たとえば、保存後にレジスタにドキュメントを保持します。 一番下の行は、オブジェクトをトランザクションに保存した後、ActiveRecordはすべての変更が成功したと想定しますが、後続の変更が例外を引き起こす可能性があるため、これは保証されません。 私たちの場合、これは、レコードが誤って作成された場合、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チュートリアルコードです。 1つの例外を除いて、データベースにオブジェクトを保存すると、トランザクションにラップされます。



どうする rollback()の後、ActiveRecordの元の状態を復元する必要があります。 この場合、元のモデル内で変更されたすべてのActiveRecordを復元する必要がありました。



そもそも、私たちはグローバルな心に目を向け、突然、自転車を発明します。 Githubでこの問題はすでに議論されています。 開発者は、リソースを大量に消費するため、フレームワークレベルでこれを計画していないと述べました。 これらは、モデルの予備検証を行うことで、ほとんどのプロジェクトで理解できます。 私たちは行方不明です-私たちは問題の解決策を書いています。



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(); } }
      
      







restoreActive()、hasStoredState()、およびstoreState()メソッドをBaseActiveRecordクラスに追加します(CActiveRecord拡張、プロジェクトに既に存在します)。



 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)をBaseActiveRecord自体のsave()メソッドに移動しました。



 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; } // ... }
      
      





これにより、コードの残りの部分は、トランザクションのロールバック後にモデルを復元する必要がないと考えることができず、現在のモデルを保存する際にすべての参加モデルを再帰的にバックアップすることも強制されます。



問題とその解決策は注目に値しないように思えるかもしれませんが、開発中にすぐにこれを考慮に入れなければ、理解できないバグの原因を長期間検索できることが実践的に示されています。



All Articles