I'm looking into rollback management with MySQLi, and I'm curious what is the benefit of begin_transaction() method. Many examples I look at skip it entirely by turning autocommit off, then executing some queries with success value returned, and testing a compound Boolean based on the return values to commit or rollback the multiple statements.
It doesn't seem like the begin_transaction() method actually does any useful work in a scenario where we are looking to commit or rollback a group of queries based on the success of all of them. I can see that it adds readability to the code perhaps by explicitly declaring a transaction, but is there a value to begin_transaction() other than in readability? What real work does it do?
As already mentioned in other answers, begin_transaction() starts a transaction, without affecting the value of autocommit. The queries are not commited into the database until you call commit() or trigger an implicit commit.
As described in MySQL docs:
A session that has autocommit enabled can perform a multiple-statement transaction by starting it with an explicit START TRANSACTION or BEGIN statement and ending it with a COMMIT or ROLLBACK statement. See Section 13.3.1, “START TRANSACTION, COMMIT, and ROLLBACK Statements”.
If autocommit mode is disabled within a session with SET autocommit = 0, the session always has a transaction open. A COMMIT or ROLLBACK statement ends the current transaction and a new one starts.
This means that the main difference between begin_transaction() and autocommit(FALSE); is whether you want a one-time transaction or whether you want contiguous transactions.
A simple one-time transaction using begin_transaction() would look like this:
<?php
// Switch on error reporting with exception mode
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'username', 'password', 'db_name');
$mysqli->set_charset('utf8mb4');
try {
// Start transaction
$mysqli->begin_transaction();
$mysqli->query('INSERT INTO some_table(col2) VALUE(4)');
$mysqli->query('INSERT INTO some_table(col2) VALUE(4');
// Commit changes
$mysqli->commit();
} catch (\Throwable $e) {
// Something went wrong. Rollback
$mysqli->rollback();
throw $e;
}
Such approach is clearer than switching the autocommit mode off altogether. .
Only thing that begin_transaction does in contrast with autocommit off is that it does not mess up with autocommit value, so after you commit/rollback transaction stared with begin_transaction the autocommit will be the same as it was before.
I always get tripped up using mysqli transactions in PHP because many examples that cover this topic throw in the method of turning off autocommit. While that works, it's not the same as using mysqli transactions. At least, not in the traditional sense where one would normally key in 'START TRANSACTION' in the query. Here is an example of using a mysqli transaction in PHP without messing with the autocommit setting. If anything, this is a reminder for myself if I ever forget how to do this.
$dbo->begin_transaction();
//Below are the 2 sample queries we need to run; each one will insert a row into a separate table
$result1 = $dbo->query($query1);
$result2 = $dbo->query($query2);
//If both queries were successful, commit
if ($result1 && $result2)
{
$dbo->commit();
}
//Else, rollback and throw error
else
{
$dbo->rollback();
echo 'Writing to DB failed.';
}
As others mention, it is somewhat influenced by your preference. With begin_transaction, you don't have to deal with toggling autocommit before and after your queries.
Related
i wonder how does transaction work.
Question is do I need to rollback if any error/exception occurs, where the Exception will be thrown and never reach the commit() call? If so why and how? and what rollback does exactly?
consider the followings PHP code (some Java code does similar things like so):
public function deleteAll():void{
$stmt = $this->con->prepare(
'DELETE FROM dd WHERE 1'
);
$this->con->begin_transaction();
if(!$stmt->execute()){
throw new Exception($stmt->error);
}
$this->con->commit();
}
public function insert(array $list):void{
$stmt = $this->con->prepare(
'INSERT INTO dd(cc) VALUES(?)'
);
$stmt->bind_param('s', $v);
$this->con->begin_transaction();
foreach($list as $v){
if(!$stmt->execute()){
throw new Exception($stmt->error);
}
}
$this->con->commit();
}
Note:
$this->con is the mysqli connection object (not using PDO, although different but PDO and mysqli will do similar thing like above).
database is MySQL, table using InnoDB engine.
Some people code shows they catch the Exception and do rollback. But I did not since the commit() is never executed if Exception is thrown. Then this let me wonder why does it need to rollback at all.
My assumption is if there are multiple queries to perform at once like the following:
public function replaceAllWith(array $list):void{
$this->con->begin_transaction();
try{
//do both previous queries at once
$this->deleteAll();
$this->insert($list);
$this->con->commit();
}catch(Exception $e){
//error
$this->con->rollback();
}
}
but then it still cannot rollback because each method/function is done and committed by different method and different transaction.
Second Question: Do I do each transaction in each method call, or only invoke transaction in the 'boss/parent' function which only call and coordinate 'subordinate/helper' functions to do work?
Not sure if I'm asking the right questions.
Database Transaction is confusing, many online resources show only the procedural process (commit only or then immediately rollback which make no differences to normal direct query) without OO concepts involved. Please any pros guide and give some advice. tq.
I've looked through resources for rollBack(), commit() and various transaction stuff, but I cannot find whether rollBack() can be called after commit() has already been called.
The situation is this:
I have two different databases: $dbu = new PDO(..db1..) and $dbb = new PDO(..db2..)
Both databases have tables that are being updated within a single function. The operation is all or none - either all tables are successfully updated, or none are.
Using two separate transactions, if the transaction for $dbu is successfully completed, but the transaction for $dbb fails, I have to undo what was done in the first transaction:
Code Block 1
$dbu->beginTransaction();
try{
$stmt = $dbu->prepare(...);
$stmt->execute();
// stuff
$dbu->commit();
}catch(Exception $e){
// do stuff
$dbu->rollBack();
exit();
}
$dbb->beginTransaction();
try{
$stmt = $dbb->prepare(...);
$stmt->execute();
// stuff
$dbb->commit();
}catch(Exception $e){
// do stuff
$dbb->rollBack();
// Need to undo what we did
$dbu->beginTransaction();
try{
$stmt = $dbu->prepare(...);
$stmt->execute();
// opposite of whatever operation was in the first transaction
$dbu->commit();
}catch(Exception $e){
}
exit();
}
This is messy, and unreliable if something happens to the connection in between the two primary transactions.
So what I'd like to do instead is nest the second transaction within the first. It seems logical that I'd be able to do this, because $dbu and $dbb are two unique PDO objects, that point to two separate databases. It looks like:
Code Block 2
$dbu->beginTransaction();
try{
$stmt = $dbu->prepare(...);
$stmt->execute();
// stuff
$dbb->beginTransaction();
try{
$stmt = $dbb->prepare(...);
$stmt->execute();
// stuff
$dbb->commit();
}catch(Exception $e){
// do stuff
$dbb->rollBack();
$dbu->rollBack(); // Since $dbu was first part of transaction, it needs to be rolled back too
exit();
}
$dbu->commit();
}catch(Exception $e){
// do stuff
$dbu->rollBack();
$dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
exit();
}
Since commit() for $dbu is called after the entire $dbb transaction, the case may arise where $dbb was successful, and $dbu failed. If that happens, I need to undo what was done in the $dbb transaction.
So...
Can I call $dbb->rollBack(); (near the end of Code Block 2) AFTER $dbb->commit(); has run? Or am I stuck in the same situation as I initially was, where I have to manually reverse whatever happened in the $dbb transaction? Again, this isn't ideal. If the connection drops in the middle of this, I could be left with data in the $dbb tables that shouldn't be there (because the $dbu transaction failed).
Perhaps I can combine the two transactions into a single try/catch block?
Code Block 3
$dbu->beginTransaction();
$dbb->beginTransaction();
try{
$stmt = $dbu->prepare(...);
$stmt->execute();
$stmt2 = $dbb->prepare(...);
$stmt2->execute();
// stuff
$dbu->commit();
$dbb->commit();
}catch(Exception $e){
// do stuff
$dbu->rollBack();
$dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
exit();
}
But this doesn't look a whole lot different than Code Block 2, because we can still have the situation where $dbu->commit(); is successful, but $dbb->commit(); fails. If that happens, then we are still trying to call $dbu->rollBack(); after its partner commit has already been processed.
If I cannot call rollBack() after commit(), is there a commonly used method to tackle this 2-DB problem? Something that is as efficient as rollBack() and doesn't require an entire extra transaction to undo the former operation.
EDIT 1
Adding on to Code Block 3, could I verify each execution as they are called?
Code Block 4
$dbu->beginTransaction();
$dbb->beginTransaction();
try{
$stmt = $dbu->prepare(...);
if(!$stmt->execute()){
throw new Exeption('something somethign');
}
$stmt2 = $dbb->prepare(...);
if(!$stmt2->execute()){
throw new Exeption('something two');
}
// stuff
$dbu->commit();
$dbb->commit();
}catch(PDOException $e){
// do stuff
$dbu->rollBack();
$dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
exit();
}catch(Exception $e){
// do stuff
$dbu->rollBack();
$dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
exit();
}
Will this help ensure the two commit statements have the best possible chance of succeeding? Or does the try/catch block automatically throw PDOException before the custom ones are ever called? It would be nice to have a simple identifier to know which transaction is failing, opposed to the entire $e->getMessage();.
It's impossible to have a proper transaction across different database connections.
Although you can do some awkward workarounds, it won't have your transaction a real one.
So, you have to either keep all the operations within a single database or forget about transactions
You cannot roll back committed changes.
As with your other question code block 3 is the way to go. Even though a commit might fail it will not fail because of common errors (like wrong syntax or constraint violation or what else). Hypothetical the whole PHP process might be killed right in between both commits resetting the latter letting you with no chance to fix the resulting errors in-code. However you will have to take care of those rare exceptions separately (e.g. backups) because I don't see an efficient way to handle them in-code.
Also remember that when committing the changes have already been applied but not been "published". So the commit itself is rarely to fail (only for exceptional reasons).
#EDIT 1
The way you handle errors depends on how you set up your PDO instances. See the documentation on how errors can be handled by PDO.
Your code block 4 will work if you use the default mode (not setting the error mode explicitely or setting it to PDO::ERRMODE_SILENT).
I'm staring myself blind on the fact why I can't execute this update statement.
$prepare = 'UPDATE plugins SET status=? WHERE name=?';
$execute = array("0", "testplugin");
$statement = $this->_dbConnectionInstance->prepare($prepare);
$statement->execute($execute);
I set error reporting to PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION and it produces no errors. However, the row in the database is not changed after execution. Executing the query directly on the database (PHPMyAdmin, replacing the question marks with the values) does work, strangely enough.
Also, the database connection is okay (a simple query 'SELECT * FROM plugins' does work).
I used an online available database class (PDO-Quick), which has an architectural error in it. When instantiating the database class, a transaction is started (1). But, nowhere in the class is committed and the transaction stays always 'open'. I now begin a transaction before every query, commit it after execution and rollback when any error occurs (2).
(1)
$this->_dbConnectionInstance->beginTransaction();
(2)
try {
$this->_dbConnectionInstance->beginTransaction();
$stmt = $this->_dbConnectionInstance->prepare($prepare);
$stmt->execute($execute);
$this->_dbConnectionInstance->commit();
} catch(PDOException $e) {
$this->_dbConnectionInstance->rollBack();
exit();
}
I tried to execute some custom queries using DB::execute() function by passing query as a parameter to this function - update query on existing table. Before running this query I checked the connection object like this $connection = DB::getConnection(); and it returned a connection identifier. Then while executing query it returned bool true from execute function, though changes were not there in database table fields. Also if I pass query with wrong syntax, it is giving error.
Is there any rollback process going on in background with update query statement in ActiveCollab? If yes how to stop this rollback to avoid the changes done by my update query ?
Could anyone tell me what could be the issue here ?
All uncommitted transactions are automatically rolled back on script shutdown so you need to make sure that you are committing transactions that you opened:
try {
DB::beginWork();
// Do something
DB::commit();
} catch(Exception $e) {
DB::rollback();
throw $e;
}
or:
DB::transact(function() {
// Do something
});
If you are within a nested transaction and outer transactions gets rolled back, your updates will be rolled back as well.
I think the question itself is pretty self-explanatory. The code is given below -
<?php
$PDO = NULL;
$pdo_dsn = 'mysql:host=localhost;dbname=pdo_test';
$pdo_persistence = array( PDO::ATTR_PERSISTENT => true );
$db_user = 'root';
$db_pass = '';
$db_query = "INSERT INTO person(name, address)
VALUES ('Mamsi Mamsi', 'Katabon')";
try
{
$PDO = new PDO($pdo_dsn, $db_user, $db_pass,
$pdo_persistence);
}
catch(PDOException $e)
{
echo "Error occured: ". $e->getMessage();
die();
}
$PDO->setAttribute(PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION);
$PDO->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
try
{
$PDO->beginTransaction();
$PDO->exec($db_query);
throw new PDOException('Generated Exception');
$PDO->commit();
}
catch(PDOException $e)
{
echo "An error occured while doing a database transaction. The
error message is : ".$e->getMessage();
$PDO->rollBack();
die();
}
?>
Even if I am rolling back the transaction inside the catch block, data are still being inserted into the database. Why?
EDIT
I am adding the following few lines from the documentation for further clarification -
Unfortunately, not every database supports transactions, so PDO needs
to run in what is known as "auto-commit" mode when you first open the
connection. Auto-commit mode means that every query that you run has
its own implicit transaction, if the database supports it, or no
transaction if the database doesn't support transactions. If you need
a transaction, you must use the PDO::beginTransaction() method to
initiate one. If the underlying driver does not support transactions,
a PDOException will be thrown (regardless of your error handling
settings: this is always a serious error condition). Once you are in
a transaction, you may use PDO::commit() or PDO::rollBack() to finish
it, depending on the success of the code you run during the
transaction.
Also, the following lines from this page -
bool PDO::beginTransaction ( void )
Turns off autocommit mode. While autocommit mode is turned off,
changes made to the database via the PDO object instance are not
committed until you end the transaction by calling PDO::commit().
Calling PDO::rollBack() will roll back all changes to the database
and return the connection to autocommit mode.
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.
You should check that you are using INNODB as your database type. MyISAM does not support transactions.