From mysql table at first want to delete row. Only after the row deleted, i want to select sum(certain_column). Want to be sure that until row deleted, i do not execute select.
Decided to use such code like this:
try{
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$db->beginTransaction();
try{
$stmt_delete = $db->prepare('DELETE FROM `table` WHERE `Id` = 1;');
$stmt_delete_items->execute( );
}
catch (PDOException $e){
echo "DataBase Error: " .htmlspecialchars( $e->getMessage() , ENT_QUOTES, "UTF-8").'<br>';
$db->rollBack();
exit;
}
try{
$stmt_select = $db->prepare('SELECT SUM(`Quantity`) AS `SumQuantity` FROM `table` WHERE `some_column` = 234;');
$stmt_select->execute( );
$arr_select = $stmt_select->fetchAll(PDO::FETCH_ASSOC);
}
catch (PDOException $e){
echo "DataBase Error: " .htmlspecialchars( $e->getMessage() , ENT_QUOTES, "UTF-8").'<br>';
}
$db->commit();
}
Expecting at first execute $stmt_delete and only then $stmt_select. In usual case i get such execution order. But is it guaranteed 100%? Somewhere read that delete may take more time than select... with exit; i prevent next execution only in case of error. But i need to delay $stmt_select until $stmt_delete.
Or may be better to use START TRANSACTION; ... COMMIT;. Or may be for DELETE to use LOCK TABLES ... UNLOCK TABLES.
Please advice solution to be 100% sure that select executes only after delete is finished.
Related
As far as I understood transaction starts once we call $mysqli->autocommit(FALSE); statement and ends after calling $mysqli->commit(); command like in the example below.
<?php
//Start transaction
$mysqli->autocommit(FALSE);
$mysqli->query('UPDATE `table` SET `col`=2');
$mysqli->query('UPDATE `table1` SET `col1`=3;');
$mysqli->commit();
//End transaction
//Executing other queries without transaction control
$mysqli->query("Select * from table1");
$mysqli->query("Update table1 set col1=2");
//End of executing other queries without transaction control
//Start transaction
$mysqli->autocommit(FALSE);
$mysqli->query('UPDATE `table` SET `col`=2');
$mysqli->query('UPDATE `table1` SET `col1`=3;');
$mysqli->commit();
//End transaction
?>
Have I understood correctly? If not could you please correct me, because it is actually my first time using transactions in real life.
Thank you.
Update Novembre 2020: #Dharman gave a better answer with more details about transactions in mysqli, just check it instead: https://stackoverflow.com/a/63764001/569101 👇
Well according to the php doc, you're right.
<?php
$mysqli = new mysqli("localhost", "my_user", "my_password", "world");
/* check connection */
if (mysqli_connect_errno()) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}
$mysqli->query("CREATE TABLE Language LIKE CountryLanguage");
/* set autocommit to off */
$mysqli->autocommit(FALSE);
/* Insert some values */
$mysqli->query("INSERT INTO Language VALUES ('DEU', 'Bavarian', 'F', 11.2)");
$mysqli->query("INSERT INTO Language VALUES ('DEU', 'Swabian', 'F', 9.4)");
/* commit transaction */
$mysqli->commit();
/* drop table */
$mysqli->query("DROP TABLE Language");
/* close connection */
$mysqli->close();
?>
In the example above:
the CREATE TABLE is auto committed because it's the default behaviour.
the INSERT INTO aren't auto committed because of the autocommit(FALSE).
the DROP TABLE is auto committed because the autocommit(FALSE) was reset by the ->commit();.
j0k is mainly right, except in the drop table.
The auto commit is not turned on with the ->commit()
Instead, the DROP TABLE is a DDL query, and DDL queries are always implicitly committed and will commit all your previously non committed work.
So, if you did not commit the work, the DDL query would force this commit.
How to use transactions in mysqli?
Prerequisite
In order for the transactions to behave properly you should enable exception error reporting. Otherwise mysqli will not report errors and the transaction will not be performed correctly. Alternatively, you could manually check each query, but that is not recommended. To connect properly with mysqli use the following 3 lines:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'user', 'pass', 'dbname');
$mysqli->set_charset('utf8mb4'); // always set the charset
Transactions only work with transactional tables. Make sure that your table storage engine supports transactions. For example, MyISAM ignores the commit/rollback.
Transactions
There are two possible ways to create a transaction using mysqli. By default all queries/statements are committed as soon as they are performed. You can either switch autocommit off or use a one-time-only transaction.
Transactions are committed to the database in the following situations:
when calling commit
after setting autocommit=1
when starting another transaction
when performing DDL query
and in a few other situations. For more information see Statements That Cause an Implicit Commit
Using autocommit(false)
If you turn autocommit off, you decide when you want to commit, but calling commit() does not switch autocommit back on.
//Start transaction
$mysqli->autocommit(false);
$mysqli->query('INSERT INTO director(name) VALUE("Steven Spielberg")');
$directorId = $mysqli->insert_id;
$movieTitle = 'Jurassic Park';
$stmt = $mysqli->prepare('INSERT INTO movie(title, directorId) VALUE(?,?)');
$stmt->bind_param('ss', $movieTitle, $directorId);
$stmt->execute();
$mysqli->commit();
// Changes are committed, but autocommit is not switched back on
// Following queries are still transactional.
// They will not be committed unless you call commit or switch autocommit back on
$mysqli->query('INSERT INTO director(name) VALUE("James Cameron")');
$directorId = $mysqli->insert_id;
$movieTitle = 'Titanic';
$stmt = $mysqli->prepare('INSERT INTO movie(title, directorId) VALUE(?,?)');
$stmt->bind_param('ss', $movieTitle, $directorId);
$stmt->execute();
$mysqli->autocommit(true);
// All queries are committed and everything that follows will be immediately committed.
Using begin_transaction()
You can start a one-time-only transaction using begin_transaction(). This does not set autocommit=false so when you call commit() you end the transaction without starting a new one.
//Start transaction
$mysqli->begin_transaction();
$mysqli->query('INSERT INTO director(name) VALUE("Steven Spielberg")');
$directorId = $mysqli->insert_id;
$movieTitle = 'Jurassic Park';
$stmt = $mysqli->prepare('INSERT INTO movie(title, directorId) VALUE(?,?)');
$stmt->bind_param('ss', $movieTitle, $directorId);
$stmt->execute();
$mysqli->commit();
// Changes are committed and the transaction has ended
// Following queries will be committed one by one as soon as they are peformed.
$mysqli->query('INSERT INTO director(name) VALUE("James Cameron")');
$directorId = $mysqli->insert_id;
$movieTitle = 'Titanic';
$stmt = $mysqli->prepare('INSERT INTO movie(title, directorId) VALUE(?,?)');
$stmt->bind_param('ss', $movieTitle, $directorId);
$stmt->execute();
Performing DDL statements
Some SQL statements trigger an explicit commit but do not affect the value of autocommit.
//Start transaction
$mysqli->autocommit(false);
$mysqli->query('INSERT INTO director(name) VALUE("Steven Spielberg")');
$directorId = $mysqli->insert_id;
$movieTitle = 'Jurassic Park';
$stmt = $mysqli->prepare('INSERT INTO movie(title, directorId) VALUE(?,?)');
$stmt->bind_param('ss', $movieTitle, $directorId);
$stmt->execute();
// The following will call commit but it will not set autocommit=true
$mysqli->query('TRUNCATE TABLE movie_genre');
// if you want to switch autocommit back on, you have to call:
$mysqli->autocommit(true);
Rollback
If an exception occurs then PHP will end execution of the script and the code will never reach the commit statement. However, in some situations, you might want to roll back the transaction explicitly, for example to avoid calling commit accidentally somewhere else in the code.
Here is an example of what such a transaction would look like. The second query tries to insert into a non-existent table which means that mysqli will throw an exception. Instead of letting PHP script die, we catch the exception and roll back the transaction. The value 4 will never be inserted into the database because both queries were rolled back.
try {
// Start transaction
$mysqli->begin_transaction();
$mysqli->query('INSERT INTO some_table(col2) VALUE(4)');
$mysqli->query('INSERT INTO does_not_exist(col2) VALUE(4)');
// Commit changes
$mysqli->commit();
} catch (\Throwable $e) {
// Something went wrong. Rollback
$mysqli->rollback();
// Rethrow the exception so that PHP does not continue
// with the execution and the error can be logged in the error_log
throw $e;
}
Prepare SQL statement ONCE, and then execute it SEVERAL times:
<?php
$Mysqli = new mysqli("host","user","pass","base");
// check connection
if(mysqli_connect_errno())
{
printf("Connect failed: %s\n",mysqli_connect_error());
exit();
}
// some data for db insertion
$countries=['Austria','Belgia','Croatia','Denmark','Estonia'];
// explicitly begin DB transaction
$Mysqli->begin_transaction();
// prepare statement (for multiple inserts) only once
$stmt=$Mysqli->prepare("INSERT INTO table(column) VALUES(?)");
// bind (by reference) prepared statement with variable $country
$stmt->bind_param('s',$country);
// load value from array into referenced variable $country
foreach($countries as $country)
{
//execute prep stat more times with new values
//$country is binded (referenced) by statement
//each execute will get new $country value
if(!$stmt->execute())
{
// rollback if prep stat execution fails
$Mysqli->rollback();
// exit or throw an exception
exit();
}
}
// close prepared statement
$stmt->close();
// commit transaction
$Mysqli->commit();
// close connection
$Mysqli->close();
?>
You think that commit automatically switches autocommit back to true?
A comment in the PHP Doc says NO!
I have a simple SQL Query:
$stmt = $db_main->prepare("SELECT id FROM user WHERE username=? AND mail=? LIMIT 1");
$stmt->bind_param('ss', $username, $mail);
$stmt->execute();
And I want to know, if it found an user. So I want to count the rows found.
I already tried to use rowCount (Not safe for SELECT) or num_rows or just looking if the result id is numeric (Which '' would not be, I hoped...)
There has to be an easy way to count the selected row, hasn't be?
Check number of rows returned with:
$stmt->num_rows;
Check for instance this site.
p.s.: added as per question in comment: use fetch() in order to get the next record.
...
$stmt->execute();
$stmt->bind_result($user_id); // access id
$stmt->store_result(); // optional: buffering (see below)
if ($data = $stmt->fetch()) {
do {
print("Id: " . $user_id);
} while ($data = $stmt->fetch());
} else {
echo 'No records found.';
}
Regarding store_result() from the documentation:
"You must call mysqli_stmt_store_result() for every query ..., if and only if you want to buffer the complete result set by the client ..."
Basically, I have a mysql database where every time I get a row, I want to delete it from the database (after reading its information).
I know I could do something like
$result = mysql_query(" SELECT <some row> FROM table ");
$row = mysql_fetch_array($result);
$id = $row["id"];
mysqli_query(" DELETE FROM table WHERE id=$id");
but now it seems I have two queries going on. Is there a command to tell mysql that I want the row deleted as soon as it gives me the information? I imagine that'd save time and resources.
In my head, it looks like
$result = mysql_query(" SELECT <some row> FROM table THEN DELETE ");
EDIT additional information: I wish to use the SELECTed information after deleting the row. To put it simply, I only want one instance of the information to exist at any give time; it would be as if I were only "moving" a physical copy of the information, so that when it is put on a device/what have you, it is no longer in the table since there is only one copy.
Sorry if my understanding of mysql is rough -- I'm pretty new to it :P
I don't know why you need it but you can use a stored procedure for that:
DELIMITER $$
CREATE PROCEDURE select_and_delete(IN aid INT)
BEGIN
SELECT * FROM table1 WHERE id = aid;
DELETE FROM table1 WHERE id = aid;
END$$
DELIMITER ;
Here is SQLFiddle demo.
mysql_* extension is deprecated, therefore use prepared statements and mysqli or PDO.
Your php code using PDO might look like
$id = 1;
try {
$db = new PDO('mysql:host=localhost;dbname=test;charset=UTF8', 'user', 'userpwd');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$query = $db->prepare("CALL select_and_delete(?)");
$query->execute(array($id));
$result = $query->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "Exception: " .$e->getMessage();
$result = false;
}
$query = null;
$db = null;
//do whatever you need to do with your resultset
var_dump($result);
Following a rather simplified table structure (with the only column id) presented in SQL Fiddle example if you call it with $id=1 you'll you'll get in $result:
array(1) {
[0]=>
array(1) {
["id"]=>
int(1)
}
}
You'll need to add a timestamp field (with default as CURRENT_TIMESTAMP) to be able to tell when the row was added.
Then you can run the following MySQL query.
DELETE FROM `table` WHERE `timestamp` > DATE_SUB(NOW(), INTERVAL 0 SECOND);
You will need to run this as a cron job though. AFAIK it can't be done in MySQL alone.
The second SQL statement below returns an error in phpMyAdmin:
SET #num=2000040;
INSERT INTO artikel( artikel_nr, lieferant_nr, bezeichnung_1, bezeichnung_1 )
SELECT #num := #num +1 AS anum, 70338, f2, f3
FROM import
WHERE id >1
MySQL says:
#1110 - Column 'bezeichnung_1' specified twice
All correct. But when I run the queries in Symfony 1.4 with this function:
// run sql query
// http://erisds.co.uk/symfony/snippet-creating-debugging-complex-sql-queries-in-symfony
// http://stackoverflow.com/questions/5434702/php-quick-refactoring
// param $sql: the query to run
// param $silent: bool if errors should be ignored
// throws: pdo error info if statement failed and $silent=false
// returns: pdo-statement (use for looping over result rows and error messages)
public static function runQuery($sql, $silent=false)
{
$conn = Propel::getConnection();
$pdo_statement = $conn->prepare($sql);
$error = null;
try
{
$pdo_statement->execute();
}
catch (Exception $e)
{
$error = $e->getMessage();
}
if ( !$error )
{
$pdo_error = $pdo_statement->errorInfo();
$error = $pdo_error[2];
}
if ( !$silent && $error ) throw new Exception($error);
return $pdo_statement;
}
no error is thrown. The two SQL statements must be submitted at the same time since they depend on each other. The faulty query is constructed from user input. I need to get that error back, otherwise I can't tell if the database was changed, and I can't tell the user about it.
Do you know why PDO doesn't complain about the invalid statement, and if it can't be made to do so, how to get the success/failure information?
BTW the query does update the database if there are no duplicate columns.
Here's the link to the PDOStatement class: http://www.php.net/manual/en/class.pdostatement.php
This is expected behavior. Since there are two statements and the first one is valid, you have to use nextRowset()
try
{
$pdo_statement->execute();
while ($pdo_statement->nextRowset()) {/* https://bugs.php.net/bug.php?id=61613 */};
}
Source: bugs.php.net/bug.php?id=61613
By default PDOStatement::execute() doesn't throw any exception, it simply returns false on error. You have to set error handling to PDO::ERRMODE_EXCEPTION through db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION).
If you have the option to use mysqli instead of PDO for the multi query, you can use mysqli_multi_query. As error handling is a little complex, here is my function:
/**
* Executes multiple queries at once (separated by semicolons) and discards the results
*
* #param string $sql
* #throws RuntimeException if a query failed
*/
function multi_query($sql) {
$mysqli = new mysqli('host', 'user', 'password', 'database');
//Execute all queries
$index = 1;
if ($mysqli->multi_query($sql)) {
do {
// next_result() fails if store_result() is not called
if ($result = $mysqli->store_result()) {
$result->free();
}
$has_next = $mysqli->more_results();
if (!$has_next)
return; // all queries successfully executed
$index++;
} while ($mysqli->next_result());
}
// At this point, either the multi_query() has returned false - which is
// when the first query failed - or more_results() was true, while next_result()
// returned false - which is when a different query failed.
$error = $mysqli->error;
if (!$error)
$error = $mysqli->errno ? "errno $mysqli->errno" : '(unknown error)';
throw new RuntimeException("mysqli query $index failed: $error");
}
I've run into an issue that I'm hoping to get a little help on. I'm using the following:
Kohana 3.0.7
PostgreSQL 8.4
Transactions in PostgreSQL using
$db->query(NULL, 'BEGIN', FALSE)
$db->query(NULL, 'ROLLBACK', FALSE);
$db->query(NULL, 'COMMIT', FALSE);
The issue is that when I send a query to the database that results in a postgres error within a transaction my system freezes up. When I send the same query to the database without wrapping it in a transaction the PDO error is reported back just as expected. Here is an exmaple:
This first example works fine and duplicate key value violates unique constraint "pk_test_table" error is returned:
$query = DB::query(Database::INSERT, 'INSERT INTO test_table (test_table_id, test_table_val) VALUES (:id, :value)';
$query->param(':id', 1);
$query->param(':value', "test value");
try
{
$result = $query->execute($db);
}
catch (Exception $e)
{
echo 'Caught exception: ', $e->getMessage(), "\n";
}
This second example causes my system to freeze (I can't tell if it's an infinite loop, or some other freeze):
$db->query(NULL, 'BEGIN', FALSE);
$query = DB::query(Database::INSERT, 'INSERT INTO test_table (test_table_id, test_table_val) VALUES (:id, :value)';
$query->param(':id', 1);
$query->param(':value', "test value");
try
{
$result = $query->execute($db);
}
catch (Exception $e)
{
echo 'Caught exception: ', $e->getMessage(), "\n";
}
$db->query(NULL, 'ROLLBACK', FALSE);
As you can see the only difference is that the second example is wrapped in a transaction.
Any ideas on what is going on? Any suggestions for things to try?
I found a way to work around the issue. Not sure if this is a bug in PDO or some other part of the tool set but what I'm doing to work around is the following:
$exception_exists = FALSE;
$db->query(NULL, 'BEGIN', FALSE);
$query = DB::query(Database::INSERT, 'INSERT INTO test_table (test_table_id, test_table_val) VALUES (:id, :value)';
$query->param(':id', 1);
$query->param(':value', "test value");
try
{
$result = $query->execute($db);
}
catch (Exception $e)
{
echo 'Caught exception: ', $e->getMessage(), "\n";
$exception_exists = TRUE;
}
if (!$exception_exists)
{
$db->query(NULL, 'ROLLBACK', FALSE);
}
By adding the variable $exception_exists in the catch I can then act of that if there is no exception. If there is an exception and I try to ROLLBACK or COMMIT then I get the freezing behavior.
This works for now but I wouldn't call it elegant.