Why does multi_query not work when I use transactions? - php

Here is an example.
$mysqli = new mysqli("localhost", "root", "123", "temp");
$mysqli->begin_transaction();
$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";
$test = $mysqli->multi_query($sql1);
$mysqli->commit();
There isn't any error in either of the queries, but when calling commit() the values are not stored in the database. The same works perfectly fine if split into separate queries and executed via query().

$mysqli->multi_query($sql1);
$mysqli->commit(); // This will result in a "Commands out of sync; you can't run this command now" error.
The above is identical to:
$mysqli->multi_query($sql1);
$mysqli->query("commit"); // This will result in a "Commands out of sync; you can't run this command now" error.
Whatever you put in $mysqli->query("...");, it WILL result in a "Commands out of sync" error, even with a simple SELECT 1;
The reason for this error is because ->commit() operation runs a single query (commit;). However, the results of the previous queries have not been read.
When a single query() operation is used, the MySQL server will answer with a response frame that depends on the query statement.
When using multi_query(), the following happens at MySQL communication protocol level:
A "Request Set Option" (as displayed in Wireshark) frame is sent with "multi statements" flag set to ON.
A frame containing the whole string transmitted to multi_query() as request.
MySQL server answers with a response that may contain different resultsets. The same is true when calling a stored procedure.
Solution 1
If you want to use multi_query(), you must have your start transaction / commit operations as part of it:
$mysqli = new mysqli("localhost", "root", "123", "temp");
$sql1 = "start transaction;"; // $mysqli->begin_transaction() is a convenience function for simply doing this.
$sql1 .= "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";
$sql1 .= "commit;"; // $mysqli->commit() is a convenience function for simply doing this.
$mysqli->multi_query($sql1);
/* As in "Solution 2", if you plan to perform other queries on DB resource
$mysqli after this, you must consume all the resultsets:
// This loop ensures that all resultsets are processed and consumed:
do {
$mysqli->use_result();
}
while ($mysqli->next_result());
*/
Solution 2
$mysqli = new mysqli("localhost", "root", "123", "temp");
$mysqli->begin_transaction();
$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";
$mysqli->multi_query($sql1);
// This loop ensures that all resultsets are processed and consumed:
do {
$mysqli->use_result();
}
while ($mysqli->next_result());
// Now that all resultsets are processed, a single query `commit;` can happen:
$mysqli->commit();
MySQL Reference: "Commands out of sync".

You shouldn't use multi query. Rewrite your code as follows
$mysqli->begin_transaction();
$mysqli->query("insert into test (Name) values ('pratik5')");
$mysqli->query("insert into test (Name) values ('pratik6')");
$mysqli->commit();
or, for the real-life inserts,
$mysqli->begin_transaction();
$stmt = $mysqli->prepare("insert into test (Name) values (?)");
$stmt->bind_param("s", $name);
$name = 'pratik5';
$stmt->execute();
$name = 'pratik6';
$stmt->execute();
$mysqli->commit();

First thing first, in the example you have shown you should not be using multi_query(). You have two separate statements which should be executed separately. See Your Common Sense's answer
multi_query() should be rarely used. It should be used only in situations when you already have a string composed of multiple queries, which you absolutely trust. Don't ever allow variable input into multi_query()!
Why commit() doesn't work after multi_query()?
Truth be told MySQL does throw an error on commit(), but mysqli is not able to throw the error as exception. Whether it is a bug or a technical limitation I do not know. You can see the error if you check manually $mysqli->error property. You should see an error as follows:
Commands out of sync; you can't run this command now
However, the exception is thrown correctly once you call use_result() or store_result().
$mysqli->multi_query(/* SQLs */);
$mysqli->commit();
$r = $mysqli->use_result(); // Uncaught mysqli_sql_exception: Commands out of sync; you can't run this command now
The reason why MySQL says the commands are out of sync is because you can only execute a single command at a time per each MySQL session. However, multi_query() sends the whole SQL string to MySQL in a single command, and let's MySQL run the queries asynchronously. PHP will initially wait for the first result-set producing non-DML query (not INSERT, UPDATE or DELETE) to finish running before the control is passed back to PHP script. This allows MySQL to run the rest of the SQL on the server and buffer the results, while you can do some other operations in PHP and collect the results later. You can't run another SQL command on the same connection until you iterate over all the results from the previous asynchronous queries.
As pointed out in another answer, commit() will try to execute another command on the same connection. You are getting the out of sync error, because you simply haven't finished processing the multi_query() command.
MySQLi blocking loop
Each asynchronous query should be followed by a blocking loop in mysqli. A blocking loop will iterate over all the executed queries on the server and fetch the results one by one, which you can then process in PHP. Here is an example of such loop:
do {
$result = $mysqli->use_result();
if ($result) {
// process the results here
$result->free();
}
} while ($mysqli->next_result()); // Next result will block and wait for next query to finish
$mysqli->store_result(); // Needed to fetch the error as exception
You must have the blocking loop always, even when you know the queries are not going to produce any result-sets.
Solution
Stay away from multi_query()! Most of the time there's a better way of executing SQL files. If you have your queries separate, then don't concatenate them together, and execute each on their own.
If you really need to use multi_query(), and you would like to wrap it in transaction, you must put the commit() after the blocking loop. All the results need to be iterated over before you can execute the COMMIT; command.
$mysqli->begin_transaction();
$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";
$test = $mysqli->multi_query($sql1);
// $mysqli->commit();
do {
$result = $mysqli->use_result();
if ($result) {
// process the results here
$result->free();
}
} while ($mysqli->next_result());
$mysqli->store_result(); // To fetch the error as exception
$mysqli->commit();
Of course to see any of the mysqli errors you need to enable exception mode. Simply, put this line before new mysqli():
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

