I am trying to implement the best way to handle database errors. I am using a mvc framework (codeigniter). When I go to create my object, I end up making multiple queries to different tables.
The problem I have is if the first query is successful, and the second fails, I get an error. However, the first query is still successful and the data is already in that table.
What I want to do is wrap all of my queries in a try block, and that way, none of the queries will be completed if any of them fail.
Is there a better way to handle this situation (perhaps codeigniter specific), by rolling back the changes?
A try block doesn't do that directly....
You need tables that support transactions i.e. InnoDB tables
First, you need to change the database engines of your database tables to InnoDB (if they're MyISAM anyways).
Before the database operation, you then need to start a transaction (check online for your method that suits you). I use PDO so I'd normally do this:
$pdoObject->startTransaction();
So from the return values of you queries, if it succeeds you continue to the next query, else you'll do a rollback() and end execution. It's at this point your try...catch could be useful because you could decide to throw an Exception in the event that a query execution failed. You catch it and do a rollback()
If all succeed you need to do a commit() else the changes won't be reflected
NOTE: Only InnoDB tables support transactions in MySQL
What you're looking for is called transactions.
You would have to make sure that all of your tables use InnoDB though.
if ($this->db->trans_status() !== false) {
// do stuff
}
Is what I use to verify the transaction. Note that you can also send a boolean for test mode:
$this->db->trans_start(true);
Related
So, let's say I'm using two drivers at the same time (in the specific mysql and sqlite3)
I have a set of changes that must be commit()ted on both connections only if both dbms didn't fail, or rollBack()ed if one or the another did fail:
<?php
interface DBList
{
function addPDO(PDO $connection);
// calls ->rollBack() on all the pdo instances
function rollBack();
// calls ->commit() on all the pdo instances
function commit();
// calls ->beginTransaction() on all the pdo instances
function beginTransaction();
}
Question is: will it actually work? Does it make sense?
"Why not use just mysql?" you would say! I'm not a masochist! I need mysql for the classic fruition via my application, but I also need to keep a copy of a table that is always synchronized and that is also downloadable and portable!
Thank you a lot in advance!
I suspect you put the cart before the horses! If
two databases are in sync
a transaction commits successfully on one DB
No OS-level error occures
then the transaction will also commit successully on the second DB.
So what you would want to do is:
- Start the transaction on MySQL
- Record all data-changing SQL (see later)
- Commit the transaction on MySQL
- If the commit works, run the recorded SQL against SQlite
- if not, roll back MySQL
Caveat: The assumption above is only valid, if the sequence of transactions is identical on both DBs. So you would want to record the SQL into a MySQL table, which is subject to the same transaction logic as the rest. This does the serialization work for you.
You mistake PDO with a database server. PDO is just an interface, pretty much like the database console. It doesn't perform any data operations of its own. It cannot insert or select data. It cannot perform data locks or transactions. All it can do is to send your command to database server and bring back results if any. It's just an interface. It doesn't have transactions on it's own.
So, instead of such fictional trans-driver transactions you can use regular ones.
Start two, one for each driver, and then rollback them accordingly. By the way, with PDO one don't have to rollback manually. Just set PDO in exception mode, write your queries and add commit at the end. In case one of queries failed, all started transactions will be rolled back automatically due to script termination.
The ZF1 Zend_Db reference manual has an entire section on performing transactions.
The ZF2 Zend\Db reference manual lacks any documentation on transactions.
How do I perform transactions in ZF2? Example code would be helpful.
You've got it. The proper way to Begin, Commit, and Rollback Transactions is as follows:
$this->getAdapter()->getDriver()->getConnection()->beginTransaction();
$this->getAdapter()->getDriver()->getConnection()->commit();
$this->getAdapter()->getDriver()->getConnection()->rollback();
Just to put this out there too you can also get the Last ID created by:
$this->getAdapter()->getDriver()->getConnection()->getLastGeneratedValue()
If you are using pgSQL you will need to add the sequence to return the Last ID created:
$this->getAdapter()->getDriver()->getConnection()->getLastGeneratedValue('mail_mailid_seq')
The missing documentation is curious.
To find out what happened, I had to dive into the API docs for Zend\Db\Adapter.
It looks like beginTransaction, rollback and commit are defined in Zend\Db\Adapter\Driver\ConnectionInterface. This means that they are methods callable on every single adapter connection. Unfortunately the connection itself is rather buried.
What I'm not clear on -- and can't provide an example for at this time -- is figuring out which object you actually call these methods on. In the worst case, it looks like you might want to call $adapter->getDriver()->getConnection()->beginTransaction().
Eww.
I'm hoping someone else with more knowledge, and a copy of ZF2 handy, will see this and provide a better option.
Don't forget that you can just issue BEGIN TRANSACTION/ROLLBACK/COMMIT/SET autocommit=... SQL statements yourself. This is probably OK, as it doesn't look like Zend\Db keeps track of the transaction state.
There are two matter for doing transaction.
1 - MyISAM is not a transactional engine , so change tables engine to InnoDB.
2 - Transaction query("START TRANSACTION;" OR "ROLLBACK;") connection must be same with other queries(Insert or Update).
For doing this in ZF2 you should get current db adapter and use it in all queries.
This code will not work correctly :
$this->getAdapter()->getDriver()->getConnection()->beginTransaction();
//do some jobs - e.g : multiple tables update or insert.
$this->getAdapter()->getDriver()->getConnection()->rollback();
Since $this->getAdapter()->getDriver()->getConnection() Creates new db connection.
Use following code instead:
$connection = $this->getAdapter()->getDriver()->getConnection();
$connection->beginTransaction();
//do some jobs - e.g : multiple tables update or insert.
$connection->rollback();
For check if your connections is correct , just enable query log in mysql.
After running query you will see connection number before each query in mysql log.Those must be same in all transaction queries.
I used beginTransaction, rollback and commit in controller.
where I performed number of transactions on different models where I using predefined functions without any control transactions (not necessary for single DB transaction).
Using $this->getAdapter()->getDriver()->getConnection()->beginTransaction();
gives error on undefined getAdapter() methods.
So I perform following way,
//begain tarnsaction
$db = Zend_Db_Table_Abstract::getDefaultAdapter();
$db->beginTransaction();
//rollback
$db->rollback();
//commit db changes
$db->commit();
Hope it may useful to solve problem.
When a deadlock situation occurs in MySQL/InnoDB, it returns this familiar error:
'Deadlock found when trying to get lock; try restarting transaction'
So what i did was record all queries that go into a transaction so that they can simply be reissued if a statement in the transaction fails. Simple.
Problem: When you have queries that depend on the results of previous queries, this doesn't work so well.
For Example:
START TRANSACTION;
INSERT INTO some_table ...;
-- Application here gets ID of thing inserted: $id = $database->LastInsertedID()
INSERT INTO some_other_table (id,data) VALUES ($id,'foo');
COMMIT;
In this situation, I can't simply reissue the transaction as it was originally created. The ID acquired by the first SQL statement is no longer valid after the transaction fails but is used by the second statement. Meanwhile, many objects have been populated with data from the transaction which then become obsolete when the transaction gets rolled back. The application code itself does not "roll back" with the database of course.
Question is: How can i handle these situations in the application code? (PHP)
I'm assuming two things. Please tell me if you think I'm on the right track:
1) Since the database can't just reissue a transaction verbatim in all situations, my original solution doesn't work and should not be used.
2) The only good way to do this is to wrap any and all transaction-issuing code in it's own try/catch block and attempt to reissue the code itself, not just the SQL.
Thanks for your input. You rock.
A transaction can fail. Deadlock is a case of fail, you could have more fails in serializable levels as well. Transaction isolation problems is a nightmare. Trying to avoid fails is the bad way I think.
I think any well written transaction code should effectively be prepared for failing transactions.
As you have seen recording queries and replaying them is not a solution, as when you restart your transaction the database has moved. If it were a valid solution the SQL engine would certainly do that for you. For me the rules are:
redo all your reads inside the transactions (any data you have read outside may have been altered)
throw everything from previous attempt, if you have written things outside of the transaction (logs, LDAP, anything outside the SGBD) it should be cancelled because of the rollback
redo everything in fact :-)
This mean a retry loop.
So you have your try/catch block with the transaction inside. You need to add a while loop with maybe 3 attempts, you leave the while loop if the commit part of the code succeed. If after 3 retry the transaction is still failing then launch an Exception to the user -- so that you do not try an inifinite retry loop, you may have a really big problem in fact --. Note that you should handle SQL error and lock or serializable exception in different ways. 3 is an arbitrary number, you may try a bigger number of attempts.
This may give something like that:
$retry=0;
$notdone=TRUE;
while( $notdone && $retry<3 ) {
try {
$transaction->begin();
do_all_the_transaction_stuff();
$transaction->commit();
$notdone=FALSE;
} catch( Exception $e ) {
// here we could differentiate basic SQL errors and deadlock/serializable errors
$transaction->rollback();
undo_all_non_datatbase_stuff();
$retry++;
}
}
if( 3 == $retry ) {
throw new Exception("Try later, sorry, too much guys other there, or it's not your day.");
}
And that means all the stuff (read, writes, fonctionnal things) must be enclosed in the $do_all_the_transaction_stuff();. Implying the transaction managing code is in the controllers, the high-level-application-functional-main code, not split upon several low-level-database-access-models objects.
I'm sorry, this is a very general question but I will try to narrow it down.
I'm new to this whole transaction thing in MySQL/PHP but it seems pretty simple. I'm just using mysql not mysqli or PDO. I have a script that seems to be rolling back some queries but not others. This is uncharted territory for me so I have no idea what is going on.
I start the transaction with mysql_query('START TRANSACTION;'), which I understand disables autocommit at the same time. Then I have a lot of complex code and whenever I do a query it is something like this mysql_query($sql) or $error = "Oh noes!". Then periodically I have a function called error_check() which checks if $error is not empty and if it isn't I do mysql_query('ROLLBACK;') and die($error). Later on in the code I have mysql_query('COMMIT;'). But if I do two queries and then purposely throw an error, I mean just set $error = something, it looks like the first query rolls back but the second one doesn't.
What could be going wrong? Are there some gotchas with transactions I don't know about? I don't have a good understanding of how these transactions start and stop especially when you mix PHP into it...
EDIT:
My example was overly simplified I actually have at least two transactions doing INSERT, UPDATE or DELETE on separate tables. But before I execute each of those statements I backup the rows in corresponding "history" tables to allow undoing. It looks like the manipulation of the main tables gets rolled back but entries in the history tables remain.
EDIT2:
Doh! As I finished typing the previous edit it dawned on me...there must be something wrong with those particular tables...for some reason they were all set as MyISAM.
Note to self: Make sure all the tables use transaction-supporting engines. Dummy.
I'd recommend using the mysqli or PDO functions rather than mysql, as they offer some worthwhile improvements—especially the use of prepared statements.
Without seeing your code, it is difficult to determine where the problem lies. Given that you say your code is complex, it is likely that the problem lies with your code rather than MySQL transactions.
Have you tried creating some standalone test scripts? Perhaps you could isolate the SQL statements from your application, and create a simple script which simply runs them in series. If that works, you have narrowed down the source of the problem. You can echo the SQL statements from your application to get the running order.
You could also try testing the same sequence of SQL statements from the MySQL client, or through PHPMyAdmin.
Are your history tables in the same database?
Mysql transactions only work using the mysqli API (not the classic methods). I have been using transactions. All I do is deactivate autocommit and run my SQL statements.
$mysqli->autocommit(FALSE);
SELECT, INSERT, DELETE all are supported. as long as Im using the same mysqli handle to call these statements, they are within the transaction wrapper. nobody outside (not using the same mysqli handle) will see any data that you write/delete using INSERT/DELETE as long as the transaction is still open. So its critical you make sure every SQL statement is fired with that handle. Once the transaction is committed, data is made available to other db connections.
$mysqli->commit();
I have 2 tables:
user_tb.username
user_tb.point
review_tb.username
review_tb.review
I am coding with PHP(CodeIgniter). So I am trying to insert data into review_tb with the review the user had submitted and if that is a success, i will award the user with some points.
Well this look like a very simple process. We will first insert the review into the review_tb with the username and use PHP to check if there is any problem with the query executed and if it's a success, we will proceed with updating the points in the user_tb.
Yea, but here comes the problem. What if inserting into review_tb is a success but the second query, inserting into the user_tb is NOT a success, can we kind of "undo" the review_tb query or "revert" the change that we did to review_tb.
It's kind of like "all or nothing".
The purpose of this is to sync all data across the database, where in real life, we will be managing a database of more tables, and inserting more data into each table which depends on each other.
Please give some enlightenment on how we can do this in PHP or CodeIgniter or just MySql query.
If you want a "all or nothing" behavior for your SQL operations, you are looking for transactions ; here is the relevant page from the MySQL manual : 12.4.1. START TRANSACTION, COMMIT, and ROLLBACK Syntax.
Wikipedia describes those this way :
A database transaction comprises a
unit of work performed within a
database management system (or
similar system) against a database,
and treated in a coherent and reliable
way independent of other transactions.
Transactions in a database environment
have two main purposes:
To provide reliable units of work that allow correct recovery from
failures and keep a database
consistent even in cases of system
failure, when execution stops
(completely or partially) and many
operations upon a database remain
uncompleted, with unclear status.
To provide isolation between programs accessing a database
concurrently. Without isolation the
programs' outcomes are typically
erroneous.
Basically :
you start a transaction
you do what you have to ; ie, your first insert, and your update
if everything is OK, you commit the transaction
else, if there is any problem with any of your queries, you rollback the transaction ; and it will cancel everything you did in that transaction.
There is a manual page about transactions and CodeIgniter here.
Note that, with MySQL, no every Engine supports transaction ; between the two most used engines, MyISAM doesn't support transactions, while InnoDB supports them.
Can't you use transactions? If you did both inserts inside the same transaction, then either both succeed or neither does.
Try something like
BEGIN;
INSERT INTO review_tb(username, review) VALUES(x, y);
INSERT INTO user_tb(username, point) VALUES(x, y);
COMMIT;
Note that you need to use a database engine that supports transactions (such as InnoDB).
If you have InnoDB support use it, but when its not possible you can use a code similar to the following:
$result=mysql_query("INSERT INTO ...");
if(!$result) return false;
$result=mysql_query("INSERT INTO somewhereelse");
if(!$result) {
mysql_query("DELETE FROM ...");
return false;
}
return true;
This cleanup might still fail, but can work whenever the insert query fails because of duplicates or constraints. For unexpected terminations, only way is to use transactions.