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.
Related
I'm trying to do Signup and Login with PHP and MySQL.. but when I enter the same email it shows me error... When I try to enter the same email.. already registered, it shows me an error. It should redirect straight to signup-success.html, but instead it shows an error on line 45.. i tried replacing it with
but even this didn't help..
I'm new to php so I'm following the tutorial I'm using to do this... https://www.youtube.com/watch?v=5L9UhOnuos0
{
$stmt->execute();
} catch (mysqli_sql_exception $e) {
if ($e->getCode() == 1062) {
die("email address is already taken");
}
}
--------------------------------------------------------------------------
$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
$mysqli = require __DIR__ . "/database.php";
$sql = "INSERT INTO user (name, email, password_hash)
VALUES (?, ?, ?)";
$stmt = $mysqli->stmt_init();
if ( ! $stmt->prepare($sql)) {
die("SQL error: " . $mysqli->error);
}
$stmt->bind_param("sss",
$_POST["name"],
$_POST["email"],
$password_hash);
if ($stmt->execute()) {
header("Location: signup-success.html");
exit;
} else {
if ($mysqli->errno === 1062) {
die("email already taken");
} else {
die($mysqli->error . " " . $mysqli->errno);
}
}
This is what website shows..
Fatal error: Uncaught mysqli_sql_exception: Duplicate entry 'email#seznam.cz' for key 'email' in D:\Programy\XAMPP\XAMPP\htdocs\login01\process-signup.php:45 Stack trace: #0 D:\Programy\XAMPP\XAMPP\htdocs\login01\process-signup.php(45): mysqli_stmt->execute() #1 {main} thrown in D:\Programy\XAMPP\XAMPP\htdocs\login01\process-signup.php on line 45
Your call to ->execute() is throwing an exception when you attempt to do your INSERT and it fails due to a duplicate value. (The rules about exception throwing changed in php version 8.) Exceptions are a bit of a pain in php; it's tricky to catch them and handle them completely without logging them.
Here's what to do about that.
Change your query to say INSERT IGNORE.
$sql = "INSERT IGNORE INTO user (name, email, password_hash)
VALUES (?, ?, ?)";
Check whether the insert succeeded using affected_rows with code like this:
$result = $stmt->execute();
if ($result && $mysqli->affected_rows === 1) {
header("Location: signup-success.html");
exit;
} else if ($result && $mysqli->affected_rows !== 1)
die("email already taken");
} else {
die($mysqli->error . " " . $mysqli->errno);
}
This will let you control your logic without relying on error numbers except for when your SQL statement crashes and burns, not when you get a UNIQUE INDEX collision.
My question is, how efficient are PHP Mysqli prepared statements? From what I have understand from basic reading, prepared statements 1) help in security using bound inputs 2) speed up and 'reduce' data sent to the server by somewhat 'pre-packaging' or 'preparing' the sql query to an extent, and once data is available, it just attaches the data to the prepared statement and executes it. This also helps on 'repeated' use of the same statement when inserting the same data (different values) repeatedly, because the statement is prepared only once.
Now, I am building a website with several functionalities, and all (or most) of them use JQuery and AJAX to get obtain user input, do some checks (either in the JS/JQ or in PHP), Send the data to a PHP file PHP_AJAX_Handler.php specified in the AJAX URL. The PHP file prepares the SQL statemtns to insert data into database, then return JSON success/failure messages. For example, most of my features/functionality are programmed as follows; below is one file which I am using to 1) check for existing continent-country pair, and 2) insert the new continent-country pair.
HTML:
<input type='text' id='continent'>
<input type='text' id='country'>
<button id='btn1'></button>
<p id='p1'></p>
<p id='p2'></p>
<p id='p3'></p>
JQuery:
$("#btn1")click(function(){
var Cntt = $("#continent").val();
var Ctry = $("#country").val();
$.post("PHP_AJAX_Handler.php",{CN:cntt,CT:ctry},function(DAT)
{ var RET_j = json.PARSE(dat);
if(RET_j.PASS=='FAIL')
{ $('#p1').html(RET_j.PASS);
$('#p2').html(RET_j.MSG1);
}
if(RET_j.PASS=='OKAY')
{ $('#p1').html(RET_j.PASS);
$('#p3').html(RET_j.MSG2);
} }
);
});
PHP_AJAX_Handler.php
<?PHP
session_start();
if( (isset($_POST['CT'])) && (isset($_POST['CN'])))
{ require_once ("golin_2.php");
$CN = $_POST['CN'];
$CT = $_POST['CT'];
$ER = "";
$CONN = mysqli_connect($SERVER, $USER, $PASS, $DBNAME);
If($CONN == FALSE)
{ $ER = $ER . "Err: Conn Could not connect to Databse ".mysqli_connect_errno().' '.mysqli_connect_error();
}
else
{ $SQL_1 = "SELECT * FROM sailors.continental_regions WHERE CONTINENT = ? AND COUNTRY = ?";
if(!($STMT_1 = mysqli_stmt_init($CONN)))
{ $ER = $ER . "Err: Stmt Prepare Failed";
}
else
{ if(!mysqli_stmt_prepare($STMT_1, $SQL_1)) ///FIRST SET of prepared statement lines
{ $ER = $ER . "Err: Stmt Prepare Failed";
}
else
{ if(!mysqli_stmt_bind_param($STMT_1,"ss",$CN, $CT))
{ $ER = $ER . "Err: Stmt Prepare Failed";
}
else
{ if(!(mysqli_stmt_execute($STMT_1)))
{ $ER = $ER . "Err: Stmt_1 Execute Failed";
}
else
{ $RES_1 = mysqli_stmt_get_result($STMT_1);
$NUMROWS_1 = mysqli_num_rows($RES_1);
if($NUMROWS_1>0)
{ $ER = $ER . "Err: duplicate '$CN' '$CT' pair";
}
if($NUMROWS_1==0)
{ $SQL_2 = "INSERT INTO DB.continental_regions (CONTINENT,COUNTRY) values (?, ?)";
if(!($STMT_2=(mysqli_stmt_init($CONN))))
{ $ER = $ER . "Err: Init2 failed";
}
else
{ if(!mysqli_stmt_prepare($STMT_2, $SQL_2)) ///SECOND SET of prepared statement lines
{ $ER = $ER . "Err: Prep2 failed".mysqli_error($CONN);
}
else
{ if(!mysqli_stmt_bind_param($STMT_2,"ss",$CN, $CT))
{ $ER = $ER . "Err: Bind2 failed";
}
else
{
if(!(mysqli_stmt_execute($STMT_2)))
{ $ER = $ER . "Err: Exec failed";
}
else
{ $arr['PASS'] = 'OK';
}
}
}
}
}
}
}
}
}
mysqli_free_result($RES_1);
mysqli_stmt_close($STMT_1);
mysqli_stmt_close($STMT_2);
mysqli_close($CONN);
}
if($ER!=="")
{ $arr['MSG'] = $ER;
$arr['PASS'] = 'FAIL';
}
if($arr['PASS']=="OK")
{ $arr['MSG2'] = "Insert Success";
}
echo json_encode($arr);
}
else
{ header("location: ../Error_Fail.php");
}
?>
As you can see, the PHP file is turning out to be pretty long. There is one set of prepare statements to check if the CC pair exists already in table, then another to insert the CC pair.
From what I see, for each AJAX request to add a new pair of values, the mysqli statements are prepared over again. Then again for the next request, and so on. I imagine this is creating a lot of overhead and data to the server just to achieve Security. Is this true for other people developing web applications with AJAX-POST-PHP? to me it seems unavoidable that for each prepare, values can only be inserted one time? How to get around to preparing this statement once, and only doing repeat executes whence data is available? I can't seem to get my head around the 'efficiency' factor of prepared statements..
Thanks.. would appreciate some advise from some seasoned programmers out there..
You said:
As you can see, the PHP file is turning out to be pretty long.
That is true, but that is not the fault of prepared statements. You must have been learning PHP development from a poorly written tutorial. This code does not need to be so long. In fact, it can be severely shortened.
Just fixing your existing code made it much more readable. I used OOP-style mysqli and I removed all these if statements. You should enable error reporting instead.
<?php
session_start();
if (isset($_POST['CT'],$_POST['CN'])) {
require_once "golin_2.php";
$CN = $_POST['CN'];
$CT = $_POST['CT'];
$ER = "";
$arr = [
'PASS' => "OK",
'MSG2' => "Insert Success",
]; // successful state should be the default outcome
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$CONN = new mysqli($SERVER, $USER, $PASS, $DBNAME);
$CONN->set_charset('utf8mb4'); // always set the charset
// To check existance of data in database we use COUNT(*)
$stmt = $CONN->prepare("SELECT COUNT(*) FROM sailors.continental_regions WHERE CONTINENT = ? AND COUNTRY = ?");
$stmt->bind_param("ss", $CN, $CT);
$stmt->execute();
$NUMROWS = $stmt->get_result()->fetch_row()[0];
if ($NUMROWS) {
$ER .= "Err: duplicate '$CN' '$CT' pair";
} else {
$stmt = $CONN->prepare("INSERT INTO DB.continental_regions (CONTINENT,COUNTRY) values (?, ?)");
$stmt->bind_param("ss", $CN, $CT);
$stmt->execute();
}
if ($ER) {
$arr = [
'PASS' => "FAIL",
'MSG' => $ER,
];
}
echo json_encode($arr);
} else {
header("location: ../Error_Fail.php");
}
If you have a composite UNIQUE key on these two columns in your table then you can remove the select statement. Also, you should clean up your response preparation. The successful state should be the default and it should be replaced with the error message only if something went wrong.
In this example, I removed one SQL statement. The whole thing is now much simpler.
<?php
define('DUPLICATE_KEY', 1062);
session_start();
if (isset($_POST['CT'],$_POST['CN'])) {
require_once "golin_2.php";
$CN = $_POST['CN'];
$CT = $_POST['CT'];
$arr = [
'PASS' => "OK",
'MSG2' => "Insert Success",
]; // successful state should be the default outcome
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$CONN = new mysqli($SERVER, $USER, $PASS, $DBNAME);
$CONN->set_charset('utf8mb4'); // always set the charset
try {
$stmt = $CONN->prepare("INSERT INTO continental_regions (CONTINENT,COUNTRY) values (?, ?)");
$stmt->bind_param("ss", $CN, $CT);
$stmt->execute();
} catch (mysqli_sql_exception $e) {
if ($e->getCode() !== DUPLICATE_KEY) {
// if it failed for any other reason than duplicate key rethrow the exception
throw $e;
}
// if SQL failed due to duplicate entry then set the error message
$arr = [
'PASS' => "FAIL",
'MSG' => "Err: duplicate '$CN' '$CT' pair",
];
}
echo json_encode($arr);
} else {
header("location: ../Error_Fail.php");
}
Regarding performance.
There is no problem with performance in this example and prepared statements don't improve or degrade the performance. I assume you are trying to compare the performance to static SQL queries, but in your simple example there should be no difference at all. Prepared statements can make your code faster compared to static queries when you need to execute the same SQL many times.
If you find writing the 3 lines of code each time too much, then you can create a wrapper function that will reduce it for you to a single function call. In fairness you should avoid using mysqli on its own. Either switch to PDO or use some kind of abstraction library around mysqli.
I just switched to PDO from mySQLi (from mySQL) and it's so far good and easy, especially regarding prepared statements
This is what I have for a select with prepared statement
Main DB file (included in all pages):
class DBi {
public static $conn;
// this I need to make the connection "global"
}
try {
DBi::$conn = new PDO("mysql:host=$dbhost;dbname=$dbname;charset=utf8", $dbuname, $dbpass);
DBi::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
DBi::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(PDOException $e) {
echo '<p class="error">Database error!</p>';
}
And in my page:
try {
$sql = 'SELECT pagetitle, pagecontent FROM mypages WHERE pageid = ? LIMIT 1';
$STH = DBi::$conn->prepare($sql);
$STH->execute(array($thispageid)); // $thispageid is from a GET var
}
catch(PDOException $e) {
echo '<p class="error">Database query error!</p>';
}
if ($STH) { // does this really need an if clause for it self?
$row = $STH->fetch();
if (!empty($row)) { // was there found a row with content?
echo '<h1>'.$row['pagetitle'].'</h1>
<p>'.$row['pagecontent'].'</p>';
}
}
It all works. But am I doing it right? Or can I make it more simple some places?
Is using if (!empty($row)) {} an ok solution to check if there was a result row with content? Can't find other decent way to check for numrows on a prepared narrowed select
catch(PDOException $e) {
echo '<p class="error">Database query error!</p>';
}
I would use the opportunity to log which database query error occurred.
See example here: http://php.net/manual/en/pdostatement.errorinfo.php
Also if you catch an error, you should probably return from the function or the script.
if ($STH) { // does this really need an if clause for it self?
If $STH isn't valid, then it should have generated an exception and been caught previously. And if you had returned from the function in that catch block, then you wouldn't get to this point in the code, so there's no need to test $STH for being non-null again. Just start fetching from it.
$row = $STH->fetch();
if (!empty($row)) { // was there found a row with content?
I would write it this way:
$found_one = false;
while ($row = $STH->fetch()) {
$found_one = true;
. . . do other stuff with data . . .
}
if (!$found_one) {
echo "Sorry! Nothing found. Here's some default info:";
. . . output default info here . . .
}
No need to test if it's empty, because if it were, the loop would exit.
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();
}
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();
}