Transaction behavior

By Christian Winther (Jippi)
A little behavior that enables you to use transactions in 1.2 without much work ;)

Example use

Sample model - note the $actsAs

To change default settings do:
Download code
var $actsAs = array('transaction' => array('keyName' => 'value') );

Model Class:

Download code <?php 
class Order extends AppModel
{
    var 
$name     'Order';
    var 
$actsAs   = array('transaction');
    var 
$hasMany  = array('OrderDetail');
?>

Controller Class:

Download code <?php 
class OrderController extends AppController {
    var 
$name 'Order';
    var 
$uses = array('Order');

    function 
checkout() {
        if(!empty(
$this->data)) {
            
$this->Order->begin(); // Start our transaction
            
if( $this->Order->save$this->data ) ) {
                if( 
$this->Order->OrderDetail->save$this->data ) ) {
                    
$this->Order->commit(); // Persist the data
                    
$this->redirect('/shop/order/thanks');
                    exit;    
                } else { 
// Couldnt save the products to the order
// since the products to the order couldnt be stored, dont store anything at all
// rollback all changes to the database sine 'begin' was called
                    
$this->Order->rollback(); 
                    
$this->redirect('/shop/order/error');
                    exit;
                }
            } else { 
                
// Didnt save
            
}
        }
    }
}
?>

Model Class:

Download code <?php 
/**
 * Transaction support for models in 1.2
 *
 * @version 0.2
 * @author Christian 'Jippi' Winther
 */
class TransactionBehavior extends ModelBehavior {
    
/**
     * Settings: (false will disable them)
     *
     * - autocommit_on_start [ false | 0 | 1 ]
     *      Disable auto-commit on start?
     *
     * - autocommit_after_commit [ false | 0 | 1 ]
     *      Set auto-commit after successfull commit
     *
     * - autocommit_after_rollback [ false | 0 | 1 ]
     *      Set auto-commit after successfull rollback
     *
     * - throw_error [ false | true ]
     *      Should we call user_error if something is wrong?
     *
     * - error_level [ E_USER_* constants ]
     *      What level should the user_error use?
     *
     * - configure_key [ string ]
     *       What key should the transaction state be stored as
     *
     * @version 0.1
     * @since   0.1
     * @access  private
     * @var     array
     */
    
var $settings = array(
        
'autocommit_on_start'       => false,
        
'autocommit_after_commit'   => 1,
        
'autocommit_after_rollback' => 1,
        
'throw_error'               => false,
        
'error_level'               => E_USER_NOTICE,
        
'configure_key'             => 'inTransaction'
    
);

    
/**
     * List of legal 'locks'
     *
     * @version 0.1
     * @since   0.1
     * @access  private
     * @var     array
     */
    
var $validLocks = array('read','write');

    
/**
     * Startup hook from the model
     *
     * @version 0.1
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   array $config
     */
    
function setup(&$model$config = array()) {
        
/**
         * Check if there has been set a transaction state in another model
         * Configure::read returns null if no value is found, so its safe to
         * set the default value for our transaction state ( false )
         */
        
if( is_null$this->_getTransactionState() ) ) {
            
$this->_setTransactionStatefalse );
        }

        
/**
         * If there was given any config settings to this behavior, merge them
         * with our default settings array
         * If the input arrays have the same string keys, then the later value for that key will overwrite the previous one
         */
        
if( !is_null$config ) && is_array$config ) ) {
            
$this->settings array_merge$this->settings$config );
        }

        
/**
         * If we wish to start an transaction at load time do so
         */
        
if( $this->settings['autocommit_on_start'] !== false ) {
            
$this->begin$model );
        }
     }

    
/**
     * Begin a database transaction
     *
     * @version 0.1
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   int $autocmmit
     * @return  boolean
     */
    
function begin( &$model$args = array()) {
        if( 
$this->_getTransactionState() === false ) {
            
$this->_setTransactionStatetrue );
            
$model->query('SET autocommit=0');
            
$model->query('begin');
            return 
true;
        }
        
$this->_error('Transaction has already started');
        return 
false;
    }

    
/**
     * Commits a database transaction
     *
     * @version 0.1
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   integer $autocommit
     * @return  boolean
     */
    
