Cant the prepared statement be used throught the transaction, from php? - php

I work on a LAPP environment (linux apache postgresql php), and i'm just triyn to find out how to use the prepared statement within the transaction (if is possible).
I hope code will explain better then words:
Example 1, simple transaction:
BEGIN;
INSERT INTO requests (user_id, description, date) VALUES ('4', 'This dont worth anything', NOW());
UPDATE users SET num_requests = (num_requests + 1) WHERE id = '4';
--something gone wrong, cancel the transaction
ROLLBACK;
UPDATE users SET last_activity = NOW() WHERE id = '4'
COMMIT;
In the example above, if i undestood right the transaction, the only effect in the database will be the last_activity's update... ye?
If i try to use that transaction in php (both with PDO or pg_ methods) the code should look like that (example 2):
/* skip the connection */
pg_query($pgConnection, "BEGIN");
pg_query($pgConnection, "INSERT INTO requests (user_id, description, date) VALUES ('$id_user', 'This dont worth anything', NOW())");
pg_query($pgConnection, "UPDATE users SET num_requests = (num_requests + 1) WHERE id = '$id_user'");
//something gone wrong, cancel the transaction
pg_query($pgConnection, "ROLLBACK");
pg_query($pgConnection, "UPDATE users SET last_activity = NOW() WHERE id = '$id_user'");
pg_query($pgConnection, "COMMIT");
And that works fine. Maybe ugly to see, but seem to work (suggestion are always welcome)
Anyway, my problem come when i try to envolve the example 2 with the prepared statements (i know that in the example 2 the use of prepared statements is not very usefull)
Example 3:
/* skip the connection */
pg_prepare($pgConnection, 'insert_try', "INSERT INTO requests (user_id, description, date) VALUES ('$1', '$2', $3)");
pg_query($pgConnection, "BEGIN");
pg_execute($pgConnection, 'insert_try', array($user_id, 'This dont worth anything', date("Y-m-d")));
/* and so on ...*/
Well, the example 3 simply dont work, the prepared statement will be effective if the transaction due in rollback.
So, the prepared statements can't be used in the transaction, or am i taking the wrong way?
EDIT:
After some try with PDO, i'm arrived at this point:
<?php
$dbh = new PDO('pgsql:host=127.0.0.1;dbname=test', 'myuser', 'xxxxxx');
$rollback = false;
$dbh->beginTransaction();
//create the prepared statements
$insert_order = $dbh->prepare('INSERT INTO h_orders (id, id_customer, date, code) VALUES (?, ?, ?, ?)');
$insert_items = $dbh->prepare('INSERT INTO h_items (id, id_order, descr, price) VALUES (?, ?, ?, ?)');
$delete_order = $dbh->prepare('DELETE FROM p_orders WHERE id = ?');
//move the orders from p_orders to h_orders (history)
$qeOrders = $dbh->query("SELECT id, id_customer, date, code FROM p_orders LIMIT 1");
while($rayOrder = $qeOrders->fetch(PDO::FETCH_ASSOC)){
//h_orders already contain a row with id 293
//lets make the query fail
$insert_order->execute(array('293', $rayOrder['id_customer'], $rayOrder['date'], $rayOrder['code'])) OR var_dump($dbh->errorInfo());
//this is the real execute
//$insert_order->execute(array($rayOrder['id'], $rayOrder['id_customer'], $rayOrder['date'], $rayOrder['code'])) OR die(damnIt('insert_order'));
//for each order, i move the items too
$qeItems = $dbh->query("SELECT id, id_order, descr, price FROM p_items WHERE id_order = '" . $rayOrder['id'] . "'") OR var_dump($dbh->errorInfo());
while($rayItem = $qeItems->fetch(PDO::FETCH_ASSOC)){
$insert_items->execute(array($rayItem['id'], $rayItem['id_order'], $rayItem['descr'], $rayItem['price'])) OR var_dump($dbh->errorInfo());
}
//if everything is ok, delete the order from p_orders
$delete_order->execute(array($rayOrder['id'])) OR var_dump($dbh->errorInfo());
}
//in here i'll use a bool var to see if anythings gone wrong and i need to rollback,
//or all good and commit
$dbh->rollBack();
//$dbh->commit();
?>
The code above fails with this output:
array(3) { [0]=> string(5) "00000" [1]=> int(7) [2]=> string(62) "ERROR: duplicate key violates unique constraint "id_h_orders"" }
array(3) { [0]=> string(5) "25P02" [1]=> int(7) [2]=> string(87) "ERROR: current transaction is aborted, commands ignored until end of transaction block" }
Fatal error: Call to a member function fetch() on a non-object in /srv/www/test-db/test-db-pgsql-08.php on line 23
So, seem like when the first execute fail (the one with id 293) the transaction is automatically aborted... does the PDO auto-rollback, or something else?
My goal is to complete the first big while loop, and at the end, using a bool var as flag, decide if to rollback or commit the transaction.

