Here is my script:
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
try{
$db_conn->beginTransaction();
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
All I want to know, can I use an if statement in the codes which are between beginTransaction() and commit() ? Something like this:
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
try{
$db_conn->beginTransaction();
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
if (/* a condition here */){
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
Can I ?
Actually I asked that because here is a sentence which says you can't and doing that is dangerous:
Won't work and is dangerous since you could close your transaction too early with the nested commit().
There is no problem with your transaction structure. The comment on php.net only means, that MySQL does not support nested transactions. In order to your further question, you can query any data (SQL), manipulate data (DML), but not modify any database structures (DDL - data definition language).
/*won't work*/
START TRANSACTION;
/*statement*/
START TRANSACTION; /*nested not supported, auto commit*/
/*statement*/
COMMIT;
/*statement dependend on 1st transaction won't work*/
COMMIT;
See also MySQL ref
Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement or one of its synonyms.
You can do everything within a transaction, the only thing you cannot do is nest transactions.
Not the if clause itself is the problem in your linked comment, but the fact there is another beginTransaction / commit pair inside.
Related
More transaction questions!
What I have right now is a mess of strung-together queries, that are all manually reversed if any fail:
Code Block 1
$stmt1 = $db->prepare(...); // Update table1, set col=col+1
if($stmt1 = $db->execute(...)){
$stmt2 = $db->prepare(...); // Insert into table2, id=12345
if($stmt2 = $db->execute(...)){
$stmt3 = $db->prepare(...); // Select val from table3
if($stmt3 = $db->execute(...)){
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
$stmt4 = $db->prepare(...); // Update table4, set status=2
if($stmt4 = $db->execute(...)){
return true;
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
$stmt2 = $db->prepare(...); // Delete from table2, where id=12345 (opposite of above)
$stmt2 = $db->execute(...);
return false;
}
}
return true;
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
$stmt2 = $db->prepare(...); // Delete from table2, where id=12345 (opposite of above)
$stmt2 = $db->execute(...);
return false;
}
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
return false;
}
}
It's a mess, difficult to debug, difficult to add on to, difficult to understand when the queries are large, and won't return all tables back to original state if the connection is lost mid-way through.
This same process is even worse when deleting a row, because everything in it needs to be stored - just in case the operation needs to be undone.
Now, I know most of this will still work when I port it over to a single transaction, but the one part I am unsure of is:
Code Block 2
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
... continue ...
}else{
... reverse operations ...
return false;
}
Because the results-gathering would take place before the commit() in the transaction. As follows:
Code Block 3
$db->beginTransaction();
try{
$stmt1 = $db->prepare(...);
$stmt1->execute();
$stmt2 = $db->prepare(...);
$stmt2->execute();
$stmt3 = $db->prepare(...);
$stmt3->execute();
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
$stmt4 = $db->prepare(...);
$stmt4->execute();
}else{
$db->rollBack();
return false;
}
$db->commit();
return true;
}catch(Exception $e){
$db->rollBack();
throw $e;
return false;
}
Will this work? Specifically, can I include the $result = $stmt3->fetchAll(); before the commit(), and then execute the conditional query?
Also, I'm not entirely sure on this, but do I require the $db->rollBack(); within the try block, if the code is exited (return false) before the commit()?
Your first question:
Specifically, can I include the $result = $stmt3->fetchAll(); before the commit(), and then execute the conditional query?
I see no reason why it should not work. A transaction behaves basically the same as operations without transactions - except that changes are only drafts. Any changes you make in the previous statements will be applied to a "working copy" valid for this single session only. For you it will appear completely transparent. However any changes will be rolled back if you do not commit them.
Also worth noting (emphasis mine):
In layman's terms, any work carried out in a transaction, even if it is carried out in stages, is guaranteed to be applied to the database safely, and without interference from other connections, when it is committed.
This can cause racing conditions.
Your second question:
Also, I'm not entirely sure on this, but do I require the $db->rollBack(); within the try block, if the code is exited (return false) before the commit()?
From the documentation it says:
When the script ends or when a connection is about to be closed, if you have an outstanding transaction, PDO will automatically roll it back.
Therefore you do not necessarily require to roll back manually as it will be done by the driver itself.
However note the following from the same source as well:
Warning PDO only checks for transaction capabilities on driver level. If certain runtime conditions mean that transactions are unavailable, PDO::beginTransaction() will still return TRUE without error if the database server accepts the request to start a transaction.
So be sure to check the compatibility beforehand!
A few notes
Do NOT begin a transaction in another transaction. This will commit the first transaction implicitely. See this comment.
Another note from the documentation:
Some databases, including MySQL, automatically issue an implicit COMMIT when a database definition language (DDL) statement such as DROP TABLE or CREATE TABLE is issued within a transaction. The implicit COMMIT will prevent you from rolling back any other changes within the transaction boundary.
Here is my script:
try{
$db_conn->beginTransaction();
$stm1 = $db_conn->prepare("UPDATE table1 SET col = 'updated' WHERE id = ?");
$stm1->execute(array($value));
$done = $stm->rowCount();
if ($done){
try {
$stm2 = $db_conn->prepare("INSERT into table2 (col) VALUES (?)");
$stm2->execute(array($id));
} catch(PDOException $e){
if ((int) $e->getCode() === 23000) { // row is duplicate
$stm3 = $db_conn->prepare("DELETE FROM table2 WHERE col = ?");
$stm3->execute(array($id));
} else {
$db_conn->rollBack(); -- this
}
}
} else {
$error = true;
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
As you see there is a rollBack() before commit() (where I've commented by -- this). Well is what I'm doing correct? Or that rollBack() is useless?
Note: that DELETE statement acts as an undo. Suppose you give a vote to a post and you want to take it back. So that DELETE statement remove it if you send a vote twice.
You dont need to make it quite so complicated.
You can run the 2 queries within a single try/catch as any of the queries that has an isse will throw an exception, and then you can do a single rollback.
If the first query fails, the database will not be changed, the rollback will just close the transaction. If the second query fails the rollback will UNDO the first query i.e. the UPDATE you did previously.
If both queries complete OK, the commit will apply the changes to the database.
try{
$db_conn->beginTransaction();
$stm1 = $db_conn->prepare("UPDATE table1 SET col = 'updated' WHERE id = ?");
$stm1->execute(array($value));
$stm2 = $db_conn->prepare("INSERT into table2 (col) VALUES (?)");
$stm2->execute(array($id));
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
ADDITIONAL NOTES
I see what you think you were trying to do. BUT!
If the INSERT fails with a 23000 error code, then the INSERT will not have been done. Your unnecessary attempt to delete the failed INSERT will actually DELETE the row that was there originally i.e. the original vote that should not be deleted!
Your code would work fine. You can get rid of your inner rollback if you would rethrow your error (that would be the "standard way"):
try {
...
if ($done){
try {
...
} catch(PDOException $e){
if ((int) $e->getCode() === 23000) { // row is duplicate
...
} else {
throw $e;
}
}
...
} else {
$error = true;
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
In this case, your final rollback will handle all exceptions that should be rolled back, while handling the one exception 23000 yourself without rollback.
And it would work fine in your code too. Technically, you can in fact combine as many commits and rollbacks as you want without resulting in an error - if you commit after a rollback, it will just commit nothing, because it rolled back already. If you use rollback without start transaction (in autocommit mode), it will just do nothing. It is just a little harder to maintain the code and to see the strcuture, that's why you usually would use the "standard way" above.
There is just one important thing to consider: transactions in mysql are not nested. If you use start transaction, it will automatically do a commit before that. So e.g.
start transaction;
delete from very_important_table;
start transaction; -- will do an autocommit
rollback;
rollback; -- will have no effect;
commit; -- will have no effect;
rollback; -- will have no effect;
will not rollback your very_import_table, because the 2nd start transaction already committed it.
I have a script which is containing some queries:
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
As you see there is two statements (insert and update). All I'm trying to do is making sure both of them work or none of them.
I mean I want to implement a dependency between those two statements. If updating fails, then inserting shouldn't work and vice versa. How can I do that?
You could use sql transactions
http://www.sqlteam.com/article/introduction-to-transactions
You can use transactions and PDO has an api for this (http://php.net/manual/en/pdo.begintransaction.php),
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
try{
$db_conn->beginTransaction();
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
As others said, you could use 'transactions'.
Or
you could mannualy check whether the data is right in the database. Just 'select' what you have inserted.
The 'execute' function return 'true' on success or 'false' on failure. You can do something like:
$isDone=$stm1->execute(array($value));
if(!$isDone){
echo 'Operation fails, I will stop.';
return false;
}
Following is my code in which I am making insertion in two different tables. My question is if due to bad server or bad connection or the like. The insertion to the first table being made but not to the second one. Then does the following code will rollback the insertion being done for the the first table and if not then how that can be done?
Note: My table type in Innodb
$mysqli->autocommit(FALSE);
$stmt_one = $mysqli->prepare("Insert into TABLE SET $var1 = ?, $var2 = ? ");
$stmt_two = $mysqli->prepare("Insert into TABLE SET $var1 = ?, $var2 = ? ");
$stmt_one->bind_param('is',$var1,$var2);
$stmt_two->bind_param('is',$var1,$var2);
$stmt_one->execute();
$stmt_two->execute();
$mysqli->commit();
$stmt_one->close();
$stmt_two->close();
Look at:
try {
$mysqli->autocommit(FALSE);
$stmt_one = $mysqli->prepare("Insert into TABLE SET $var1 = ?, $var2 = ? ");
$stmt_two = $mysqli->prepare("Insert into TABLE SET $var1 = ?, $var2 = ? ");
$stmt_one->bind_param('is',$var1,$var2);
$stmt_two->bind_param('is',$var1,$var2);
$stmt_one->execute();
$stmt_two->execute();
$mysqli->commit();
$stmt_one->close();
$stmt_two->close();
} catch (Exception $e) {
$mysqli->rollBack();
echo "Fallo: " . $e->getMessage();
}
For starting transaction you need turn off auto-committing DB modifications. You do it via $mysqli->autocommit(FALSE);. It means that all queries-changes DB must be "fixed" for "finish" transaction. Its doing via $mysqli->commit();. While you don't calling commit ("fix") - there will be no changes in DB.
In your case ( inserting into 2 tables ): if the insertion to the first table being made but not to the second , will be invoked exception with $mysqli->rollBack();. Since we have not caused сommit transaction (due to exception ), all changes will be roll back to start point (turning off auto-committing), in other words, we do rollback current transaction.
I'm using PDO in my web application. In a part of this application, I need to work with PDO Transactions. I need to know last inserted id of first query and use it in the second query, and then if no problem occurs, I will commit this transaction.
The problem I have is that how can I find out last inserted id before transaction commit?
This is a sample of my need :
$db->beginTransaction();
$stmt1 = "INSERT ..."; // An insert query
$q = $db->prepare($stmt1);
$q->execute(array());
$last = $db->lastInsertId();
$stmt2 = "UPDATE ..."; // An update query
$q2 = $db->prepare($stmt2);
$q2->execute(array($last));
$db->commit();