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
Related
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.
Currently I have an insert query to my table and I don't have any idea how I can validate it, if the data is already exists in the table.
So for now I just used try-catch to handle the duplicate entry. I just wondering if I could return some text If value goes to catch then display it to my view as alert or something.
Here is my query from controller:
public ActionResult AddUser(int id, string name, string age)
{
string constr = ConfigurationManager.ConnectionStrings["ConString"].ConnectionString;
using (MySqlConnection con = new MySqlConnection(constr))
{
string sqlQuery = "INSERT INTO myTable (id, name, age) VALUES (#id, #name, #age)";
MySqlCommand cmd = new MySqlCommand(sqlQuery, con);
cmd.Parameters.AddWithValue("#id", id);
cmd.Parameters.AddWithValue("#name", name);
cmd.Parameters.AddWithValue("#age", age);
con.Open();
try {
cmd.ExecuteNonQuery();
con.Close();
return RedirectToAction("Index");
}
catch (Exception )
{
con.Close();
return this.Json("This Data already exist on table");
}
}
}
And this is my script to read my controller query:
function add(id, name, age) {
var result = confirm("Are you want to add " + name + " to list?");
if (result == true) {
$.ajax({
url: '/Home/AddUser',
type: 'POST',
data: {
'id': id,
'name': name,
'age': age,
},
success: function (data) {
alert('Data has been successfully added');
},
error: function (jqXhr, textStatus, errorThrown) {
alert(errorThrown);
}
});
}
}
So far I can successfully insert it to my table but if there is already exist and it's a duplicate it will do nothing so the user will not have a prompt if they already added it or already exist or not.
Any suggestions or comments. TIA.
Consider creating a constraint in your database table (unique constraint on that column(s)). That will prevent any duplicate records to be saved even if it passes your C# code which is talking to the database.
Add a check to see whether the record exist, just before executing the INSERT statement. You can write a SQL statement which checks before the insertion step. May be a stored procedure which has this SQL scripts which does this and you may call the stored procedure from your c# method.
There is no point in returning a redirect response if you are calling this from Ajax code. Consider returning a JSON structure which can tell the client side code whether the record was successfully inserted or duplicate found or code crashed.
Here is a quick and simple example, where I am calling the UserExist method, which checks whether there is a record with the specified name. If it returns false, I continue to execute my code where I will try to insert. If you are using a stored procedure, you can add the record exist check inside that as well.
[HttpPost]
public ActionResult AddUser(int id, string name, string age)
{
try
{
// your existing code to insert record to db
// Check record exist
if(UserExist(name))
{
return Json(new { status="failed", message = "Name exist"});
}
// name does not exist. Try to call the Insert code now.
return Json(new { status="success", message = "Successfully saved"});
}
catch (SqlException exs)
{
// Check exs to see whether this was caused by unique constraint violation
return Json(new { status="error", message = "User exist"});
}
catch (Exception ex)
{
// to do : log the exception
return Json(new { status="error", message = "Error in saving"});
}
}
private bool UserExist(string name)
{
// to do: check record exist in db
// You may use ExecuteScalar method if you use raw ADO.NET
// to do : return boolean value.
}
and in your success handler, check the status property of the response json and show the user the appropriate message
success:function(data)
{
if(data.status==="success")
{
alert("Saved successfully");
}
else if(data.status==="failed")
{
alert(data.message);
}
}
You can set the status property of your JSON object to failed when you are trying to insert duplicate record.
You can use specific exceptions for your exception handling part which can catch the exception when the unique key constraint is violated in the database level. Send a message to user in that catch block as well.
The first step you can do is creating a stored procedure which has output parameters to show insertion status, which must be different when duplicate data is found:
DELIMITER //
CREATE PROCEDURE AddUser(in #id int, #name varchar(50), #age int, out #status varchar(20))
AS
BEGIN
-- check if duplicate exists
IF (SELECT EXISTS (SELECT 1 FROM myTable WHERE name = #name))
BEGIN
-- duplicate exist, no insertion to table
SET #status = 'duplicate';
END
ELSE
BEGIN
INSERT INTO myTable (id, name, age) VALUES (#id, #name, #age)
SET #status = 'success';
END
END
END
//
DELIMITER ;
Then, use the stored procedure name inside MySqlCommand and use its output parameter to return the status string:
[HttpPost]
public ActionResult AddUser(int id, string name, string age)
{
string constr = ConfigurationManager.ConnectionStrings["ConString"].ConnectionString;
using (MySqlConnection con = new MySqlConnection(constr))
{
string sqlQuery = "AddUser";
MySqlCommand cmd = new MySqlCommand(sqlQuery, con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#id", id);
cmd.Parameters.AddWithValue("#name", name);
cmd.Parameters.AddWithValue("#age", age);
cmd.Parameters.Add("#status", MySqlDbType.VarChar).Direction = ParameterDirection.Output;
con.Open();
try
{
cmd.ExecuteNonQuery();
con.Close();
return Json(new { status = (string)cmd.Parameters["#status"].Value });
}
catch (MySqlException ex)
{
con.Close();
return Json(new { status = "error", message = ex.Message });
}
catch (Exception e)
{
con.Close();
return Json(new { status = "error", message = e.Message });
}
}
}
Then you can output messages depending on current status in AJAX callback:
$.ajax({
url: '/Home/AddUser',
type: 'POST',
data: {
'id': id,
'name': name,
'age': age,
},
success: function (data) {
if (data.status === "success")
{
alert("Data has been successfully added");
}
else if (data.status === "duplicate")
{
alert("This Data already exist on table");
}
else if (data.status === "error")
{
alert(data.message);
}
}
error: function (xhr, status, err) {
// error handling
}
}
If you don't want to check with SELECT query like above, consider altering the table by applying UNIQUE constraint and check against error code 1062 in MySqlException:
ALTER TABLE myTable ADD CONSTRAINT UNIQUE (name);
I have this sql statement:
selectAllUsersByCriteria = connection.prepareStatement(
"SELECT * FROM Users WHERE ? = ?" );
And the follow method running the statement:
public ArrayList<User> getUsersByCriteria(String 1criteria, String 2criteria)
{
ArrayList<User> results = null;
ResultSet resultSet = null;
try
{
selectAllUsersByCriteria.setString( 1, 1criteria);
selectAllUsersByCriteria.setString( 2, 2criteria);
// executeQuery returns ResultSet containing matching entries
resultSet = selectAllUsersByCriteria.executeQuery();
results = new ArrayList< User >();
while ( resultSet.next() )
{
results.add( new User( resultSet.getString( "userName" ),
resultSet.getString( "Password" ),
resultSet.getBoolean( "AdminRights" ),
resultSet.getDouble( "Balance" )
) );
} // end while
} // end try
catch ( SQLException sqlException )
{
sqlException.printStackTrace();
} // end catch
finally
{
try
{
resultSet.close();
} // end try
catch ( SQLException sqlException )
{
sqlException.printStackTrace();
close();
} // end catch
} // end finally
return results;
}
It doesn't work. I figure it is the first ? that is the issue. Isn't it possible to set the WHERE ? as a ?. Can it be solved in another way.
It is a table I want to show, but it should only be show the users follow it meet the two criteria.
You would need to inject the column name directly into the string. That would open you up to a SQL injection attack, so I'd recommend querying (and probably caching) the table's schema info (specifically found in INFORMATION_SCHEMA.COLUMNS).
This way you can make sure that your user-submitted column name matches one of the column names in your table before injecting it into the script by seeing if it's in your list of available columns.
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.
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);