With PostgreSQL, if any statement produces a server error during a transaction, that transaction is marked as aborted. That doesn't mean it's actually rolled back yet- just that you can hardly do anything except roll it back. I assume PDO doesn't automatically issue a rollback, it waits for you to call the "rollback" method.
To achieve what I think you want, you can use a savepoint. Rather than rolling back the entire transaction, you can just rollback to the savepoint, and continue the transaction. I'll give an example of using this from psql:
srh#srh#[local] =# begin;
BEGIN
srh#srh#[local] *=# insert into t values(9,6,1,true);
INSERT 0 1
srh#srh#[local] *=# savepoint xyzzy;
SAVEPOINT
srh#srh#[local] *=# insert into t values(9,6,2,true);
ERROR: duplicate key value violates unique constraint "t_pkey"
srh#srh#[local] !=# insert into t values(10,6,2,true);
ERROR: current transaction is aborted, commands ignored until end of transaction block
srh#srh#[local] !=# rollback to savepoint xyzzy;
ROLLBACK
srh#srh#[local] *=# insert into t values(10,6,2,true);
INSERT 0 1
srh#srh#[local] *=# commit;
COMMIT
srh#srh#[local] =#
So in this example, the first column of t is the primary key. I tried to insert two rows into t with an id of 9, and the got a uniqueness constraint. I can't just redo the insert with the right values because now any statement will get the "current transaction is aborted..." error. But I can do "rollback to savepoint", which brings me back to the state I was at when I did "savepoint" ("xyzzy" is the name of the savepoint). Then I can issue the correct insert command and finally commit the transaction (which commits both inserts).
So in your case, I suspect what you need to do is create a savepoint before your UPDATE statement: if it gives an error, do a "rollback to savepoint" and set your flag. You'll need to generate unique names for the savepoints: using a counter, for example.
I'm not entirely sure I understand why you're doing all this. Surely you want to stop processing as soon as you know you're going to roll back the transaction? Or is there some other processing going on in the loop that has to happen as well?

You should be using
pdo_obj->beginTransaction()
pdo_obj->commit()
pdo_obj->prepare()
Also you have a random commit at the end of your first example.
begin
// do all your stuff
// check for errors through interface
commit OR not
pg_query($pgConnection, "ROLLBACK"); // end of tx(1)
// start new transaction after last rollback = tx(2)
pg_query($pgConnection, "UPDATE users SET last_activity = NOW() WHERE id = '$id_user'");
// commit tx(2) or don't here
// this isn't needed pg_query($pgConnection, "COMMIT");
If you didn't commit, and need to manually adjust stuff, use another transaction. Preparing your query (if I recall) is part of a transaction, because it can fail. You can't really just manually take an SQL statement and turn it into queries. The PDO interface has abstractions for a reason. :)
http://uk3.php.net/pdo <-- Solid examples of PHP/Postgre using PDO
good luck

Related

php's mysqli::commit has a $name param - what does it do

https://www.php.net/manual/en/mysqli.commit.php
public mysqli::commit(int $flags = 0, ?string $name = null): bool
Parameters
flags: A bitmask of MYSQLI_TRANS_COR_* constants.
name: If provided then COMMIT/*name*/ is executed.
My question: what is COMMIT/*name*/ ??
I can't find any Mysql documentation for this
https://dev.mysql.com/doc/refman/8.0/en/commit.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-autocommit-commit-rollback.html
nor any usage in the wild
So I turned on query logging
and this is what was logged
passing name to commit():
Query START TRANSACTION
Query INSERT INTO `bob` (`t`) VALUES ("test 1")
Query SAVEPOINT `foo`
Query INSERT INTO `bob` (`t`) VALUES ("test 2")
Query COMMIT /*foo*/
both queries were logged
passing name to rollback:
Query START TRANSACTION
Query INSERT INTO `bob` (`t`) VALUES ("test 1")
Query SAVEPOINT `foo`
Query INSERT INTO `bob` (`t`) VALUES ("test 2")
Query ROLLBACK /*foo*/
Query COMMIT /*comment*/
neither insert happened... entire transaction was rolled back
Nutshell:
mysqli::commit and mysqli::rollback have $name parameters that don't do anything beside add a comment to the query.
To actually rollback to a savepoint, you'll have to execute a query (ROLLBACK TO `name`).
mysqli extension provides a savepoint($name) method, but no rollback_to_savepoint!
When you start a transaction, you have the option of creating a savepoint name for the transaction. You can then use that name to commit. From the MySQL docs:
The SAVEPOINT statement sets a named transaction savepoint with a name of identifier. If the current transaction has a savepoint with the same name, the old savepoint is deleted and a new one is set.

