PHP PDO Transaction with nested PHP check - php

i have to do 2 MySql-queries:
SELECT id FROM X WHERE [...]
INSERT [...]
The second query should only be executed, if the first query returns an correct id.
Is it possible, to mix PHP conditions between both queries?
Eg.
try
{
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh->beginTransaction();
$stmt = $dbh->prepare("SELECT id FROM [...]");
$stmt->bindParam(1, [...]);
if($stmt->execute())
{
if($row = $stmt->fetch())
{
$matchID = $row['id'];
$checkD = $this->checkId($matchID);
if($checkD)
{
return '-1';
}
else
{
$stmt = $dbh->prepare("INSERT INTO [...]");
$stmt->bindParam(1,[...]);
$stmt->execute();
stmt = $dbh->prepare("DELETE [...]");
$stmt->bindParam(1,[...]);
$stmt->execute();
$dbh->commit();
return $matchID;
}
}
else
{
return '-1';
}
}
else
{
return '-1';
}
} catch(Exception $e)
{
$dbh->rollBack();
return '-1';
}
Is this correct? (i get zero errors)
If not: how can i realize it?
I want to be sure, that no other user could reach the INSERT query, when annother is performing the 1. query.

Transactions are isolated from the current data. How they behave exactly is dependent on the isolation level they use. For example a transaction with serializable isolation level completely lives in the past, and it knows nothing of the data changes have been made since the beginning of the transaction.
If you want to prevent anybody to do changes on the database while your script is working on something, then you have to lock your database, tables or rows. This is usually not necessary, with the proper code.
In your case
you can use read committed transaction isolation level (default) call the DELETE after the SELECT, and check whether there are affected rows by the DELETE before the INSERT
if you don't want to change the order of your queries then you can
simply throw an exception if the DELETE does not affect any row, and so the INSERT will be rolled back
add a constraint which prevents multiple INSERTs with the same data, so the duplicated INSERT will violate a constraint, and it will be rolled back
lock the rows with a SELECT FOR UPDATE

This rather makes no sense. there is no use for transactions at all. What you want to roll back? a SELECT query result?
I want to be sure, that no other user could reach the INSERT query, when another is performing the 1. query.
This is achieved via table locking, not transactions. Or, rather by the simple key uniqueness. Without knowing your business logic it's impossible to answer more. Better ask another question, explaining user experience, and avoiding technical terms like "transaction" at all.

Related

Multiple queries with Firebird and PHP 7.3

It's my first time working with Firebird database and I need some help in the transactions department. I need to run multiple functions inside a ''main function'', those functions have queries, but if one fails, I need them all to rollback. Can I open the transaction in the top of the main function and close it at the bottom or do I have to open and close for each query? I'll post an example.
public function Main_function()
{
$id = $this->create_user_id();
$connection = ibase_connect($this->_db, $this->_username, $this->_password, '100');
$trans = ibase_trans($connection, IBASE_READ+IBASE_COMMITTED+IBASE_REC_NO_VERSION);
$query = "INSERT INTO USERS (user_id, name) VALUES ('john', '$id')";
$newuser = ibase_query($trans, $query);
$return = $this->insert_new_job($id);
ibase_commit($trans);
ibase_close($connection);
}
public function create_user_id()
{
$id = '2';
return $id;
}
public function insert_new_job($id)
{
///DO I NEED TO OPEN A NEW TRANSACTION OR THE OTHER IS STILL ACTIVE?
$query = "INSER INTO jobs (name, id, job) VALUES ('john',$id,'developer') ";
$result = ibase_query($trans, $query);
return $result;
}
If you want all statements executed in a single transaction, then you need to start the transaction once, use it for all statements you want in that transaction, and then commit at the end of the unit work. In other words: you should not start a transaction for each individual statement.
Starting a transaction for each statement would not have your desired result, as each would be executed in its own transaction, which is - by your own words - not what you want. In addition, as Firebird supports multiple active transactions on a single connection, you would have the added problem that you need to ensure that you keep track of all transactions handles to properly commit or rollback, as starting a new transaction will not automatically end a previous transaction.
As an aside, I don't program in PHP, but it looks like $connection and $trans (and other variables) are global variables, and it would be better to declare them as local variables and pass them to your query methods instead of relying on access to global variables.

How to check a condition if it was true then insert some thing at race condition

We have an api function which check a condition on database with a select-query then if it was true we want just for one time insert some thing to database for example inserting to database that insertion done. Problem is when we call multiple times this api-function concurrently race condition happen, in another words assume we call this function 2 times, first request check the condition it's true then second request check that and it's true again so their do insert to database. But we want to when we check condition no other one can check it again until we do insertion.
We use php/Laravel and know about some ways like using insert into ... select or using some thing like replace into ... and so on.
$order = Order::find($orderId);
$logRefer = $order->user->logrefer;
if (!is_null($logRefer) && is_null($logRefer->user_turnover_id)) {
$userTurnover = new UserTurnover();
$userTurnover->user_id = $logRefer->referrer_id;
$userTurnover->order_id = $order->id;
$userTurnover->save();
$logRefer->order_id = $order->id;
$logRefer->user_turnover_id = $userTurnover->id;
$logRefer->save();
}
If logrefer not found set it and corresponding user-turnover just for one time. We expect to see just one user-turnover related to this order but after running it multiple time concurrently we see multiple user-turnover has inserted.
I usually take advantage of transaction when operations need to be sequential but i think that in your case the situation it's a bit complex due to the fact that also the condition need to be evaluated conditionally if the function it's running. So the idea that i can give you it's to have on the database a variable (another table) used as semaphore which allow or not to perform actions on the table (condition gived by the fact that you set or unset the value of the semaphore). I think as good programmer that semaphore are useful in a lot of cases of concurrential functions.
The database should have a unique key on columns expected to be unique, even if some mechanism in the code prevents duplicates.
Wrap connected queries into a transaction which will fail and roll back in a race event condition
try {
DB::transaction(function() {
$order = Order::find($orderId);
...
$logRefer->save();
});
} catch (\Illuminate\Database\QueryException $ex) {
Log::error(“failed to write to database”);
}

