Transaction behavior
A little behavior that enables you to use transactions in 1.2 without much work ;)
Example use
Sample model - note the $actsAsTo 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->_setTransactionState( false );
}
/**
* 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->_setTransactionState( true );
$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->_setTransactionState( false );
$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->_setTransactionState( false );
$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
Comment
1 Bugfix in rollback()
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
Question
2 does this work
Comment
3 I got a lot of confuse...
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.
Comment
4 I mean
Comment
5 InnoDB
or does it 'emulate' transactions by using table locks in MyISAM type tables?
Comment
6 Mysql
InnoDB Transactions with MySQL at least, must be BEGIN or START TRANSACTION
Comment
7 SQL Server
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)
Comment
8 Problem
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.
Comment
9 InnoDB engine required
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
Comment
10 SaveAll Transcations
If you use SaveAll, it uses it's owns transaction functions automatically and so no need for this behaviour.
Comment
11 don´t work
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
Comment
12 You can use Core methods to achieve this
Comment
13 Correct locking behavior for InnoDB
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.
Comment
14 RE: Correct locking behavior for InnoDB
Comment
15 RE: Correct locking behavior for InnoDB
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.