This is the code i use to insert record. Whenever there is insert error , The subscriber table auto - inc number will still increase even i have roll back? What is the problem? I just want the auto increment number not add when error occur.thanks a lot for help .
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$conn->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
$conn->beginTransaction();
try {
$email = $_POST['Email'];
$FirstName = $_POST['FirstName'];
$LastName = $_POST['LastName'];
$query="INSERT INTO subscriber (Email,FirstName,LastName,CreateDate) VALUES (?,?,?,CURDATE())";
$stmt = $conn->prepare($query);
$stmt->bindParam(1, $email , PDO::PARAM_STR);
$stmt->bindParam(2, $FirstName, PDO::PARAM_STR);
$stmt->bindParam(3, $LastName, PDO::PARAM_STR);
$stmt->execute();
$conn->commit();
}
catch(PDOException $e)
{
$conn->rollBack();
die ($e->getMessage()."<a href='addSub.php'>Back</a>");
}
$conn->beginTransaction();
try {
$userID = $_SESSION['username'];
$query="INSERT INTO list_sub (SubID,ListID) VALUES ('',$_SESSION[ListID])";
$stmt = $conn->prepare($query);
$stmt->execute();
$conn->commit();
}
catch(PDOException $e)
{
$conn->rollBack();
die ($e->getMessage()."<a href='addSub.php'>Back</a>");
}
$conn = null;}
Without knowing line numbers in your code, its hard to know but you commit your transaction at the end of the first try-catch block, and then proceed without starting a new transaction in your second try-catch block.
Add $conn->beginTransaction(); at the beginning of your second try-catch block.
EDIT -
You mention "I just want the auto increment number not add when error occur". You should not rely on the auto-increment feature to generate a "gapless" sequence of numbers.
PDO's auto-commit is probably enabled, and it's causing a problem when you try to rollback, since it has already committed. You can use PDO::ATTR_AUTOCOMMIT to disable this behavior:
$conn->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
When running under apache (or perhaps other mechanism that does connection pooling), you may need to check and ensure that you have not got persistent connections in use.
This is because when connection pooling is in effect, you cannot guarantee that the call to ->commit() winds up on the same connection on which you called ->beginTransaction().
This kind of situation manifests in cases where you are using multiple database handles to perform multiple queries, and only one series of queries belong in a transaction, which you have dutifully bound to a specific database handle (variable). However, underneath you, the pooling connection manager may be sending your queries among any available connection so long as you haven't specified "exclusive" use by disabling persistence on one of the handles.
In my case, I was performing a transaction with a series of steps. Due to the code being composed of functions to accomplish specific tasks in the transaction, some of the functions would spin-off a separate database connection to perform some related query (e.g verify ownership of an asset identified in a table row). Running in an apache environment, I would get this error 'There is no active transaction', and in addition, the rollBack() call appeared to be ineffective -- the changes were committed even thought he call to commit reported an error.
Took me a while to narrow it down to persistent connections. Turning off connection pooling on the handle for which I was performing a transaction made things work the way I expected.
Related
I am attempting to port an 8-year-old PHP/MySQL web app to a more recent server stack using MariaDB instead of MySQL. I have found it impossible to run more than one stored procedure on the same connection due to a "packets out of order" error. Below is code that should work, but doesn't. Can someone point out where I may have gone astray here or what would be a successful alternative approach?
<?php
$host = "localhost";
$user = "mysqli_test";
$password = "";
$database = "mysqli_test";
function get_connection()
{
GLOBAL $host, $user, $password, $database;
$connection = new mysqli($host, $user, $password, $database);
if (! $connection || mysqli_connect_errno() )
printf("Connection failure: %s.\n", mysqli_connect_error());
return $connection;
}
// Minimum viable function: isolate necessary steps.
function get_person($connection, $first_name)
{
$query = "CALL Get_By_First_Name(?)";
if (($stmt = $connection->prepare($query))) // error here after first pass
{
$stmt->bind_param('s', $first_name);
if ($stmt->execute())
{
$stmt->store_result();
$stmt->bind_result($id, $fname, $lname, $pets);
while($stmt->fetch())
printf("%3d %20s %20s %2d.\n", $id, $fname, $lname, $pets);
$stmt->free_result();
while ($stmt->next_result()) // my suspected culprit
{
$stmt->store_result();
while ($stmt->fetch())
;
$stmt->free_result();
}
}
$stmt->close();
}
}
if ($conn = get_connection())
{
get_person($conn, "Samuel"); // it works the first time
get_person($conn, "Zelda"); // this time it fails
}
?>
Running aPHP/mysqli code,
lmost identical C++ code using the C API works fine, so I can isolate where I think the problem starts: with the next_result() function. In the C++ code, next_result() returns TRUE to indicate a new result was available after using the prepared statement to run the stored procedure. In the PHP/mysqli code, next_result() returns false, and, in fact, fails to produce a new result even if I ignore the false return value.
I created a github repository that includes more explanation and scripts that can run on your computer to replicate the error, if anyone is interested.
The best practice is to avoid using stored procedures from PHP... That is not always possible; sometimes stored procedures are necessary and they might even be useful on rare occasions. But if you can, try to move the logic to the PHP application rather than storing it on a MySQL server. It's much less cumbersome this way.
If you want to know how to call stored procedures correctly, the best resource to reach for is the PHP manual. I have recently improved most of the examples in the manual so I know that the examples there reflect best practices and actually work. Read stored procedures using mysqli and mysqli::multi_query() documentation.
I would advise avoiding mysqli::multi_query(), despite stored procedures being probably the primary reason that function even exists. You have made the right choice to use prepared statements so you can bind the parameters and avoid SQL injection.
The main thing you have to remember is that CALL() statement produces an empty result. If the stored procedure also produces result set/sets then you need to iterate over them and fetch each one. The problem with stored procedures is that you can never be certain how many result sets will be produced. How can you handle the unknown?
Take a look at this example (it's your code but I made some changes and enabled error reporting):
function get_connection($host, $user, $password, $database): mysqli
{
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
return new mysqli($host, $user, $password, $database);
// you might want to set the correct charset (e.g. utf8mb4) here before returning
}
function get_person(mysqli $connection, $first_name): mysqli_result
{
$query = "CALL Get_By_First_Name(?)";
$stmt = $connection->prepare($query);
$stmt->bind_param('s', $first_name);
$stmt->execute();
/* We expect this SP to return the main result set,
which we want to return and then an empty result for CALL.
Get the result here and return immediately.
The prepared statement will be closed automatically once
we leave the scope and this will clean up the remaining result set.
*/
return $stmt->get_result();
}
$conn = get_connection($host, $user, $password, $database);
$res1 = get_person($conn, "Samuel");
$res2 = get_person($conn, "Zelda");
var_dump($res1->fetch_all(), $res2->fetch_all());
In the above code, I make an assumption that the SP I am calling will return only two result sets. I want only the first one and I don't care about the result of CALL(). The second result set is discarded when the statement is cleaned up. If that was not the case, I would need to call mysqli_stmt::next_result() until all result sets are fetched (not the mysqli::next_result()!). This is the easiest way to handle the cumbersome stored procedures.
Even if you converted the code to PDO, this would still be the simplest way to do it. With mysqli things can get very complicated if you go over the board though, so be careful not to overengineer the solution. If your stored procedures use cursors, note that there was a bug in PHP up to PHP 7.4.
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).
We have been going through some old code of ours and we have found some code that looks something like:
try
{
$stmt = $db->prepare($query);
$stmt->bindvalue(1, $id, PDO:ARAM_INT);
$stmt->execute();
$row = $stmt->fetchColumn();
}
catch(PDOException $e)
{
echo "There was an issue with query: ";
print_r($db->errorInfo());
}
Which at first glance we thought looked fine (Even many answers on Stack Exchange give this as example code). Then we looked at the PHP documentation for the errorInfo function and it states that:
PDO::errorInfo() only retrieves error information for operations performed directly on
the database handle. If you create a PDOStatement object through
PDO:repare() or PDO::query() and invoke an error on the statement
handle, PDO::errorInfo() will not reflect the error from the statement
handle
Which, if we understand it correctly, means that if anything goes wrong in any of the statement operations we do, we will not actually print out the error code we are expecting after "There was an issue with the query: ". Is this correct?
In light of this, we started looking for the proper way to do this, we started by looking at the PDOException class documentation which suggests that we might do something like:
try
{
$stmt = $db->prepare($query);
$stmt->bindvalue(1, $id, PDO:ARAM_INT);
$stmt->execute();
$row = $stmt->fetchColumn();
}
catch(PDOException $e)
{
echo "There was an issue with query: ";
print_r($e->errorInfo());
}
My questions are:
Is the above way the proper way of doing this? If not, what IS the proper way of doing it?
Is there any more useful information avaliable by using $db->errorInfo() and $db->errorCode ( or $stmt->errorInfo and $stmt->errorCode ) beyond what you can see from PDOException?
If there IS anything more detailed available-and-useful from those detailed calls, then is there a way to differentiate, by examining the PDOException, whether it was thrown by PDO or by PDOStatement?
The exception may be thrown by either $db->prepare or any of the $stmt operations. You do not know whence the error originated, so you should not guess. The exception itself contains all the information about what went wrong, so yes, consulting it and only it is the only sensible thing to do.
Moreover, it's usually nonsense to try..catch directly around the database call (unless you have a clear plan about something you want to do if this particular database operation fails). In your example, you're merely outputting the error and are continuing as if nothing happened. That's not sane error handling. Exceptions explicitly exist to abort and jump ship in case of a severe error, which means the part of your code which should actually be catching the exception should live several layers up and not have access to $db or $stmt at all (because it's in a different scope). Perhaps you should't be catching the exception at all and have it terminate your entire script (again, unless you expected an error to occur and have a clear plan how to handle it and how to recover your application into a known state). So looking only at the information in the exception itself is, again, the only sensible thing to do.
If there IS anything more detailed available-and-useful from those detailed calls, then is there a way to differentiate, by examining the PDOException, whether it was thrown by PDO or by PDOStatement?
This is only useful if, again, you have any sort of plan for recovery and that plan differs depending on where the error occurred. First of all, I doubt that, but if that's indeed the case, then you'd do something like this:
try {
$stmt = $db->prepare($query);
} catch (PDOException $e) {
// do something to save the day
}
try {
$stmt->bindValue(...)
..
} catch (PDOException $e) {
// save the day another way
}
In other words, you isolate the try..catch statements to smaller parts of your code that you want to distinguish. But, really... if $db->prepare failed, what are you going to do? You can't continue with the rest of the code either way. And what are you going to do differently than if a $stmt method failed? As one atomic unit, you were unable to query the database, period.
The PDOException::$code will give you the more detailed SQLState error code, which may tell you something useful (e.g. unique constraint violation), which is useful information to work with on occasion. But when inspecting that it's pretty irrelevant which specific line threw the error.
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 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.