Related

Transaction: commit() vs rollBack()

I have some queries at one script and I want to execute either all of them or none of them ..! I've searched about that and I figured out I have to use transaction.
Actually I want to use PDO::beginTransaction. Now there is two approaches.
rollback() function
commit() function
So what's the difference between them? Both of them seems identical to me, So when should I use which one?
<?php
$dbh->beginTransaction();
$sth1 = $dbh->exec("DROP TABLE fruit");
$sth2 = $dbh->exec("UPDATE dessert SET name = 'hamburger'");
$sth3 = $dbh->exec("INSERT INTO names(id, name) VALUES (NULL, 'peter')");
// which one?
$dbh->commit();
// or
$dbh->rollBack();
// ??
/* Database connection is now back in autocommit mode */
?>
Both of them seems identical to me
That's wrong. Transaction by definition is Atomic in nature means either it will happen and succeed executing all commands in the group or none at all. If it's successful and you want to persist the change then COMMIT else if any of the statement in the group fails then ROLLBACK to get back to pristine state.
So in your case, you would want to have all the below statement execute successfully and if that then COMMIT to persist the change but if any of the statement fails for any so called reason then it may end up giving a undesired result which you don't want to persist and so ROLLBACK and get back to previous consistent state.
$sth1 = $dbh->exec("DROP TABLE fruit");
$sth2 = $dbh->exec("UPDATE dessert SET name = 'hamburger'");
$sth3 = $dbh->exec("INSERT INTO names(id, name) VALUES (NULL, 'peter')");
Read about Transaction and also see this another post PHP + MySQL transactions examples
You use commit to perform the transaction, and rollback is the opposite, you use rollback when you want to keep all unchanged (for example if you have detected some error during some step of the transaction).

How to close the result set in mysqli prepared statement, but keep the statement - without buffering entire result?