How does the if statement in PHP work?

The code below does exactly what it's expected to do. It adds a client into the database successfully. But I never told the query to execute or add a new client all I did was store the query in a variable and checked to see if it was valid in the if statement. I need some help understanding how the query executed.
$query = "INSERT INTO clients(id, name, email, phone, address, company, notes, date_added) VALUES(NULL, '$clientName', '$clientEmail', '$clientPhone', '$clientAddress', '$clientCompany', '$clientNotes', CURRENT_TIMESTAMP)";
$result = mysqli_query($connection, $query);
// if query was successful
if( $result ){
header("LOCATION: clients.php?update=success");
} else{
// something went wrong
echo "Error: $query <br>" . mysqli_error($connection);
}
The way you should be doing this is a little more self-explanatory:
// Prepare this query with placeholders for where the user data will go.
// This creates a prepared statement handle ($stmt)
$stmt = $connection->prepare("INSERT INTO clients(name, email, phone, address, company, notes, date_added)
VALUES(?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)");
// Bind the user data to the statement so it will be escaped properly
// and inserted correctly.
$stmt->bind_param(
"ssssss",
$clientName,
$clientEmail,
$clientPhone,
$clientAddress,
$clientCompany,
$clientNotes
);
// Execute the statement now that everthing is in place. This actually
// sends the query to the MySQL server to be executed and waits
// for the result. The result of this function call indicates success
// or failure.
if ($stmt->execute()) {
// Query was successful then `execute()` returns a logically true value
// and this block of code will run.
header("Location: clients.php?update=success");
} else {
// If that previous condition didn't trigger, then we end up here and
// this code will run instead.
echo "Error: $query <br>" . $connection->error;
}
If you have an AUTO_INCREMENT column don't specify it in your list of VALUES, you can omit it and it will be populated automatically. Any column with a NULL default can also be omitted. There's no point in force-inserting NULL if that's how it will end up anyway.
You also need to pay careful attention to how you insert your data. You cannot use string interpolation to do this, it's extremely dangerous. The bind_param method takes care of adding the data in a safe manner if you've created a prepared statement that has placeholder values (?). This all but guarantees your code will be safe, secure and free from escaping errors that can take a lot of time to identify and repair.
I've also switched this to use the object-oriented style of mysqli. Not only is this significantly less verbose, it also becomes more clear as to what the operation is being performed on. $stmt->function() is obviously something making use of or manipulating the $stmt object. If it's just one argument of many that can be harder to identify.
Specifying arguments directly to functions instead of leaning on these intermediate variables is also a good habit to get into. Things like $sql tend to clutter up your code and confuse the intent of that string, plus if you have several of them you're juggling, like $sql3 and $sql8 there's an opportunity to make a tiny typo that causes real problems.

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 test is row exists in the database

I have a table called user_bio. I have manually entered one row for username conor:
id: 1
age: 30
studying: Business
language: English
relationship_status: Single
username: conor
about_me: This is conor's bio.
A bio is unique to a user, and obviously, a user cannot manually set their bio from inserting it in the database. Consider the following scenario's:
Logged in as Conor. Since Conor already has a row in the database, I simply want to run an UPDATE query to update the field where username is equal to conor.
Logged in as Alice. Since Alice has no row in the database corresponding to her username. Then I want to run an INSERT query. For all users, all users will have to have their details inputted, and then updated correspondingly.
At the moment, I am struggling with inserting data in the database when no rows exist in the database.
Here is my current approach:
$about_me = htmlentities(trim(strip_tags(#$_POST['biotextarea'])));
$new_studying = htmlentities(trim(strip_tags(#$_POST['studying'])));
$new_lang = htmlentities(trim(strip_tags(#$_POST['lang'])));
$new_rel = htmlentities(strip_tags(#$_POST['rel']));
if(isset($_POST['update_data'])){
// need to check if the username has data already in the db, if so, then we update the data, otherwise we insert data.
$get_bio = mysqli_query($connect, "SELECT * FROM user_bio WHERE username ='$username'");
$row_returned = mysqli_num_rows($get_bio);
$get_row = mysqli_fetch_assoc ($get_bio);
$u_name = $get_row['username'];
if ($u_name == $username){
$update_details_query = mysqli_query ($connect, "UPDATE user_bio SET studying ='$new_studying', language ='$new_lang',
relationship_status = '$new_rel', about_me = '$about_me' WHERE username ='$username'");
echo " <div class='details_updated'>
<p> Details updated successfully! </p>
</div>";
} else {
$insert_query = mysqli_query ($connect, "INSERT INTO user_bio
VALUES ('', '$age','$new_studying','$new_lang','$new_rel', '$username', '$about_me'");
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
echo " <div class='details_updated'>
<p> Details added successfully! $row_returned </p>
</div>";
}
}
The UPDATE query works fine, when logged in as Conor. But again, INSERT does not work when logged in as Alice.
MySQL support INSERT ... ON DUPLICATE KEY UPDATE type of queries. So you do not need to make few queries to check existance of row in your php code, just add corrct indexes and let your DB take care about this.
You can read about such type of queries here
Here are a few things you could do to make it work:
Prevent SQL injection
As this is an important issue, and the suggested corrections provided below depend on this point, I mention it as the first issue to fix:
You should use prepared statements instead of injecting user-provided data directly in SQL, as this makes your application vulnerable for SQL injection. Any dynamic arguments can be passed to the SQL engine aside from the SQL string, so that there is no injection happening.
Reduce the number of queries
You do not need to first query whether the user has already a bio record. You can perform the update immediately and then count the records that have been updated. If none, you can then issue the insert statement.
With the INSERT ... ON DUPLICATE KEY UPDATE Syntax, you could further reduce the remaining two queries to one. It would look like this (prepared):
INSERT INTO user_bio(age, studying, language,
relationship_status, username, about_me)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY
UPDATE studying = VALUES(studying),
language = VALUES(language),
relationship_status = VALUES(relationship_status),
about_me = VALUES(about_me);
This works only if you have a unique key constraint on username (which you should have).
With this statement you'll benefit from having the data modification executed in one transaction.
Also take note of some considerations listed in the above mentioned documentation.
NB: As in comments you indicated that you prefer not to go with the ON DUPLICATE KEY UPDATE syntax, I will not use it in the suggested code below, but use the 2-query option. Still, I would suggest you give the ON DUPLICATE KEY UPDATE construct a go. The benefits are non-negligible.
Specify the columns you insert
Your INSERT statement might have failed because of:
the (empty) string value you provided for what might be an AUTO_INCREMENT key, in which case you get an error like:
Incorrect integer value: '' for column 'id'
a missing column value, i.e. when there are more columns in the table than that you provided values for.
It is anyway better to specify explicitly the list of columns in an INSERT statement, and to not include the auto incremented column, like this:
INSERT INTO user_bio(age, studying, language,
relationship_status, username, about_me)
VALUES (?, ?, ?, ?, ?, ?)
Make sure you get notified about errors
You might also have missed the above (or other) error, as you set your error reporting options only after having executed your queries. So execute that line before doing any query:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
And also add there:
error_reporting(E_ALL);
ini_set('display_errors', 1);
In a production environment you should probably pay some more attention to solid error reporting, as you don't want to reveal technical information in the client in such an environment. But during development you should make sure that (unexpected) errors do not go unnoticed.
Do not store HTML entities in the database
It would be better not to store HTML entities in your database. They are specific to HTML, which your database should be independent of.
Instead, insert these entities (if needed) upon retrieval of the data.
In the below code, I removed the calls to htmlentities, but you should then add them in code where you SELECT and display these values.
Separate view from model
This is a topic on its own, but you should avoid echo statements that are inter-weaved with your database access code. Putting status in variables instead of displaying them on the spot might be a first step in the right direction.
Suggested code
Here is some (untested) code which implements most of the above mentioned issues.
// Calls to htmlentities removed:
$about_me = trim(strip_tags(#$_POST['biotextarea']));
$new_studying = trim(strip_tags(#$_POST['studying']));
$new_lang = trim(strip_tags(#$_POST['lang']));
$new_rel = trim(strip_tags(#$_POST['rel']));
// Set the error reporting options at the start
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
if (isset($_POST['update_data'])) {
// Do not query for existence, but make the update immediately
$update_stmt = mysqli_prepare ($connect,
"UPDATE user_bio
SET studying = ?,
language = ?,
relationship_status = ?,
about_me = ?
WHERE username = ?");
mysqli_stmt_bind_param($update_stmt, "sssss",
$new_studying, $new_lang, $new_rel, $about_me, $username);
mysqli_stmt_execute($update_stmt);
$num_updated_rows = mysqli_stmt_affected_rows($update_stmt);
mysqli_stmt_close($update_stmt);
if ($num_updated_rows === 0) {
$insert_stmt = mysqli_prepare ($connect,
"INSERT INTO user_bio(age, studying, language,
relationship_status, username, about_me)
VALUES (?, ?, ?, ?, ?, ?)");
mysqli_stmt_bind_param($insert_stmt, "isssss",
$age, $new_studying, $new_lang, $new_rel, $username, $about_me);
mysqli_stmt_execute($insert_stmt);
mysqli_stmt_close($insert_stmt);
}
// Separate section for output
$result = $num_updated_rows ? "Details updated successfully!"
: "Details added successfully!";
echo " <div class='details_updated'><p>$result</p></div>";
}
Aside from security issues and bad coding practices, here are a couple things you can do.
You don't need to compare the name to check if the bio already exists. You can just count the number of rows returned. If it is more than zero, then the user bio already exists.
When comparing strings, === is preferred over ==. You can read about it and find out why but here is an example (2nd answer)
You should really look into either REPLACE INTO or ON DUPLICATE KEY UPDATE. Just using either of there 2, depending on your use case pick one, you can pretty much eliminate more than half of your currently displayed code. Basically, both will insert and if the record already exists, they updates. Thus, you wouldn't even need to check if the record already exists.

PHP: SQL Prepared statement transaction not working correctly. It's inserting 1 SQL statement, not both

I'm finding that the PDO Transaction is only commiting 1 of my 2 SQL statement. For some reason, my PHP script is not inserting into my MySQL database 'homes' table BUT it does insert into the 'invoices' table - even though I'm using a PHP PDO database transaction.
Code below:
$conn_str = DB . ':host=' . DB_HOST . ';dbname=' . DB_NAME;
$dbh = new PDO($conn_str, DB_USERNAME, DB_PASSWORD);
/* Begin a transaction, turning off autocommit */
$dbh->beginTransaction();
$sql_create_home_listing = 'INSERT INTO homes ( customer_id,
account_type_id,
address,
city,
state,
zip,
display_status
) VALUES (?,?,?,?,?,?,true)';
$stmt = $dbh->prepare($sql_create_home_listing);
$stmt->bindParam(1, $customer_id);
$stmt->bindParam(2, $account_type_id);
$stmt->bindParam(3, $_SESSION['street']);
$stmt->bindParam(4, $_SESSION['city']);
$stmt->bindParam(5, $_SESSION['state']);
$stmt->bindParam(6, $_SESSION['zip']);
$stmt->execute();
$home_id = $dbh->lastInsertId();
// another SQL statement
$sql_create_invoice = "INSERT INTO invoices (customer_id, account_type_id, price, cc_authorized, home_id) VALUES (?,?,?,?,?)";
$cc_authorized = false;
$anotherStmt = $dbh->prepare($sql_create_invoice);
$anotherStmt->bindParam(1, $customer_id);
$anotherStmt->bindParam(2, $account_type_id);
$anotherStmt->bindParam(3, $account_plan_price);
$anotherStmt->bindParam(4, $cc_authorized);
$anotherStmt->bindParam(5, $home_id);
$anotherStmt->execute();
/* Commit the changes */
$dbh->commit();
How is it possible that only the 'invoices' table is getting the insert and not both the 'invoices' table AND the 'homes' table?
Note: no errors are reported by PHP.
Firstly, check whether you really have any errors in PHP - it's notoriously crap at telling you. There is an option you can set on PDO objects to throw an exception on database error - I recommend you set it.
It sounds to me like it is inserting the row, but you're in a transaction which is never committed so it gets rolled back, and the row is never visible (Your isolation mode is READ_COMMITTED or higher).
In that case you need to re-examine how your application uses transactions and try to see if you can get it properly consistent. Using transactions is nontrivial; it needs either a lot of code to get things right, or some well thought out wrapper code or something. If you don't understand any of that, leave autocommit on.
Check that your tables are transactional (InnoDB vs MyISAM as an example..).
Might want to do a try catch, so that if there is an error you can rollback. This may give you some insight.
Is true a valid value for display_status ?
MySQL does not have a bool type, nor does it have true as a predefined function or constant. So my guess is that it's a syntax error.
Get some decent error handling. Set the option which throws when there's a SQL error (See PDO docs!)
I found the problem. On my homes table I had a field deemed as 'unique' but it was not and preventing the insert from happening

Categories