How to remove previous sql insert if one after fails? - php

When I insert a new record into one table (work_log), I update a record in another table (employers) with the last inserted record from work_log.
But if updating employers-table doesn't succeed, after successfully inserted the new record into work_log, I need to remove the newly added record to work_log since that entry would not be valid anymore.
Here's my script so far:
/**
* This first part has no direct affect on the question, but serves as additional information to understand the script better..
* - - -
* First, a new work session is inserted (this session has nothing to do with browser session)
* If this fails, the script does not continue, and the user is redirected back to the form with an error-message.
* otherwise, the script continues, and try to activate the session by adding a new work_log entry.
*/
$ins_session = $con['site']->prepare('INSERT INTO work_sessions (fields) VALUES (?)');
$ins_session->execute(array(values));
if($ins_session){
KD::notice('success','New work session is created.');
$session_id = $con['site']->lastInsertId();
} else {
KD::notice('error','Work session was not created.');
KD::redirect(); // stops the script, and redirects
}
/**
* This part affects my question
* - - -
* Add a new entry to the work log in order to automatically start the work session.
* If this entry is successfully inserted, then add an indicator to the corresponding employer, in the employers table, to indicate that this employer has an active session (and which one it is).
*/
$ins_work_log = $con['site']->prepare('INSERT INTO work_log (fields) VALUES (?)');
$ins_work_log->execute(array(values));
if($ins_work_log){
$upd_employer = $con['site']->prepare('UPDATE employers SET fk_work_sessions_id = ? WHERE id = ?');
$upd_employer->execute(array($session_id,$_POST['employer_id']));
if($upd_employer){
KD::notice('success','New session was created and started.');
KD::redirect();
} else {
// need to remove the entry from work_log.
KD::notice('Work session was created, but not started. Please start the session manually.');
}
}
To my understanding, I have to delete the last inserted record in the work_log-table?
Is there any other way to do this? like, in another order, or to automatically remove the entry from work_log if this (the update query) fails?
The work_log-table is innoDB, and row format is compact if that is important to know...
UPDATE
I've set it up like this:
It seems to work, but I'm a bit unsure if I'm using it correctly regarding the if/else statements.
$con['site']->beginTransaction();
$ins_work_log = $con['site']->prepare('INSERT INTO work_log (fields) VALUES (?)');
$ins_work_log->execute(array(values));
if($ins_work_log){
# update employer
$upd_employer = $con['site']->prepare('UPDATE employers SET fk_work_sessions_id = ? WHERE id = ?');
$upd_employer->execute(array($session_id,$_POST['employer_id']));
if($upd_employer){
$con['site']->commit();
KD::notice('success','New session was created and started.');
} else {
$con['site']->rollBack();
KD::notice('error','Work session was created, but not started. Please start the session manually.');
}
//
} else {
$con['site']->rollBack();
KD::notice('error','');
}
KD::redirect();
Will if($ins_work_log), and if($upd_employer), have any affect when the query hasn't been committed yet?

This is a classic case for using START TRANSACTION, COMMIT, and ROLLBACK.
http://dev.mysql.com/doc/refman/5.0/en/commit.html
Just make sure you are using a database engine that supports it.
Pseudocode:
query("START TRANSACTION;");
query("INSERT INTO table1 ...");
if (query("INSERT INTO table2 ..."))
query("COMMIT;");
else
query("ROLLBACK;");

Related

Deadlock in script for sending emails