I'm trying to use multiple prepared statements with the mysqli driver in PHP and getting the infamous error:
Commands out of sync; you can't run this command now
Although I think this database API is atrociously brain-damaged, I do understand why I'm getting the error, as described by this question.
Several solutions are offered in this and various other answers I've found laying around, the only ones I've seen that seem to work involve
$stmt->store_result()
which buffers the entire result set.
However, I don't want to buffer the entire result set - I want to discard it without using (a large amount of) memory to store it!
All I want to do, is create a prepared statement, use bind_param() and bind_result() and fetch() to get some of the results, and then somehow close the results so that I can reuse the statment later and run other queries until then.
I've tried just about every way of getting the actual result object being used from the statement and explicitly closing that, and I've tried calling next_result() until it returns null. Neither of those solutions prevents that error from appearing.
So, how can I close a result set and execute other prepared statements after a fetch(), without trashing the executed statement and having to re-parse the query?
$q = $dbc->prepare("SELECT id, typename, storageclass, tablename FROM _dbtype WHERE typename=?");
$q->bind_param("s", $typeName);
$q->bind_result($id,$typeName,$storageClass,$tableName);
$q->execute();
$q->fetch(); // ie. only once
... <something that does not destroy $q> ...
$q2 = $dbc->prepare("SELECT id, name FROM _storagetype");
see mysqli_stmt::free_result()
(and http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-free-result.html for the hint that "If there is a cursor open for the statement, mysql_stmt_free_result() closes it.")
<?php
mysqli_report(MYSQLI_REPORT_STRICT|MYSQLI_REPORT_ALL);
$mysqli = new mysqli('localhost', 'localonly', 'localonly', 'test');
if ($mysqli->connect_errno) {
trigger_error( sprintf('mysqli connect error (%d) %s', $mysqli->connect_errno, $mysqli->connect_error), E_USER_ERROR);
die;
}
$mysqli->query('CREATE TEMPORARY TABLE sofoo ( id int auto_increment, primary key(id))');
$mysqli->query('INSERT INTO sofoo VALUES (),(),(),(),(),()');
$stmtA = $mysqli->prepare('SELECT id FROM sofoo WHERE id>1');
$stmtB = $mysqli->prepare('SELECT id FROM sofoo WHERE id<10 ORDER BY id DESC');
$stmtA->execute();
$stmtA->bind_result($id);
$stmtA->fetch(); echo $id, PHP_EOL;
$stmtA->free_result(); // without this the script ends with "Fatal error: Uncaught mysqli_sql_exception: Commands out of sync;"
$stmtB->execute();
$stmtB->bind_result($id);
$stmtB->fetch(); echo $id, PHP_EOL;
If there are multiple result sets (i.e. if you have to use next_result()) you must call free_result()for each result set separately.

PDO::query() run into "Cannot execute queries while other unbuffered queries are active."

Maybee some other have the same problem than me.
I run over the error:
Cannot execute queries while other unbuffered queries are active.
Consider using PDOStatement::fetchAll(). Alternatively, if your code
is only ever going to run against mysql, you may enable query
buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.
on PDO. As in many threads mentioned the error can at be at least one of the following problems:
The query cursor was not closed with closeCursor() as mentioned here; Causes of MySQL error 2014 Cannot execute queries while other unbuffered queries are active
There are more than two querys with one statement like mentioned here: PDO Cannot execute queries while other unbuffered queries are active
A bug in mysql-driver as mentioned here: What is causing PDO error Cannot execute queries while other unbuffered queries are active?
In my case all above did not help and it took some time till i solved the problem. this was my code (pseudo-code):
$stmt->startTransaction();
$stmt = db::getInstance()->prepare("CALL phones(:phone)");
$stmt->prepare('SELECT * FROM database');
$stmt->execute();
$aData = $stmt->fetchAll();
$stmt->closeCursor();
$stmt->query("USE sometable;");
After I changed it to:
$stmt->startTransaction();
$stmt = db::getInstance()->prepare("CALL phones(:phone)");
$stmt->prepare('SELECT * FROM database');
$stmt->execute();
$aData = $stmt->fetchAll();
$stmt->closeCursor();
$stmt->exec("USE sometable;");
It worked for my. What is the difference between query and exec?
PDO::exec() - "Execute an SQL statement and return the number of affected rows"
PDO::query() - "Executes an SQL statement, returning a result set as a PDOStatement object"
Why in this case PDO::query() does not work? The cursor IS closed, when called.
While it could conceivably be true that you've encountered the mysql driver bug here, we can't be sure of that because you've not given us that information (what version of PHP are you using? Does it use mysqlnd => check with php -i | grep mysqlnd? What does the rest of your code look like?).
There are many other possible explanations for your problem. I suspect the issue is actually your failing to close all the cursors, and/or fetch all the results, because $stmt is being reused heavily:
Quoted directly from the PDO::query manual page:
If you do not fetch all of the data in a result set before issuing your next call to PDO::query(), your call may fail. Call PDOStatement::closeCursor() to release the database resources associated with the PDOStatement object before issuing your next call to PDO::query().
You call closeCursor on $stmt, that's true, but you've not closed all cursors that have been created by you:
//<-- what is $stmt here?
$stmt->startTransaction();
//no matter, you've reassigned it a PDOStatement instance
$stmt = db::getInstance()->prepare("CALL phones(:phone)");
//Huh? You're preparing yet another query on an instance of PDOStatement?
$stmt->prepare('SELECT * FROM database');
//you're executing this one, though
$stmt->execute();
//and fetching all data
$aData = $stmt->fetchAll();
//and closing this last statement
$stmt->closeCursor();
But what about the first statement you assigned to $stmt (the stored procedure call)? That cursor isn't closed anywhere
Now for the major difference between PDO::query and PDO::exec. Again, quoting the manual:
PDO::exec() does not return results from a SELECT statement.
Whereas:
PDO::query() executes an SQL statement in a single function call, returning the result set (if any) returned by the statement as a PDOStatement object.
I came across this problem too. It is likely to be a bug. If we take the following code, then you will see how it fails with the message 'General error: 2014 Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll().'
$pdo = new \PDO("mysql:host=localhost", "root", "");
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query("USE test");
If you change $pdo->query("USE test"); to $pdo->exec("USE test"); it will work. If you change $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); to $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); it will also work. I haven't been able to find a proper solution yet though.
I solve the issue with steps:
After from performed:
$stmt = db::getInstance()->prepare("CALL phones(:phone)");
I close the:
$stmt->startTransaction();
And after that, I open again the transaction to use the query below:
$stmt->prepare('SELECT * FROM database');
My solve it is: One statement to call the procedure "CALL phones(:phone)" and another to execute the query wtih "SELECT * FROM database".
That is it.
Be careful, This can also happen if you are trying to fetch a non SELECT query (Eg - UPDATE/INSERT/ALTER/CREATE)

