mysqli transaction committing only some of queries - php

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();
}

Related

Mysqli Prepared statement usage with AJAX POST to PHP file

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.

Rolling back MySQL queries

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
}

php exception not working

I have a simple program that should fire two exceptions, the second being a rethrow of the first. However, only the first exception is being thrown.
This is my code:
In dao.php
function executeSQLC($cn, $sql) {
$rs = odbc_exec($cn, $sql);
if($rs == false) {
throw new Exception("ex1: ".odbc_error());
}
return $rs;
}
In bean.php
include("dao.php");
$conn = odbc_connect("xxx", "xxxx", "xxxx");
odbc_autocommit($conn, FALSE);
$sql ="invalid sql";
try {
executeSQLC($conn, $sql);
odbc_commit($conn);
odbc_close($conn);
} catch(Exception $e) {
odbc_rollback($conn);
throw new Exception("ex2: ".$e->getMessage(). " " .$sql);
}
I see "ex1", so i think the first exception, at the dao, is being thrown, however, i can't see "ex2" and the connection never gets rollbacked.
Thanks
Ok, some other code was calling/messing with the $sql variable, not sure that this was the cause, but things seem to be working now. Thanks folks.

rollback if one of multple inserts fails using prepared statements

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.

MySQL 5.5 SIGNAL and PDO exceptions

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();
}

Categories