function commit( &$model$args = array() ) {
        if( 
$this->_getTransactionState() === true ) {
            
$this->_setTransactionStatefalse );

            
$autoCommit = empty($args[0]) ? $this->settings['autocommit_after_commit'] : $args[0];
            
$model->query('commit');
            if( 
$autoCommit !== false ) {
                
$model->query('SET autocommit='$autoCommit);
            }
            return 
true;
        }
        
$this->_error('No transaction active');
        return 
false;
    }

    
/**
     * Rollback a transaction
     *
     * @version 0.1
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   integer $autocommit
     */
    
function rollback( &$model$args ) {
        if( 
$this->_getTransactionState === true ) {
            
$this->_setTransactionStatefalse );
            
$model->query('rollback');
            
$autoCommit = empty($args[0]) ? $this->settings['autocommit_after_rollback'] : $args[0];
            if( 
$autoCommit !== false ) {
                
$model->query('SET autocommit='$autoCommit);
            }
            return 
true;
        }
        
$this->_error('No transaction active');
        return 
false;
    }

    
/**
     * Locks the model table for either READ or WRITE
     *
     * - Arg[0] is either 'READ' or 'WRITE'
     * - Arg[1]:
     *     If null     : Its going to lock the model table
     *     If string   : Locking the given table
     *     If array    : Going to lock all tables specified
     *                   NOTE: You have to specify model table aswell!
     *
     * @version 0.2
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   array $args
     */
    
function lockTable( &$model$args = array( ) ) {
        
$type  strtolower( empty( $args[0] ) ? null  $args[0] );
        
$table strtolower( empty( $args[1] ) ? $model->table $args[1] );
        if( 
is_null$type ) ) {
            
$this->_error('Missing parameter for lockTable(), either READ or WRITE');
            return 
false;
        }
        if( 
array_search$type$this->validLocks ) !== false ) {
            if(
is_array($table)) {
                
$res = array();
                foreach ( 
$table AS $tableName ) {
                    
$res$tableName ] = $model->query('LOCK TABLE '.$table.' 'strtoupper($type));
                }
                return 
$res;
            } else {
                
$model->query('LOCK TABLE '.$table.' 'strtoupper($type));
                return 
true;
            }
        }
        return 
false;
    }

    
/**
     * Remove all locks made on tables
     *
     * @version 0.1
     * @since   0.1
     * @access  public
     * @param   AppModel $model
     * @param   array $args
     */
    
function unlockTable( &$model$args = array() ) {
        
$model->query('UNLOCK TABLES');
    }


    
/**
     * Show an error to screen (When debug > 0 )
     *
     * @version 0.1
     * @since 0.1
     * @access private
     * @param string $string
     */
    
function _error$string ) {
        if( 
$this->settings['throw_error'] ) {
            
trigger_error($string$this->settings['error_level'] );
        }
    }

    
/**
     * Get the current transaction state
     *
     * @version 0.1
     * @since 0.1
     * @access private
     * @return boolean
     */
    
function _getTransactionState() {
        return 
Configure::read$this->settings['configure_key'] );
    }

    
/**
     * Write a new transaction state
     *
     * @version 0.1
     * @since 0.1
     * @access private
     * @param boolean $value
     */
    
function _setTransactionState$value ) {
        if( 
is_bool$value ) ) {
            
Configure::write$this->settings['configure_key'], $value );
        }
        
$this->_error('Not valid transaction state');
    }
}
?>

 

Comments 228

CakePHP Team Comments Author Comments
 

Comment

1 Bugfix in rollback()

Hi, there appear to be two minor typos in the rollback function as follows:


function rollback( &$model, $args ) {}

fails with an error but



