I have a TRIGGER for a MySQL table which raises exception with SIGNAL when there's something wrong with the data provided. How can I catch this exception in PHP with PDO?
Thanks in advance.
UPDATE
More information on my problem: I am executing several queries within a transaction and I want to rollback the transaction if one of them fails because there was a SIGNAL. Currently a signal is activated but all the other queries are executed.
ANOTHER UPDATE
So far I've got:
try {
$db->beginTransaction();
if (!$db->query('UPDATE accounts SET amount = amount - 10 WHERE id = 1')) {
throw new Exception($db->errorInfo()[2]);
}
if (!$db->query('UPDATE accounts SET amount = amount + 10 WHERE id = 2')) {
throw new Exception($db->errorInfo()[2]);
}
$db->commit();
} catch (Exception $e) {
$db->rollback();
echo 'There was an error: ' . $e->getMessage();
}
Is this the proper way to do a transaction with handling the exceptions?
Sorry if my question is too broad.
Try something like:
$ls_error = "";
$mysqli->autocommit(FALSE);
// first sql
if (!$mysqli->query($sql)) {
$ls_error .= sprintf("Error: %d: %s\n",$mysqli->errno, $mysqli->error);
$mysqli->rollback();
}
// second one
if ($ls_error == "") {
if (!$mysqli->query($sql)) {
$ls_error .= sprintf("Error: %d: %s\n",$mysqli->errno, $mysqli->error);
$mysqli->rollback();
}
}
// third one
if ($ls_error == "") {
if (!$mysqli->query($sql)) {
$ls_error .= sprintf("Error: %d: %s\n",$mysqli->errno, $mysqli->error);
$mysqli->rollback();
}
}
if ($ls_error == "") {
$mysqli->commit();
} else {
echo $ls_error;
}
Abstract example. I have the solution works without any problems.
Trigger:
IF NEW.value < 0 THEN
SET #msg = CONCAT('Wrong count_from value! ', NEW.value);
SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = #msg;
END IF;
php (PDO):
try {
$conn->query('INSERT INTO `table` SET `value` = -1');
} catch (Exception $err) {
$error_message = $conn->query('SELECT #msg')->fetchColumn();
}
Related
If I have multiple queries on chain, on a structure based on IFs, like this:
$query1 = mysqli_query("query here");
if(!query1){
//display error
} else {
$query2 = mysqli_query("another query here");
if(!query2){
//display error
//rollback the query1
} else {
query3 = mysqli_query("yet again another query");
if(!query3) {
//display error
//rollback the query2
//rollback the query1
} else {
query4 = mysqli_query("eh.. another one");
if(!query4){
//display error
//rollback the query3
//rollback the query2
//rollback the query1
} else {
return success;
}
}
}
}
Is there a best way to rollback the previous query, if the next one fails?
Otherwise I'm gonna have the first 2 query successfull, which edited the database, but the 3° failed, so 3° and 4° didn't edit the dabatase, with the result of having it corrupted.
I thought about something like:
...
$query2 = mysqli_query("another query here");
if(!query2){
//display error
$rollback = mysqli_query("query to rollback query1");
} else {
query3 = mysqli_query("yet again another query");
if(!query3) {
//display error
$rollback = mysqli_query("query to rollback query2");
$rollback = mysqli_query("query to rollback query1");
} else {
...
But the above method grants even more chances to fail more queries.
Is there any other more effective methods?
This is how i would do it with mysqli:
Configure mysqli (somewehere at the begining of your application) to throw exceptions when a query fails.
mysqli_report(MYSQLI_REPORT_STRICT);
This way you will not need all the if .. elseif .. else.
$connection->begin_transaction();
try {
$result1 = $connection->query("query 1");
// do something with $result1
$result2 = $connection->query("query 2");
// do something with $result2
$result3 = $connection->query("query 3");
// do something with $result3
// you will not get here if any of the queries fails
$connection->commit();
} catch (Exception $e) {
// if any of the queries fails, the following code will be executed
$connection->rollback(); // roll back everything to the point of begin_transaction()
// do other stuff to handle the error
}
Update
Usually the user don't care about, why his action failed. If a query fails, it's never the users fault. It's either the fault of the developer or of the environment. So there shouldn't be a reason to render an error message depending on which query failed.
Note that if the users intput is the source of the failed query, then
you didn't validate the input properly
your queries are not injection safe (If the input can cause an SQL error it can also be used to compromise your DB.)
However - I don't say there can't be reasons - I just don't know any. So if you want your error message depend on which query failed, you can do the following:
$error = null;
$connection->begin_transaction();
try {
try {
$result1 = $connection->query("query 1");
} catch (Exception $e) {
$error = 'query 1 failed';
throw $e;
}
// do something with $result1
try {
$result2 = $connection->query("query 2");
} catch (Exception $e) {
$error = 'query 2 failed';
throw $e;
}
// do something with $result2
// execute more queries the same way
$connection->commit();
} catch (Exception $e) {
$connection->rollback();
// use $error to find out which query failed
// do other stuff to handle the error
}
I am trying catch database errors within a transaction and if one occurs then rollback and throw an exception.
However, the code is stopping and displaying the db error screen before it throws the exception.
Any ideas how I can make it detect db error without stopping running the subsequent code?
try {
$this->my_function($data);
} catch (Exception $e) {
var_dump($e);
}
private function my_function($data)
{
$this->db->trans_start();
foreach($data as $reg)
{
$sql = $this->db->insert_string('my_table', $reg);
if($this->db->query($sql))
{
continue;
} else {
$this->db->trans_rollback();
throw new Exception('Exception message here...');
}
}
$this->db->trans_complete();
}
This has been answered before on this question
As answered by cwallenpoole:
In application/config/database.php set
// suppress error output to the screen
$db['default']['db_debug'] = FALSE;
In your model or controller:
// try the select.
$dbRet = $this->db->select($table, $dataArray);
// select has had some problem.
if( !$dbRet )
{
$errNo = $this->db->_error_number()
$errMess = $this->db->_error_message();
// Do something with the error message or just show_404();
}
Or in you case:
private function my_function($data)
{
$errors = array();
$this->db->trans_start();
foreach($data as $reg)
{
$sql = $this->db->insert_string('my_table', $reg);
if($this->db->query($sql))
{
continue;
} else {
$errNo = $this->db->_error_number()
$errMess = $this->db->_error_message();
array_push($errors, array($errNo, $errMess));
}
}
$this->db->trans_complete();
// use $errors...
}
Even better
I believe this question has all the answers you need because it takes multiple inserts into account and let's you finish the once that did not return an error.
I have a form script that inserts data in multiple tables. But if one fails it has to revert everything back. I've been looking for this but I didn't find a question that uses multiple (mysqli) prepared statements and rollback.
$mysqli = new mysqli(/* connection details */);
$mysqli->autocommit(FALSE);
// rollback should revert here
$mysqli->begin_transaction();
if ($stmt_one = $mysqli->prepare('INSERT INTO main_table (one, two, three) VALUES (?, ?, ?)')) {
$stmt_one->bind_param('sss', $one, $two, $three);
$stmt_one->execute();
$id = (int) $mysqli->insert_id;
if ($stmt_two = $mysqli->prepare('INSERT INTO sub_table_one (id, four) VALUES (?, ?)')) {
$stmt_two->bind_param('is', $id, $four);
$stmt_two->execute();
}
if ($stmt_three = $mysqli->prepare('INSERT INTO sub_table_two (id, five) VALUES (?, ?)')) {
$stmt_three->bind_param('is', $id, $five);
$stmt_three->execute();
}
}
if ($mysqli->commit()) {
// everything ok
header('Location: /');
} else {
// something went wrong, we have to rollback
$mysqli->rollback();
// and display the error message
echo $stmt_one->error;
echo $stmt_two->error;
echo $stmt_three->error;
}
I'm not sure about this, can I do something like that? Or do I have to check every $stmt_* for errors?
UPDATE
(The problem was that I was using DB Engine MyISAM instead of InnoDB)
I've upgraded to PHP 7 and now rollback is not working. I've tried the following:
if (!$mysqli->rollback()) $log .= 'no rollback :(';
But I don't see that message in my log...
My current code looks like:
public function Upload() {
try {
// rollback should revert here
$mysqli->begin_transaction();
// multiple prepared statements
if ($mysqli->commit()) {
$exit = $log;
} else {
throw new Exception('Transaction commit failed. Property ID: ' . $this->id);
}
} catch (Exception $e) {
try {
$test = $this->owner['id'] ? 'property' : ($this->applicant ? 'demand' : 'Fatal Error: PropertyFromInput() contact error (no owner, no applicant)');
$log = 'Rolling back new ' . $test . ' upload' . "\n";
if (!$mysqli->rollback()) $log .= 'no rollback...' . "\n";
if ($test == 'property') $log .= $this->cleanup_prop() ? 'property successfully cleaned up' . "\n" : 'error while cleaning up property' . "\n";
$err_msg = $e->getMessage();
} catch (Exception $f) {
$err_msg .= $f->getMessage();
}
$usr_msg = $upload_err ? $err_msg : 'Se ha producido un error. Por favor contacte con un administrador.';
$log .= 'User triggered an error while uploading a new ' . $test . ".\n" . 'Error message: ' . $err_msg;
$exit = array($log, $usr_msg);
}
$mysqli->autocommit(TRUE);
return $exit;
}
My log looks like:
Logging new property upload...
Rolling back new property upload
property successfully cleaned up
User triggered an error while uploading a new property.
Error message: Upload Error: $thumbnailsPath is false and/or not a dir: ". Property ID: 200
That means rollback() was executed/ didn't return false but the tables were inserted anyways...
Now about the new php features since 5.4:
5.5 added the finally block, do I just use it for setting autocommit to true?
I read php 7 improved the errors handling but I really have no idea how to take advantage of this.
I need this fixed because I wouldn't like my production db ending full of broken/unused data, who would?
As Xorifelse mentioned, your best option is to use try and catch even if you can't use finally. I've never used mysqli but I am assuming it is similar to PDO where you would do something like this:
$redirect = false;
try {
$mysqli = new mysqli(...);
$mysqli->autocommit(FALSE);
// rollback should revert here
$mysqli->begin_transaction();
if ($stmt_one = $mysqli->prepare('INSERT INTO main_table (one, two, three) VALUES (?, ?, ?)')) {
$stmt_one->bind_param('sss', $one, $two, $three);
if (!$stmt_one->execute()) {
throw new Exception($stmt_one->error);
}
$id = (int) $mysqli->insert_id;
if ($stmt_two = $mysqli->prepare('INSERT INTO sub_table_one (id, four) VALUES (?, ?)')) {
$stmt_two->bind_param('is', $id, $four);
if (!$stmt_two->execute()) {
throw new Exception($stmt_two->error);
}
}
if ($stmt_three = $mysqli->prepare('INSERT INTO sub_table_two (id, five) VALUES (?, ?)')) {
$stmt_three->bind_param('is', $id, $five);
if (!$stmt_three->execute()) {
throw new Exception($stmt_three->error);
}
}
}
if ($mysqli->commit()) {
$redirect = true;
} else {
throw new Exception('Transaction commit failed.');
}
} catch (Exception $e) {
try {
// something went wrong, we have to rollback
$mysqli->rollback();
// and display the error message
echo $e->getMessage();
} catch (Exception $f) {
// and display the error message
echo $f->getMessage();
}
}
// don't forget to do this also
$mysqli->autocommit(TRUE);
if ($redirect) {
header('Location: /');
}
In regards to the last line,
According to http://www.php.net/manual/en/mysqli.commit.php#89976,
calling $mysqli->commit() will NOT automatically set autocommit() back
to 'true'. That means that any queries following $mysqli->commit() will
be rolled back when your script exits, if autocommit() will be not
switched back to TRUE.
I have 2 DBs: 1 in MySQL and the other one on SQLite3.
I need to insert the same data into both. To achieve this by a Form, I'm making a PHP script, that has some issue.
Here below the code then the explanation on what's going on:
// MySQL
try {
$sql = new PDO($pdo_servername, $username, $password, $pdo_options);
$sql->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$ret = $sql->exec($query);
if(!$ret){
echo $sql->lastErrorMsg();
} else {
echo "New record created successfully on MySQL DB";
}
} catch (PDOException $e) {
echo $sql . "<br>" . $e->getMessage();
}
$sql->close();
// SQLite
try {
$sqlite = new PDO($pdo_servername_sqlite3);
$sqlite->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$retlite = $sqlite->exec($query);
if(!$retlite){
echo $sqlite->lastErrorMsg();
} else {
echo "New record created successfully on SQLite3 DB";
}
} catch (PDOException $e) {
echo $sqlite . "<br>" . $e->getMessage();
}
$sqlite->close();
The MySQL works fine, while the SQLite3 doesn't even start.
Inverting the blocks, thus first SQLite3 then MySQL, the issue is inverted: the SQLite3 works fine and MySQL doesn't start.
I have not any error returned
I tried also to avoid any try-catch-finally, and I just wrote the code as simple it is, and I got the same identical situation.
Is it forbidden to open 2 PDO connections, to 2 different DBs?
Where is my mistake please?
Try this way, that is the only breakpoint where you really need try...catch:
// MySQL
try {
$sql = new PDO($pdo_servername, $username, $password, $pdo_options);
} catch (PDOException $e) {
echo 'MySQL connection failed: ' . "<br>" . $e->getMessage();
$sql = false;
}
// SQLite
try {
$sqlite = new PDO($pdo_servername_sqlite3);
} catch (PDOException $e) {
echo 'SQLite connection failed: '. "<br>" . $e->getMessage();
$sqlite = false;
}
if ($sql != false) {
$ret = $sql->exec($query);
if(!$ret){
echo $sql->lastErrorMsg();
} else {
echo "New record created successfully on MySQL DB";
}
$sql->close();
}
if ($sqlite != false) {
$retlite = $sqlite->exec($query);
if(!$retlite){
echo $sqlite->lastErrorMsg();
} else {
echo "New record created successfully on SQLite3 DB";
}
$sqlite->close();
}
First of all I want to thank everybody contributed here :)
I would like to post the definitive working code, because some line, should also be changed, respect the above code.
Indeed the PDO method lastErrorMsg(); seems don't exist, and the same for the PDO method close(); It should be used errorInfo()in place of lastErrorMsg();and it's an array. While to close the DB connection: I read somewhere here on Stackoverflow that when the script execution ends, automatically PDO closes it, OR you need to destroy the object assign a null.
Because finally the code suggested by #Alex, with these small changes, was working, I was able to get the errors from PHP highlighting the above details.
Please here below the final working code, hoping that can be useful to any other had my same issue:
/**
* MySQL - try to open it. If it fails,
* it returns which error and continues the execution of the script
*/
try {
$sql = new PDO($pdo_servername, $username, $password, $pdo_options);
} catch (PDOException $e) {
echo 'MySQL connection failed: ' . "<br>" . $e->getMessage();
$sql = false;
}
/**
* SQLite - try to open it. If it fails,
* it returns which error and continues the execution of the script
*/
try {
$sqlite = new PDO($pdo_servername_sqlite3);
} catch (PDOException $e) {
echo 'SQLite connection failed: '. "<br>" . $e->getMessage();
$sqlite = false;
}
/**
* If the connection is made, it executes the Query
* If anything wrong with the Query insertion, an error is returned.
* The script continues
*/
if ($sql != false) {
$ret = $sql->exec($query);
if(!$ret){
print_r($sql->errorInfo()); // THIS is the valid method for PDO Exec and returns an array
} else {
echo "New record created successfully on MySQL DB";
}
}
if ($sqlite != false) {
$retlite = $sqlite->exec($query);
if(!$retlite){
print_r($sqlite->errorInfo()); // THIS is the valid method for PDO Exec and returns an array
} else {
echo "New record created successfully on SQLite3 DB";
}
}
/**
* Closes the DB Connections
*/
$sql = null;
$sqlite = null;
Thanks to all of you for your valid help. I very much appreciated it :)
below is my code. when I use transaction, the transaction is committed but the first query ($receipt_query) is not entered into the DB, the other two are. When running the queries without transaction, all queries are run successfully. So can anyone spot the problem here?!
$mysqli->autocommit(false);
if(!empty($_POST['receipt'])) {
$result = $mysqli->query("insert query 1");
if (!$result) {
$error = 'Some error message';
}
}
if (!empty($_POST['payment'])) {
$result = $mysqli->query("insert query 2");
if (!$result) {
$error = 'Some error message';
}
}
if(empty($error)) {
if($mysqli->query("query 3")) {
$mysqli->commit();
} else {
$mysqli->rollback();
}
} else {
$mysqli->rollback();
}
Doesn't transaction mean "All or None"? so how come the first one doesn't commit even though the whole transaction is committed?
You need to start transaction after autommit.
$mysqli->autocommit(false);
$mysqli->begin_transaction();
An surround the code with try-catch:
try {
// First of all, let's begin a transaction
$mysqli->autocommit(false);
$mysqli->begin_transaction();
// A set of queries
$mysqli->query('first query');
$mysqli->query('second query');
$mysqli->query('third query');
// If we arrive here, it means that no exception was thrown
// i.e. no query has failed, and we can commit the transaction
$mysqli->commit();
} catch (Exception $e) {
// An exception has been thrown
// We must rollback the transaction
$mysqli->rollback();
}