How to nest DB transactions without actually nesting them.

You are viewing revision #3 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#4) »

To secure your database from inconsistencies resulting from failing (complex) operations, you (should) use transactions. However, complex methods often rely on other complex methods that themselves build on transactions. This HowTo presents a method to nest or embed transactions without relying on the database's nesting capabilities.

If you are looking to do real transaction nesting, look at How to use nested DB transactions (MySQL 5+, PostgreSQL) .

Here is the replacement for CActiveRecord which provides beginTransaction, rollback and commit as methods of CActiveRecord:

class YActiveRecord extends CActiveRecord {
    /**
     * Keeps transaction status
     *    (true if previous transaction, transaction if local trasaction, null if no transaction).
     *
     * @var CDbTransaction||bool||null
     */
    private $_transaction=null;
    /**
     * Begin a transaction on the database.
     */
    public function beginTransaction() {
        if($this->_transaction===null) {
            throw new CException('Transaction already started');
        }
        $dc=$this->getDbConnection();
        $transaction=$dc->getCurrentTransaction();
        if($transaction) {
            $this->_transaction=true;
        } else {
            $this->_transaction=$dc->beginTransaction();
        }
    }

    /**
     * Commit a transaction on the database.
     *
     * @throws CException
     */
    public function commit() {
        if($this->_transaction===null) {
            throw new CException('No ongoing transaction');
        } else {
            $this->_transaction->commit();
        }
        $this->_transaction=null;
    }

    /**
     * Rollback a transaction on the database.
     * @throws CException
     */
    public function rollback() {
        if($this->_transaction===null) {
            throw new CException('No ongoing transaction');
        } else {
            $this->_transaction->rollback();
        }
        $this->_transaction=null;
    }
}

So to use the transaction replacements, you have to extend your models from YActiveRecord in stead of CActiveRecord. You can also choose to put the above code in your models of course.

As I found it cumbersome to do a 'getDbConnection()' first and then do the transaction request, I have made those "native" methods of the ActiveRecord. You are either doing a transaction with the model or not, you do not really want to know about the database.

The implemention will check if a transaction is already ongoing on the database or not. If there is one, no new transaction will be created and '$_transaction' is set to true to indicate that a transaction is active, but that it was not created in this instance. It relies on the fact that you should begin and end a transaction in the same method. It does not allow you to end a transaction in another model (while other implementations allow you to do that, which might actually be error prone).

When ending the transaction ('commit' or 'rollback'), this implementation will check if this instance previously started a transaction, and then actually perform the 'commit' or 'rollback' depending on whether the real transaction was started locally or not.

The risk of this implementation is that a nested transaction that you 'rollback' is not actually rolled back in the database. This method relies on the fact that the calling method will check the result of the "sub"-method and rollback itself if an error is returned.

Hence you should use it like this:

class Model1 extends YActiveRecord {
   public function complexRecordCreation() {
       $result=true; // Suppose everything is ok.
       $this->beginTransaction();
       // Do some work. ($result&=...)
       $result&=$model2->complexRecordCreation();

       // ...
       if($result)
           $result&=$model3->save();
       // Do some other work. ($result&=...)
       if(!$result) {
          $this->rollback();
       } else {
          $this->commit();
       }
       return $result;
   }
}

class Model2 extends YActiveRecord {
   public function complexRecordCreation() {
       $result=true; // Suppose everything is ok.
       $this->beginTransaction();
       // Do some work. ($result&=...)
       $result&=$model4->complexRecordCreation();

       // Do some other work. ($result&=...)

       if(!$result) {
          $this->rollback();
       } else {
          $this->commit();
       }
       return $result;
   }
}

In the above code, '$result' indicates the success of the operation. You can use this to check if you should continue performing the operation or not in the complex operation - in the end it must indicate the overall success of the operation. As the callers rely on this result, the overall transaction is rolled back even if one of the subtransactions fails.

Each method has its (dis)advantages, so it is up to you to choose.

1 0
1 follower
Viewed: 10 523 times
Version: Unknown (update)
Category: How-tos
Written by: le_top
Last updated by: le_top
Created on: Nov 22, 2014
Last updated: 9 years ago
Update Article

Revisions

View all history

Related Articles