function rollback( &$model, $args = array() ) {

works fine. Also,


if( $this->_getTransactionState === true ) {

should be



if( $this->_getTransactionState() === true ) {

Make those changes and the behavior works on 1.2. Thanks, Ianh
Posted Aug 13, 2007 by Ian
 

Question

2 does this work

does this work in 1.1.x?
Posted Sep 14, 2007 by stab
 

Comment

3 I got a lot of confuse...

same as stab question, does this work in 1.1.x ?

I thought the transaction documentation in manual was very bad. Only shown the function and its parameters, without any instruction how to use it.

Can't move on with 1.2, since it's alpha and I'm build public web-application.
Posted Sep 25, 2007 by maulana kurniawan
 

Comment

4 I mean

I know behaviours don't, but can this work in 1.1 with current filters/hooks?
Posted Sep 25, 2007 by stab
 

Comment

5 InnoDB

does this require the database engine to be set to InnoDB
or does it 'emulate' transactions by using table locks in MyISAM type tables?
Posted Sep 26, 2007 by declan costello
 

Comment

6 Mysql

MyISAM

InnoDB Transactions with MySQL at least, must be BEGIN or START TRANSACTION

does this require the database engine to be set to InnoDB
or does it 'emulate' transactions by using table locks in MyISAM type tables?
Posted Sep 26, 2007 by stab
 

Comment

7 SQL Server

It looks like this code is MySQL-specific which is fine for most CakePHP applications. Here are some examples of how some other popular databases start transactions:

MySQL - BEGIN
PostgreSQL - START TRANSACTION
SQL Server 2000/MSDE/2005/Express - BEGIN TRANSACTION

Unfortunately this is yet another area of SQL that is not shared among database vendors.

Also, note that the cake / libs / model / dbo / dbo_*.php classes have the begin/commit/rollback transaction functions built-in.

It appears that the models themselves have begin(), commit(), and rollback() functions. I just tested with MySQL. This would be more database independent than the query() function.

(I'm using Cake version 1.1.16.5421)
Posted Oct 18, 2007 by Graeme Wicksted
 

Comment

8 Problem

I find this article interesting but I've been trying to use it for hours and I don't get to make it work. I've copied the behavior into /app/models/behaviors, added the $actsAs to the model and copied the code for the controller, but it doesn't work.

I'm using CakePHP 1.2.0.6311 beta version, and maybe this is the problem. I don't know whether it implements the transactions such as version 1.1 or such as version 1.2. I've also tried writting "var $transactional = true;" in the model (it corresponds with version 1.1), but it didn't work either.

Can anyone help me? I'll accept any ideas... it seems to be easy but I don't know why I can't.
Posted Aug 22, 2008 by Cristina
 

Comment

9 InnoDB engine required

I find this article interesting but I've been trying to use it for hours and I don't get to make it work. I've copied the behavior into /app/models/behaviors, added the $actsAs to the model and copied the code for the controller, but it doesn't work.

I'm using CakePHP 1.2.0.6311 beta version, and maybe this is the problem. I don't know whether it implements the transactions such as version 1.1 or such as version 1.2. I've also tried writting "var $transactional = true;" in the model (it corresponds with version 1.1), but it didn't work either.

Can anyone help me? I'll accept any ideas... it seems to be easy but I don't know why I can't.


I'm so busy I forgot to tell you I solved my problem. I thought the problem was in my CakePHP version, but I didn't realize that my database tables had MyISAM engines. Just setting them to InnoDB my problem disappeared.

Finally, I decided not to use CakePHP transactions because I wanted to commit some tables at the same time and I don't find useful having to do commit in every model, separatedly. So I'm using PHP instruction mysql_query();
The next page is in Spanish but it may help you.

http://carnar.blogspot.com/2006/11/transacciones-en-php-y-mysql.html
Posted Sep 3, 2008 by Cristina
 

Comment

10 SaveAll Transcations

I find this article interesting but I've been trying to use it for hours and I don't get to make it work. I've copied the behavior into /app/models/behaviors, added the $actsAs to the model and copied the code for the controller, but it doesn't work.

I'm using CakePHP 1.2.0.6311 beta version, and maybe this is the problem. I don't know whether it implements the transactions such as version 1.1 or such as version 1.2. I've also tried writting "var $transactional = true;" in the model (it corresponds with version 1.1), but it didn't work either.

Can anyone help me? I'll accept any ideas... it seems to be easy but I don't know why I can't.


I'm so busy I forgot to tell you I solved my problem. I thought the problem was in my CakePHP version, but I didn't realize that my database tables had MyISAM engines. Just setting them to InnoDB my problem disappeared.

Finally, I decided not to use CakePHP transactions because I wanted to commit some tables at the same time and I don't find useful having to do commit in every model, separatedly. So I'm using PHP instruction mysql_query();
The next page is in Spanish but it may help you.

http://carnar.blogspot.com/2006/11/transacciones-en-php-y-mysql.html

If you use SaveAll, it uses it's owns transaction functions automatically and so no need for this behaviour.
Posted Sep 3, 2008 by Penfold
 

Comment

11 don´t work

Hi to all.
I´m writing a web application where need to edit information of the students.
I´m tring use transaction, but I think that don´t works.
The function for editing is:

function edit($id = null) {
$this->Alumno->begin();

$this->Alumno->id = $id;

if (empty($this->data)) {
$this->data = $this->Alumno->read();
} else {

$this->data["Alumno"]["nombre_apellidos"] = $this->data ["Alumno"]["nombre"]." ".$this->data["Alumno"]["apellidos"];




if ($this->Alumno->save($this->data))
{
$this->Alumno->commit(); // Persist the data
$this->flash('Las modificaciones se han guardado.','/alumnos');
exit;
}
else
{
$this->Alumno->rollback();
$this->flash('En este momento no se puede modificar datos del alumno.
Intente mas tarde por favor.','/alumnos');
exit;
}
}
}

Any idea what is wrong? or need to do more configurations?
Apreciate any help, thanks
Posted Jun 4, 2009 by Nelli
 

Comment

12 You can use Core methods to achieve this

Actually you can use cakephp features to use transactions, in that way you are sure your transactions will be executed no matter what database engine you use, take a look at this article I wrote: http://www.blog.cakephp4all.com/?p=7
Posted Mar 2, 2010 by Guillermo Mansilla
 

Comment

13 Correct locking behavior for InnoDB

Refer to the following passage from http://dev.mysql.com/doc/refman/5.0/en/lock-tables-and-transactions.html:

The correct way to use LOCK TABLES and UNLOCK TABLES with transactional tables, such as InnoDB tables, is to begin a transaction with SET autocommit = 0 (not START TRANSACTION) followed by LOCK TABLES, and to not call UNLOCK TABLES until you commit the transaction explicitly.

So the use of lockTable() and begin() should be exclusive, and lockTable() should be modified to set autocommit = 0 prior to acquiring the lock. Otherwise, calling begin() followed by lockTable() will implicitly commit any live transactions, and calling lockTable() followed by begin() will implicitly release any existing locks.
Posted Mar 31, 2010 by Tad Fisher
 

Comment

14 RE: Correct locking behavior for InnoDB

So, How would you do it in the cakephp way?
Refer to the following passage from http://dev.mysql.com/doc/refman/5.0/en/lock-tables-and-transactions.html:

The correct way to use LOCK TABLES and UNLOCK TABLES with transactional tables, such as InnoDB tables, is to begin a transaction with SET autocommit = 0 (not START TRANSACTION) followed by LOCK TABLES, and to not call UNLOCK TABLES until you commit the transaction explicitly.

So the use of lockTable() and begin() should be exclusive, and lockTable() should be modified to set autocommit = 0 prior to acquiring the lock. Otherwise, calling begin() followed by lockTable() will implicitly commit any live transactions, and calling lockTable() followed by begin() will implicitly release any existing locks.
Posted Apr 1, 2010 by Guillermo Mansilla
 

Comment

15 RE: Correct locking behavior for InnoDB

Ideally, locking primitives should be implemented at the DB abstraction layer, providing both table-level and row-level locking functionality. It's possible to emulate this behavior for DBMSs that do not provide it, by using an application-wide lock mechanism (such as temporary lock descriptors). Model methods could implemented to expose this feature to users, and model fields could be set to automatically enable/disable locking tables and/or rows during a transaction.

On a tangent, when I come across a design that requires locking, I often find that an application-wide mutex can better serve this purpose. MySQL provides a decent solution through its 'GET_LOCK', 'RELEASE_LOCK', and 'IS_FREE_LOCK' functions. Working with these gives you much more fine-grained control over locking; for example, you can lock only around controller actions that update-after-read, leaving other actions free to read those tables/rows to their heart's content.

So, How would you do it in the cakephp way?
Refer to the following passage from http://dev.mysql.com/doc/refman/5.0/en/lock-tables-and-transactions.html:

The correct way to use LOCK TABLES and UNLOCK TABLES with transactional tables, such as InnoDB tables, is to begin a transaction with SET autocommit = 0 (not START TRANSACTION) followed by LOCK TABLES, and to not call UNLOCK TABLES until you commit the transaction explicitly.

So the use of lockTable() and begin() should be exclusive, and lockTable() should be modified to set autocommit = 0 prior to acquiring the lock. Otherwise, calling begin() followed by lockTable() will implicitly commit any live transactions, and calling lockTable() followed by begin() will implicitly release any existing locks.
Posted Apr 15, 2010 by Tad Fisher