Checking for foreign key restrictions errors without mysqli_report

For the following comment (source)
PHP does not report mysqli or PDO errors by default because that information is highly sensitive, displaying it to a user is a great way to learn how to inject malicious data.
MYSQLI_REPORT_ERROR tells it to turn on the errors and MYSQLI_REPORT_STRICT tells it to convert those errors into Exceptions. This will give you a full report of the error message, so never do this in production environments.
How would one check for foreign key constraint errors in production to notify the user that he or she is, for example, not able to delete a record until all associated records has been deleted?
As a general guideline, exceptions should not be used for control flow. What you should rather do is guard your DELETE statements inside if statements, i.e.:
<?php
$mysqli = new mysqli(/* blah */);
$parent_id = /* id of parent record that we're trying to delete */;
if ($stmt = $mysqli->prepare("select count(1) from child_table where parent_id = ?")) {
$stmt->bind_param("i", $parent_id);
$stmt->execute();
$stmt->bind_result($child_row_count);
$stmt->fetch();
$stmt->close();
if ($child_row_count == 0) {
// there is no dependant object, so we can delete
}
else {
die("Can't delete object, it has child objects linked to it");
}
}
?>
An alternative would be to use cascading deletes to automatically remove child data, in which case you no longer need to check for orphans.

Call to mysqli::query and mysqli::execute don't return errors, but aren't executed in the database

I'm trying to execute a DROP TEMPORARY TABLE statement. I've run the statement using MySQL Workbench and it works there. I've made sure I'm successfully logged in as the same user in both PHP and MySQL Workbench.
I've tried using both mysqli::query() and mysqli::prepare then mysqli::execute() methods. Neither works when I try to execute the DROP TEMPORARY TABLE statement. However, both work when I execute either an INSERT or SELECT statement.
This code doesn't return error, but also doesn't execute in the database:
public function executeSQL()
{
// SQL statement using query method
if (!$this->mySQLi->query("DROP TEMPORARY TABLE IF EXISTS TIME_INTERVAL_DATA") )
{
$this->writeSQLErrorToLog("Query method returned an error.");
}
if(!$this->mySQLi->commit())
{
// Committing the transaction failed.
$this->writeSQLErrorToLog("Commit method returned an error.");
}
// SQL statement using prepare and execute methods
$stmt = $this->mySQLi->prepare("DROP TEMPORARY TABLE IF EXISTS TIME_INTERVAL_DATA");
if($stmt)
{
if(!$stmt->execute())
{
// Execute method returned an error.
$this->writeSQLErrorToLog("Execute method returned an error.");
$this->throwMySQLErrorAndCloseStatement($stmt);
}
} else {
// Prepare method returned an error.
$this->writeSQLErrorToLog("Prepare method returned an error.");
return;
}
// ...
}
However, the following code does execute in the database:
$stmt = $this->mySQLi->prepare("INSERT INTO TABLE1 (ID) VALUES (1)");
if(!$stmt->execute()) {
// Return the MySQL Error message and number
$this->throwMySQLErrorAndCloseStatement($stmt);
}
I've been reading as many of the MySQL and PHP docs as I can find. I've read and re-read the docs on all of the above mysqli methods. And I've read about and tried variations of using mysqli::escape_string(), but when I use it an error is returned. I've also tried many variations of different SQL statements.
In a nutshell, all of the INSERT and SELECT statements work. But, the DROP TEMPORARY TABLE statement never works and never returns an error. I'm really scratching my head on this one. Any help would be much appreciated!!

PDO error checking for SQL queries

I am trying to add more error outputs to my code as it is growing. But I am not sure how deep I need to go. For instance, if I send a PDO DELETE FROM command to a table, is it necessary for me to then query the table again to see if the row was deleted (and confirm to the user)? Or should I rely on error handling, as in, if there was no error, it was definitely successful.
Thanks.
You should wrap every SQL statement in a try and catch block to see if any SQL exceptions or any other exceptions occured.
Depending on how you execute your PDO queries you have two options.
You can execute a statement directly with the PDO class using the exec() function. This function will return the number of affected rows for each query. However this function does not return the number of affected rows for an SELECT query.
So for delete queries you would use
$connection = new PDO($dsn, $username, $password);
$affectedRows = $connection->exec('DELETE from users WHERE id = 1);
This way affectedRows will probably be 1 or 0 if the user doesn't exist. If any other errors occur the catch part of the try-catch block will make sure to fetch these.
If you want to execute an SELECT query and find out if it was successful I would recommend doing this
$connection = new PDO($dsn, $username, $password);
$statement = $connection->prepare(SELECT * FROM users);
$statement->execute();
$returnedUsers = $statement->fetchAll();
As you can see, calling the method $connection->prepare() will return a statement which is an object of the PDOStatement class.
If you have parameters set in the prepare query you would want to set these through the bindParam() methods of the PDOStatement class.
Keep in mind that when it's neccesary to execute multiple similar queries in one go use an object of PDOStatement and then set different parameters using the bindParam() method.

Categories