I really like this NestedPDO solution for Yii but I need some different transaction handling.
I want to commit my nested transactions only if all nested transactions could be commited and if ONE transaction does a rollback all transactions should be rolled back.
How can I do that?
My try of changing the rollBack function which didn't work:
public function rollBack() {
$this->transLevel--;
if($this->transLevel == 0 || !$this->nestable()) {
parent::rollBack();
} else {
$level = $this->transLevel;
for($level; $level>1; $level--){
$this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->constantlevel}");
}
//parent::rollBack();
}
}
I was thinking of adapting the NestedPDO: In function commit() do a commit only on the outermost transaction and in function rollBack() do a rollback to the outermost transaction no matter which sub transaction caused the rollback. But I could not get it done...
I'm using MySQL and InnoDB tables and I'm not sure about autocommit but when echoing the value of autocommit within a transaction I always get the value 1 which should mean autocommit is on but within a transaction autocommit should be set to 0. I'm not sure whether this is the cause why a whole rollback does not work for me?
If you want the whole transaction be rolled back automatically as soon as an error occurs, you could just re-throw the exception from B's exception handler when called from some specific locations (eg. from A()):
function A(){
...
$this->B(true);
...
}
/*
* #param B boolean Throw an exception if the transaction is rolled back
*/
function B($rethrow) {
$transaction=Yii::app()->db->beginTransaction();
try {
//do something
$transaction->commit();
} catch(Exception $e) {
$transaction->rollBack();
if ($rethrow) throw $e;
}
}
Now I understand you actually just want your wrapper to detect if a transaction is already in progress, and in this case not start the transaction.
Therefore you do not really need the NestedPDO class. You could create a class like this instead:
class SingleTransactionManager extends PDO {
private $nestingDepth = 0;
public function beginTransaction() {
if(!$this->nestingDepth++ == 0) {
parent::beginTransaction();
} // else do nothing
}
public function commit() {
$this->nestingDepth--;
if (--$this->nestingDepth == 0) {
parent::commit();
} // else do nothing
}
public function rollback() {
parent::rollback();
if (--$this->nestingDepth > 0) {
$this->nestingDepth = 0;
throw new Exception(); // so as to interrupt outer the transaction ASAP, which has become pointless
}
}
}
Based on the answer of #RandomSeed I've created a 'drop in' for default Yii transaction handling:
$connection = Yii::app()->db;
$transaction=$connection->beginTransaction();
try
{
$connection->createCommand($sql1)->execute();
$connection->createCommand($sql2)->execute();
//.... other SQL executions
$transaction->commit();
}
catch(Exception $e)
{
$transaction->rollback();
}
This is my SingleTransactionManager class:
class SingleTransactionManager extends CComponent
{
// The current transaction level.
private $transLevel = 0;
// The CDbConnection object that should be wrapped
public $dbConnection;
public function init()
{
if($this->dbConnection===null)
throw new Exception('Property `dbConnection` must be set.');
$this->dbConnection=$this->evaluateExpression($this->dbConnection);
}
// We only start a transaction if we're the first doing so
public function beginTransaction() {
if($this->transLevel == 0) {
$transaction = parent::beginTransaction();
} else {
$transaction = new SingleTransactionManager_Transaction($this->dbConnection, false);
}
// always increase transaction level:
$this->transLevel++;
return $transaction;
}
public function __call($name, $parameters)
{
return call_user_func_array(array($this->dbConnection, $name), $parameters);
}
}
class SingleTransactionManager_Transaction extends CDbTransaction
{
// boolean, whether this instance 'really' started the transaction
private $_startedTransaction;
public function __construct(CDbConnection $connection, $startedTransaction = false)
{
$this->_startedTransaction = $startedTransaction;
parent::__construct($connection);
$this->setActive($startedTransaction);
}
// We only commit a transaction if we've started the transaction
public function commit() {
if($this->_startedTransaction)
parent::commit();
}
// We only rollback a transaction if we've started the transaction
// else throw an Exception to revert parent transactions/take adquate action
public function rollback() {
if($this->_startedTransaction)
parent::rollback();
else
throw new Exception('Child transaction rolled back!');
}
}
This class 'wraps' the main database connection, you should declare it as component like this in your config:
'components'=>array(
// database
'db'=>array(
'class' => 'CDbConnection',
// using mysql
'connectionString'=>'....',
'username'=>'...',
'password'=>'....',
),
// database
'singleTransaction'=>array(
'class' => 'pathToComponents.db.SingleTransactionManager',
'dbConnection' => 'Yii::app()->db'
)
Note that the dbConnection property should be an expression to the master database connection.
Now, when nesting transactions in nested try catch blocks, you can create an error in for example nested transaction 3, and the ones on 1 and 2 are rolled back also.
Test code:
$connection = Yii::app()->singleTransaction;
$connection->createCommand('CREATE TABLE IF NOT EXISTS `test_transactions` (
`number` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;')->execute();
$connection->createCommand('TRUNCATE TABLE `test_transactions`;')->execute();
testNesting(4, 3, 1);
echo '<br>';
echo 'Rows:';
echo '<br>';
$rows = $connection->createCommand('SELECT * FROM `test_transactions`')->queryAll();
if($rows)
{
foreach($rows as $row)
{
print_r($row);
}
}
else
echo 'Table is empty!';
function testNesting(int $total, int $createErrorIn = null, int $current = 1)
{
if($current>=$total)
return;
$connection = Yii::app()->singleTransaction;
$indent = str_repeat(' ', ($current*4));
echo $indent.'Transaction '.$current;
echo '<br>';
$transaction=$connection->beginTransaction();
try
{
// create nonexisting columnname when we need to create an error in this nested transaction
$columnname = 'number'.($createErrorIn===$current ? 'rr' : '');
$connection->createCommand('INSERT INTO `test_transactions` (`'.$columnname.'`) VALUES ('.$current.')')->execute();
testNesting($total, $createErrorIn, ($current+1));
$transaction->commit();
}
catch(Exception $e)
{
echo $indent.'Exception';
echo '<br>';
echo $indent.$e->getMessage();
echo '<br>';
$transaction->rollback();
}
}
Results in the following output:
Transaction 1
Transaction 2
Transaction 3
Exception
CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'numberrr' in 'field list'. The SQL statement executed was: INSERT INTO `test_transactions` (`numberrr`) VALUES (3)
Exception
Child transaction rolled back!
Exception
Child transaction rolled back!
Rows:
Table is empty!
IMHO, the idea of simulating "nested transactions" in application code is an anti-pattern. There are numerous anomaly cases that are impossible to solve in the application (see my answer to https://stackoverflow.com/a/319939/20860).
In PHP, it's better to keep it simple. Work is organized naturally into requests, so use the request as the transaction scope.
Start a transaction at the controller level, before you call any model classes.
Let models throw exceptions if anything goes wrong.
Catch the exception at the controller level, and rollback if necessary.
Commit if no exception is caught.
Forget about all the nonsense about transaction levels. Models should not be starting, committing, or rolling back any transactions.
Related
I have a nested transaction that uses to commit few different tables. I only want to commit one table in the nested and the other two tables in the outer commit.
beginTransaction();
error_message.=function A();
error_message.=function B();
if (empty error_message){
commit(); //commit table B and C
} else {
rollback();
}
function A(){
beginTransaction();
update table A //wants to commit table A immediately
commit();
//do some stuff
update table B
if(error) {
return error_message
} else {
return null
}
}
function B(){
beginTransaction();
update table A //wants to commit table A immediately
commit();
//do some stuff
update table C
if(error) {
return error_message
} else {
return null
}
}
When you call beginTransaction(); before the function, there will be two beginTransaction(); first is outside of the function and second is inside the function.
beginTransaction();
error_message.=function A();
error_message.=function B();
Put your beginTransaction(); below the function call.
error =0;
error_message = function A();
if(empty(error_message)){
error =1;
}
error_message = function B();
if(empty(error_message)){
error =1;
}
beginTransaction();
if (error){
commit(); //commit table B and C
} else {
rollback();
}
i think in you miss rollback() in write function a() or b().
rollback()
and dbtransaction is not running when you skip 1 methods of 3
1 beginTransaction()//start transection
2 commit()//commit your transaction
3 rollback()//rollback your transaction
without rollback beginTransaction not working.
and i think you miss ()after empty
What happens if transaction.Rollback/Commit never called in before closing the connection ?
public DBStatus InsertUpdateUserProfile(Int64 UserID, W_User_Profile oUser)
{
MySqlConnection oMySQLConnecion = null;
MySqlTransaction tr = null;
DBStatus oDBStatus = new DBStatus();
try
{
oMySQLConnecion = new MySqlConnection(DatabaseConnectionString);
if (oMySQLConnecion.State == System.Data.ConnectionState.Closed || oMySQLConnecion.State == System.Data.ConnectionState.Broken)
{
oMySQLConnecion.Open();
}
tr = oMySQLConnecion.BeginTransaction();
if (oMySQLConnecion.State == System.Data.ConnectionState.Open)
{
string Query = #"INSERT INTO user .....................;"
INSERT IGNORE INTO user_role ....................;";
MySqlCommand oCommand = new MySqlCommand(Query, oMySQLConnecion);
oCommand.Transaction = tr;
oCommand.Parameters.AddWithValue("#UserID", UserID);
oCommand.Parameters.AddWithValue("#AddressID", oUser.AddressID);
................
................
int sqlSuccess = oCommand.ExecuteNonQuery();
if (sqlSuccess>0)
{
tr.Commit();
oDBStatus.Type = DBOperation.SUCCESS;
oDBStatus.Message.Add(DBMessageType.SUCCESSFULLY_DATA_UPDATED);
}
oMySQLConnecion.Close();
}
else
{
oDBStatus.Type = DBOperation.ERROR;
oDBStatus.Message.Add(DBMessageType.ERROR_DUE_TO_NO_DB_CONNECTION);
}
return oDBStatus;
}
catch (Exception ex)
{
if (oMySQLConnecion.State == System.Data.ConnectionState.Open)
{
tr.Rollback();
oMySQLConnecion.Close();
}
oDBStatus.Type = DBOperation.ERROR;
oDBStatus.Message.Add(DBMessageType.ERROR_OR_EXCEPTION_OCCURED_WHILE_UPDATING);
oDBStatus.InnerException.Add(ex.Message);
return oDBStatus;
}
}
In the above function, I do Commit if transaction is successful and Rollback if it fails and connection is still on.
If the connection is terminated there is no Rollback. I read many places that says it will be a automatic Rollback if the connection terminates without Commit (what I want). Is this a bad practice ? I could add try-catch after connection establishes but add little code in every similar functions. Does it really necessary ?
What happens if transaction.Rollback/Commit never called in before closing the connection ?
In MySQL the transaction is rolled back. But some other table servers commit it on connection close.
Pro tip: Don't rely on this behavior except as a way to handle a hard crash.
I am working in a project where the users have a rate plan associated. When a new user is created, a valid rate plan must be specified.
I have the following MySQL schema and Eloquent models:
/**
* User Eloquent model file ...
*
*/
public function ratePlans() {
return $this->belongsToMany(
'App\Models\RatePlan',
'users_rate_plans',
'users_id',
'rate_plans_id'
);
}
So, to create a new user with your selected rate plan i do:
try {
\DB::beginTransaction();
$model->create($data);
$model->ratePlans()->attach($data['rate_plan'], ['active' => 1]);
\DB::commit();
return $model;
} catch(\Exception $e) {
\DB::rollback();
return false;
}
But, i am getting the next exception:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column
'users_id' cannot be null (SQL: insert into users_rate_plans
(active, rate_plans_id, users_id) values (1, 43, ))
Why te transaction didn't work ? How i can do that task ?
UPDATE 1
I changed the transaction code but, the result is the same.
try {
\DB::beginTransaction();
$ratePlan = \App\Models\RatePlan::find($data['rate_plan']);
$user->ratePlans()->attach($ratePlan, ['active' => 1]);
$user->create($data);
\DB::commit();
return $user;
} catch(\Exception $e) {
\DB::rollback();
die($e->getMessage());
return false;
}
UPDATE 2
I changed the transaction code again and its works:
\DB::beginTransaction();
$user = \App\Models\User::create($data);
$ratePlan = \App\Models\RatePlan::find($data['rate_plan']);
$user->ratePlans()->attach($ratePlan, ['active' => 1]); \DB::commit();
return $user;
I think you're not loading the rate_plans entity.
$model->ratePlans()->attach($data['rate_plan'], ['active' => 1]);
In this line $data['rate_plan'] shoud be an instance of your model "rate_plans", and I'm assuming that $model in this situation stand for an entity of your User model
Also have tried a test without the transaction if the result's is the same ?
I give you an example of one of my code which is similar:
try {
DB::beginTransaction();
$group = Group::create($data);
$employees = User::whereIn('reference', $references)->get();
$group->employees()->attach($employees);
$group->save();
DB::commit();
} catch(Exception $e) { [...] }
Good luck
this is my code:
$transaction = Yii::app()->db->beginTransaction();
try {
$tModel->save();
$activationLink = new ActivationLink;
$activationLink->User_id = $tModel->id;
$activationLink->hash1 = User::generateHashCode(100);
$activationLink->hash2 = User::generateHashCode();
$activationLink->hash3 = User::generateHashCode();
$activationLink->time = time();
$activationLink->save();
User::sendActivatonLink($tModel->mail,$activationLink->id, $activationLink->hash1, $activationLink->hash2, $activationLink->hash3);
$transaction->commit();
$this->redirect(array('view', 'id' => $tModel->id));
} catch (Exception $e) {
$transaction->rollback();
Yii::app()->user->setFlash('error', "{$e->getMessage()}");
$this->refresh();
}
$tModel saved but $activationLink doesn't so it should rolled back. but it didn't ,why?
Yii save() does not throw an exception, when just the validation fails. Thus you have to check the result of save() yourself:
if (!$model->save())
$transaction->rollback();
//or:
if (!$model->save())
throw new Exception("This will trigger my catch statement block");
Please check your mysql engine I think you are not using innodb. To execute transaction we must use innodb. Let me know your table type/engine.
OR
You also need to add in your code to understand error in log.
throw new Exception($e);
It it possible in Silex to use an error handler based on what exception is thrown?
I know this is possible with a single exception handler and a switch statement on the classname of the thrown exception but to me it seems the "Silex way" is cleaner, yet doesn't work.
This is how I would expect it to work
<?php
// Handle access denied errors
$app->error(function (\App\Rest\Exception\AccessDenied $e) {
$message = $e->getMessage() ?: 'Access denied!';
return new Response($message, 403);
});
// Handle Resource not found errors
$app->error(function (\App\Rest\Exception\ResourceNotFound $e) {
$message = $e->getMessage() ?: 'Resource not found!';
return new Response($message, 404);
});
// Handle other exception as 500 errors
$app->error(function (\Exception $e, $code) {
return new Response($e->getMessage(), $code);
});
Problem is that when I throw a ResourceNotFound exception in my controller, the errorhandler tied to AccessDenied is executed
Catchable fatal error: Argument 1 passed to {closure}() must be an instance of App\Rest\Exception\AccessDenied, instance of App\Rest\Exception\ResourceNotFound given
Is this achievable in another way or should I just stuff everything in the handler that works with generic Exceptions and switch on the type of exception thrown?
PS: i'm aware of the $app->abort() method but prefer working with exceptions
EDIT: This feature has now made it into Silex core!
This is currently not possible. Right now you'd have to either have a single handler with a switch statement, or many handlers with an if ($e instanceof MyException) each.
I do like the idea though, and it should be possible to implement it by using reflection. It would be awesome if you could create a new ticket on the tracker, or even work on a patch, if you're interested.
Cheers!
Another solution that I use in my projects:
class ProcessCallbackException extends Exception
{
public function __construct(\Closure $callback, $message = "", Exception $previous = null)
{
parent::__construct($message, 0, $previous);
$this->callback = $callback;
}
public $callback;
}
class AccessDeniedException extends ProcessCallbackException
{
public function __construct($message = null)
{
$f = function() {
return app()->redirect('/login');
};
parent::__construct($f, $message);
}
}
# Handle your special errors
$app->error(function (\Exception $e, $code) {
if ($e instanceof ProcessCallbackException)
{
/** #var ProcessCallbackException $callbackException */
$callbackException = $e;
return call_user_func($callbackException->callback);
}
else
return null;
});