I have a script for sending emails in the background. The script runs in parallel to send out multiple emails simultaneously. It works basically like this, with a mixture of MySQL and PHP:
/* TransmissionId is a PRIMARY KEY */
/* StatusId is a FOREIGN KEY */
/* Token is UNIQUE */
/* Pick a queued (StatusId=1) transmission and set it to pending (StatusId=2) */
/* This is a trick to both update a row and store its id for later retrieval in one query */
SET #Ids = 0;
UPDATE transmission
SET StatusId=IF(#Ids := TransmissionId,2,2), LatestStatusChangeDate=NOW()
WHERE StatusId = 1
ORDER BY TransmissionId ASC
LIMIT 1;
/* Fetch the id of the picked transmission */
$Id = SELECT #Ids;
try {
/* Fetch the email and try to send it */
$Email = FetchEmail($Id);
$Email->Send();
/* Set the status to sent (StatusId=3) */
$StatusId = 3;
} catch(Exception $E) {
/* The email could not be sent, set the status to failed (StatusId=4) */
$StatusId = 4;
} finally {
/* Save the new transmission status */
UPDATE transmission
SET StatusId=$StatusId, LatestStatusChangeDate=NOW(), Token='foobar'
WHERE TransmissionId = $Id;
}
The issue is that I sometimes get a deadlock: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction. This has happened when executing the last query. I've not seen it happen when executing the first query. Can anyone understand how a deadlock can happen in this case? Could it be that the first query and the last query lock StatusId and TransmissionId in opposite order? But I don't think the first query needs to lock TransmissionId, nor do I think the last query needs to lock StatusId. How can I find this out, and how can I fix it?
Edit
There is another query that might also play a role. Whenever someone opens the email, this query is run:
/* Selector is UNIQUE */
UPDATE transmission SET
OpenCount=OpenCount+1
WHERE Selector = 'barfoo'
InnoDB uses automatic row-level locking. You can get deadlocks even in the case of transactions that just insert or delete a single row. That is because these operations are not really “atomic”; they automatically set locks on the (possibly several) index records of the row inserted or deleted. dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks-handling.ht‌​ml

Function to revert mysql statement

How can I implement a undo changes function in my php application, so if the user for example added a new member to a group, or deleted item a notification message will appear: The selected item have been deleted, undo delete action(link that trigger the undo function).
The following is a working code:
public function deleteRecord($id) {
$group = $this->query('SET autocommit=0');
$group = $this->query('DELETE FROM `members` WHERE memberID = '.$id.'');
}
public function undo_delete() {
$member = $this->query('rollback');
if($member) {
trigger_error('Undo action has been completed. ', E_USER_NOTICE);
return true;
}
}
The only problem with this code is the transaction has not been submitted, so rollback or log out will undo all the changes that has been done during the user session, I am not sure if this is the best approach to create a redo function, any suggestions?
You will not be able to undo with 2 different requests .Because when you will call the undo . It will undo all delete happened for all the members . To solve it I would suggest -
You create a new table where you keep the information of the deleted record, eg- deleted_members.
When user calls for undo fetch the data from the deleted_members table and insert into original members table.
After successful insert in original table ,remove the data from the delete information table.
I hope this helped.
Transactions do not survive across PHP requests. If a transaction is still open at the end of the first request, it will be implicitly rolled back when the MySQL connection closes at the end of the request. As a result, there is no longer a transaction to roll back when an undo is requested later.
If you want to implement this functionality, it will need to be implemented in application logic.

MySQL - INSERT validation in query

Take the following scenario:
Item cost = 30
User money = 25
Form won't submit if user doesn't have enough money.
$error = false;
if($user_money < $item_cost){
//don't submit form
$error = true;
}
But is that enough? Can a user get around it and purchase the item even if there isn't enough money?
Would it be better to do something like this:
Keep the above:
$error = false;
if($user_money < $item_cost){
//don t submit form
$error = true;
}else{
$myclass->purchaseItem($item_id, $user_id);
}
public function purchaseItem($item_id, $user_id) {
//do the validation here again something like. I don t know how to do the query exactly.
$q = $this->db->mysqli->prepare("INSERT INTO buys (bl bla blah) VALUES (?,?,?) IF ... user has enough points in user_points table");
}
Hope that makes sense and I don't get down voted.
In your database you can use a trigger to check the constraint. Depending on you model you might need a transaction to prevent a record from being inserted incorrectly.
Assuming the following:
Two tables:
buys
wallet
If a user buys something (definite BUY, so not a shopping cart placement action), the wallet is updated in the same action.
To do this you can either write a transaction: See How to start and end transaction in mysqli? on how to.
and use 2 statements:
UPDATE wallet SET amount=amount-{buyAmount} WHERE user=?;
INSERT INTO buys (amount,user,orderId) VALUES (?,?,?);
(Of course buyAmount is also a ? in the prepared statement)
Or you can use a trigger. The trigger has to lock the user record when inserting in the buys table:
CREATE TRIGGER updateWallet() BEFORE INSERT ON buys
BEGIN
SET #updatedWalletAmount=0;
SELECT amount-NEW.buyAmount FROM wallet WHERE user=NEW.user FOR UPDATE;
IF(#updatedWalletAmount>0) THEN
UPDATE wallet SET amount=#updatedWalletAmount;
ELSE
SIGNAL SQLSTATE 'ERR0R'
SET
MESSAGE_TEXT = 'Not enough money',
MYSQL_ERRNO = 'USER-1';
END;
END;
The error will have to be caught in php.
Validating data on server shouldn't be made twice. Validating the data on the php side would be easier and as reliable as on your database server.
For more information on validating input data you can check this.

How get value from POST

This piece of code has been tripping me out for the past four hours. It is deleting a row of photos by the primary ID.
I have var_dump($selectedPhoto) and it is the correct ID, a number. My code will run every time I press delete photo, get to the mysqli_stmt_store_result part and shoots out the $txtMessage, But the database does not update.
This is really weird because I have used the exact same code, with different variables on another page and it works perfectly fine.
Can you see any errors by looking at this? OR have a better way to writing the delete statement.
if (isset($_POST['btnDeletePhoto']))
{
$selectedPhoto = $_SESSION['selectedPhoto'];
$deleteString = "DELETE FROM Photos WHERE PhotoID = ?";
$preparedDeleteStmt = mysqli_prepare($link, $deleteString);
mysqli_stmt_bind_param($preparedDeleteStmt, 'i', $selectedPhoto);
if (!mysqli_stmt_execute($preparedDeleteStmt))
{
mysqli_close($link);
die("The system is not available, try again later");
}
if(mysqli_stmt_store_result($preparedDeleteStmt))
{
$txtMessage = "Delete successfull";
}
To add: $selectedPhoto is a value of a select, drop down list value.
If the photo comes from the value of a select, it is not going to be stored in a session variable, so you probably need to change:
$selectedPhoto = $_SESSION['selectedPhoto'];
to:
$selectedPhoto = $_POST['selectedPhoto'];
Apart from that you need to add error handling to all database operations.

Php's Mysqli executes statements in random order (very strange)

EDIT: I am very sorry, I found the mistake and it's very stupid. (See answer)
I seem to have a very strange problem. I have a website that uses 3rd party authorization for login. My users use two social networks for this: Facebook and Vkontakte (Russian analogue).
On log in, I search the database for the user with the passed social id (which is, depending on the chosen social network, user's id in FB or VK, for which I have two different columns) and fetch it.
In case a user has accounts in both FB and VK and has logged in through both of them, he or she now has two separate accounts on my site. However, he can join them into one, by logging in through one social network (this will be his master account) and using the 'user_bind' function with another social network.
This function finds the user's another account and relinks all data in the database to the master account. It then deletes the other account and adds its social id to the master account, so that now the user can log in through both social networks. Social id column has a UNIQUE index, naturally.
However, when the script executes, it seems to execute the UPDATE, which adds the social id, before the DELETE statement, which removes the old user. This produces an error, because it attempts to add an existing social id (because the old user is still there).
When I check the database after the script execution, the old user is gone, so I guess that means that the DELETE statement is indeed executed, but with a delay, in which other statements are executed. The MySQL Workbench's log confirms this, though I'm not sure whether it's reliable.
My question is how do I ensure that the DELETE (or any other MySQL statement for that matter) has actually been executed before executing the rest of the script? And why does this happen anyway?
Here's the adequately-commented code (though I will gladly accept an answer which has no code in it and just explains the principle).
The user_bind function:
function user_bind($eSourceType)
{
//$eSourceType can be 'fb' or 'vk', depending on the social network of the secondary account
$usrMe=get_gl_me(); //gets the user's account, through which he is logged in - the master account
if ($eSourceType=='fb') //if the social network that we are binding this account to is Facebook
{
$vSidName='facebook_id'; //name of the column which contains the social id
if (!$usrMe->get_private_property("facebook_id") & $usrMe->get_private_property("vkontakte_id") ) //check if the master account really doesn't have facebook_id set
{
$fb=get_facebook();//gets facebook object (from FB PHP SDK)
$sid=$fb->getUser();//gets user's id in facebook (social id)
}
else
{
error("The account has facebook_id set");
}
}
elseif($eSourceType=='vk')//same as before, but the id is fetched through $_GET, not object
{
$vSidName='vkontakte_id';
if ($usrMe->get_private_property("facebook_id") & !$usrMe->get_private_property("vkontakte_id") ) //check if it's the right account
{
$sid=$_GET['uid'];
}
else
{
error("The account has vkontakte_id set");
}
}
if(!$sid) //if we couldn't retrieve the social id
{
error("Can't bind: \$sid not set.");
}
$idNew=$usrMe->get_id();//get id (database id) of the master account
$usrOld=fetch_user_by_sid($sid, $eSourceType, true); //fetches the 'user' object by the social id we retrieved before
if ($usrOld)//if there is a user with this social id (if there is a secondary account)
{
$idOld=$usrOld->get_id();//get id of the secondary account
$tblsRelink=array("comments", "posts", "users_private", "vote_posts", "vote_comments"); //get tables in which we have to relink users
foreach($tblsRelink as $tbl)
{
//update set users_idusers to userid
$sp=new Statement_Parameter; //this is a class from PHP.com: http://php.net/manual/en/mysqli-stmt.bind-param.php. It allows to bind variables to the prepared statement in MySQLi without much pain
$query="UPDATE $tbl SET users_idusers=" . db_stmt_operands($idNew, $sp, 'idNew') . " WHERE users_idusers=". db_stmt_operands($idOld, $sp, 'idOld'); //db_stmt_operands inserts question marks in the query, while binding the variables through Statement_Parameter
$affected_rows=db_submit($query, $sp);//see below for the db_submit() function explanation
}
//delete old user
$sp=new Statement_Parameter; //clear Statement_Parameter
$query="DELETE FROM users WHERE idusers=" . db_stmt_operands($idOld, $sp, 'idOld');
$affected_rows=db_submit($query, $sp);
echo "<br>affected: $affected_rows<BR>"; //this actually returns 1
//lets see if the user was actually deleted
$usrTest=fetch_user_by_sid($sid, $eSourceType, true); //fetch the user by the social id
if($usrTest) //if a user is fetched
{
debug_array($usrTest); //custom implementation of print_r
error("User still exsists. Oh no.");//it always does
}
}
$usrMe->set_private_property($vSidName, $sid);//sets the property 'facebook_id' or 'vkontakte_id' to the social id that we got in the beginning
$usrMe->update();//UPDATE statement, which brings the object's properties in the database up to date (in our case: adds the social id)
//the UPDATE statement doesn't execute because the old user is still there
}
The db_submit function:
function db_submit($query, $sp=NULL)
{
$mysqli = db_connect(); //fetches PHP MySQLi object
if ($stmt = $mysqli->prepare($query)) //if the statement is successfully prepared
{
if($sp)//if there is a Statement_Parameter passed
{
$sp->Bind_Params($stmt); //bind parameters from SP
}
if($stmt->execute())//try to execute the statement
{
//on success
if ($mysqli->insert_id) //if this was an INSERT
{
return $mysqli->insert_id;
}
else //if this was DELETE or UPDATE
{
return $mysqli->affected_rows;
}
}
else
{
//on failure
error("Could not submit: could not execute statement. Query: $query." . $stmt->error); //this kills the script
}
}
else
{
error("Could not submit. Query: $query." . $mysqli->error);
}
}
The thing is: private_properties (including the social ids) or the object 'user' are stored in a separate table ('users_private'), which was linked to the main table ('users') through a foreign key.
I included the 'users_private' table in the array of tables that require relinking:
$tblsRelink=array("comments", "posts", "users_private", "vote_posts", "vote_comments");
This resulted in the record in 'users_private' for the old user being relinked to the new user (which now had 2 records - how reckless of me not to make this field UNIQUE). So when the old user got deleted, its associated 'users_private' record was not, because it was now linked to the new user. Naturally, attempt to add the social id was producing an error, because that id already was there, relinked from the old user.
This could be prevented by either
Thinking a bit more about what I was doing (why did I consider 'users_private' a table eligible for relinking?)
More careful database structuring (if a field is supposed to be unique - create a UNIQUE key!)
or, even better, both.

Categories