PDO “Uncaught exception 'PDOException' .. Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll().”

I know this question has been asked many times, but I've read the answers to many of the questions and still cannot understand why I am receiving this error:
Fatal error: Uncaught exception 'PDOException' with message
'SQLSTATE[HY000]: General error: 2014 Cannot execute queries while
other unbuffered queries are active. Consider using
PDOStatement::fetchAll(). Alternatively, if your code is only ever
going to run against mysql, you may enable query buffering by setting
the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.'
The first thing that is odd, is that I do not get an error on my localhost (wampserver), but I do get it on my web server. The php version on my localhost is 5.3.10, and on my web server it is 5.3.13.
I have read that the source of this error is making a query when data left in the buffer from a previous query. This is not the case for me -- I have echo'd out all of the data and I know for a fact that every row returned in a query is being fetched.
With that said, I have found that changing one of my queries to fetchAll instead of fetch fixes the problem, but it simply makes no since because I know that all of the rows returned are being read. When I used fetchAll for the query (it is being made in a loop), I printed out the array each loop, and only one item was in the array for each query in the loop.
One more piece of information. It's not the query that I changed to fetchAll (which makes the error go away) that throws the PDO error, there is another query later in my php file that throws the error. My file is basically like this:
... code ...
query 1
... code ...
loop
query 2
end loop
... code ...
query 3
If I comment out query 3, there is no error. If I comment out, or change to fetchAll, query 2, there is no error. query 1 has no affect whatsoever.
I would also like to add that I have tried adding LIMIT 1 to all of the queries on the page (at the same time), and the error is still there. I think this proves there is not unread data in the buffer, right?
I'm really confused, so I would appreciate your advice. Before someone asks, I can't post the full code for this, but here is a simplified version of my code:
$stmt = $this->db->prepare('SELECT ... :par LIMIT 1');
makeQuery($stmt, array(':par' => $var));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $this->db->prepare('SELECT ... :par LIMIT 1');
for loop
makeQuery($stmt, array(':par' => $var));
$row2 = $stmt->fetch(PDO::FETCH_ASSOC);
... [use row2] ...
end for loop
$stmt = $this->db->prepare('SELECT ... :par LIMIT 1');
makeQuery($stmt, array(':par' => $var));
$row3 = $stmt->fetch(PDO::FETCH_ASSOC);
Here is makeQuery().
/**************************************************************************************************************
* Function: makeQuery *
* Desc: Makes a PDO query. *
* Pre conditions: The statement/query and an array of named parameters (may be empty) must be passed. *
* Post conditions: The PDO query is executed. Exceptions are caught, displayed, and page execution stopped. *
**************************************************************************************************************/
function makeQuery($stmt, $array, $errMsg = '')
{
try
{
$stmt->execute($array);
}
catch (PDOException $e)
{
print $errMsg != ''?$errMsg:"Error!: " . $e->getMessage() . "<br/>";
die();
}
}
Thanks for your help!
EDIT: I also tried doing the following after query 2 (since that seems to be the source of the problem:
$row2 = $stmt->fetch(PDO::FETCH_ASSOC); var_dump($row2);
The output was:
bool(false)
Have I stumbled across a PDO bug?
You need to fetch until a row fetch attempt fails. I know you may only have one row in the result set and think one fetch is enough, but its not (when you're using unbuffered queries). PDO doesn't know how many rows there are until it reaches the end, where it tries to fetch the next row, but it fails.
You probably have other statements where you didn't fully "fetch until a fetch failed". Yes, I see that you fetch until the fetch failed for one of the statements, but that doesn't mean you did it for all of them.
To clarify -
When you execute a query via execute(), you create a result set that must be fetched from the db into php. PDO can only handle 1 of these "result set in progress of being fetched" at a time (per connection). You need to completely fetch the result set, all the way to the end of it, before you can start fetching a different result set from a different call to execute().
When you "call fetch() until a fetch() fails", the fact that you reached the end of the results is internally noted by PDO when that final call to fetch() fails due to there being no more results. PDO is then satisfied that the results are fully fetched, and it can clean up whatever internal resources between php and the db that were established for that result set, allowing you to make/fetch other queries.
There's other ways to make PDO "call fetch() until a fetch() fails".
Just use fetchAll(), which simply fetches all rows, and so it will hit the end of the result set.
or just call closeCursor()
*if you look at the source for closeCursor(), the default implementation literally just fetches the rows and discards them until it reaches the end. It's written in c obviously, but it more or less does this:
function closeCursor() {
while ($row = $stmt->fetch()) {}
$this->stmtFullyFetched = true;
}
Some db drivers may have a more efficient implementation that doesn't require them to fetch lots of rows that nobody cares about, but that's the default way PDO does it. Anyway...
Normally you don't have these problems when you use buffered queries. The reason is because with buffered queries, right after you execute them, PDO will automatically fully fetch the db results into php memory, so it does the "call fetch() until a fetch() fails" part for you, automatically. When you later call fetch() or fetchAll() yourself, it's fetching results from php memory, not from the db. So basically, the result set is immediately fully fetched when using buffered queries, so there's no opportunity to have more than 1 "result set in progress of being fetched" at the same time (because php is single threaded, so no chance of 2 queries running at the same time).
Given this:
$sql = "select * from test.a limit 1";
$stmt = $dbh->prepare($sql);
$stmt->execute(array());
Ways to fully fetch the result set (assuming you only want the first row):
$row = $stmt->fetch();
$stmt->closeCursor();
or
list($row) = $stmt->fetchAll(); //tricky
or
$row = $stmt->fetch();
while ($stmt->fetch()) {}
After struggling with this issue for days, I finally found that this worked for me:
$db = new PDO ($cnstring, $user, $pwd);
$db->setAttribute (PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
This also happen if you are trying to fetch a non SELECT query (Eg - UPDATE/INSERT/ALTER/CREATE). Make sure to use fetch or fetchAll only for SELECT queries.
Possible Duplicate Answer/Question

PHP's PDO Prepare Method Fails While in a Loop

Given the following code:
// Connect to MySQL up here
$example_query = $database->prepare('SELECT * FROM table2');
if ($example_query === false) die('prepare failed');
$query = $database->prepare('SELECT * FROM table1');
$query->execute();
while ($results = $query->fetch())
{
$example_query = $database->prepare('SELECT * FROM table2');
if ($example_query === false) die('prepare failed'); //it will die here
}
I obviously attempt to prepare statements to SELET everything from table2 twice. The second one (the one in the WHILE loop) always fails and causes an error.
This is only on my production server, developing locally I have no issues so it must be some kind of setting somewhere.
My immediate thought is that MySQL has some kind of max_connections setting that is set to 1 and a connection is kept open until the WHILE loop is completed so when I try to prepare a new query it says "nope too many connected already" and shits out.
Any ideas?
EDIT: Yes I know there's no need to do it twice, in my actual code it only gets prepared in the WHILE loop, but like I said that fails on my production server so after some testing I discovered that a simple SELECT * query fails in the WHILE loop but not out of it. The example code I gave is obviously very watered down to just illustrate the issue.
Problem was that I couldn't do something while an unbuffered query was running (which it was in the WHILE loop)
Solution was to add the following line to ensure buffered queries were being used:
$database->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,true);
There shouldn't be a need to prepare $example_query more than once. Simply execute the query inside your loop.
EDIT: If you must prepare a new query in each loop iteration, an explicit $example_query->closeCursor() at the end of the loop should free any resources associated with the statement object.

Categories