Issue with maintaining a MySQL WooCommerce Customer Table - php
Well, I'm afraid that I will not be able to post a minimum reproducible example, and for that I apologize. But, here goes nothing.
Ours is a weekly prepared meals service. I track order volume in many ways. Here is the structure of the relevant table:
So then I utilize the highlighted fields in many ways, such as indicating to delivery drivers if a customer is returning from the prior order being more than a month ago (last_order_w - prev_order_w > 4), for instance.
Lately I have been noticing that the data is not consistently updating properly. In the past 3 weeks, I would say it is an occurrence of 5%. If it were more consistent, I would be more confident in my ability to track down the issue, but I am not even sure how to provoke it, as I only really notice it after the fact.
The code that should cause the update is below:
<?php
//retrieve and iterate over IDs of orders placed since last synchronization.
$newOrders=array_map('reset',$dbh->query("select id from wp_posts where id > (select max(synced) from fitaf_weeks) and post_type='shop_order' and post_status='wc-processing'")->fetchAll(PDO::FETCH_NUM));
foreach($newOrders as $no){
//retrieve the metadata for the current order
$newMetas=array_map('reset',$dbh->query("select meta_key,meta_value from wp_postmeta where post_id=$no")->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_UNIQUE));
//check if the current order is associated with an existing customer
$exist=$dbh->query("select * from fitaf_customers where id=".$newMetas['_customer_user'])->fetch();
//if not, gather the information we want to store from this post
$noExist=[$newMetas['_customer_user'],$newMetas['_shipping_first_name'],$newMetas['_shipping_last_name'],$newMetas['_shipping_address_1'],(strlen($newMetas['_shipping_address_2'])==0?NULL:$newMetas['_shipping_address_2']),$newMetas['_shipping_city'],$newMetas['_shipping_state'],$newMetas['_shipping_postcode'],$phone,$newMetas['_billing_email'],1,1,$no,$newMetas['_paid_date'],$week[3],$newMetas['_order_total']];
if($exist){
//if we found a record in the customer table, retrieve the data we want to modify
$oldO=$dbh->query("select last_order_id,last_order,last_order_w,lo,num_orders from fitaf_customers where id=".$newMetas['_customer_user'])->fetch(PDO::FETCH_GROUP|PDO::FETCH_ASSOC|PDO::FETCH_UNIQUE);
//make changes to the retrieved data, and make sure we are storing the most recently used delivery address and prepare the data points for the update command
$exists=[$phone,$newMetas['_shipping_first_name'],$newMetas['_shipping_last_name'],$newMetas['_shipping_postcode'],$newMetas['_shipping_address_1'],(strlen($newMetas['_shipping_address_2'])==0?NULL:$newMetas['_shipping_address_2']),$newMetas['_shipping_city'],$newMetas['_shipping_state'],$newMetas['_paid_date'],$no,$week[3],$oldO['last_order'],$oldO['last_order_id'],$oldO['last_order_w'],($oldO['num_orders']+1),($oldO['lo']+$newMetas['_order_total']),$newMetas['_customer_user']];
}
if(!$exist){
//if the customer did not exist, perform an insert
$dbh->prepare("insert into fitaf_customers(id,fname,lname,addr1,addr2,city,state,zip,phone,email,num_orders,num_weeks,last_order_id,last_order,last_order_w,lo) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")->execute($noExist);
}
else{
//if the customer did exist, update their data
$dbh->prepare("update fitaf_customers set phone=?,fname=?,lname=?,zip=?,addr1=?,addr2=?,city=?,`state`=?,last_order=?,last_order_id=?,last_order_w=?,prev_order=?,prev_order_id=?,prev_order_w=?,num_orders=?,lo=? where id=?")->execute($exists);
}
}
//finally retrieve the most recent post ID and update the field we check against when the syncornization script runs
$lastPlaced=$dbh->query('select max(id) from wp_posts where post_type="shop_order"')->fetch()[0];
$updateSync=$dbh-> query("update fitaf_weeks set synced=$lastPlaced order by id desc limit 1");
?>
Unfortunately I don't have any relevant error logs to show, however, as I documented the code for this post, I realized a potential shortcoming. I should be utilizing the data retrieved from the initial query of new posts, rather than a selecting the highest post id after performing this logic. However, I have timers running on my scripts, and this section hasn't taken over 3 seconds to run in a long time. So it seems unlikely, that the script, which runs on a cron every 5 minutes, is experiencing this unintended overlap?
While I have made the change to pop the highest ID off of $newOrders, and hope it solves the issue, I am still curious to see if anyone has any insights on what could cause this logic to fail at such a low occurrence.
It seems likely your problem comes from race conditions between multiple operations accessing your db.
First of all, your last few lines of code do SELECT MAX(ID) and then uses that value to update something. You Can't Do That™. If somebody else adds a row to that wp_posts table anytime after the entry you think is relevant, you'll use the wrong ID. I don't understand your app well enough to recommend a fix. But I do know this is a serious and notorious problem.
You have another possible race condition as well. Your logic is this:
SELECT something.
make a decision based on what you SELECTED.
INSERT or UPDATE based on that decision.
If some other operation, done by some other user of the db, intervenes between step 1 and step 3, your decision might be wrong.
You fix this with a db transaction. The ->beginTransaction() operation, well, begins the transaction. The ->commit() operation concludes it. And, the SELECT operation you use for step one should say SELECT ... FOR UPDATE.
Related
Mysqli/PHP prevent certain dirty reads
currently I am running into a problem and I am breaking my head over it (although I might be over thinking it). Currently i have a table in my SQL DB with some products and the amount in stock. People can visit the product page, or order it (or update it if you are an admin). But now I am affraid of race conditions. The order process happens as following: 1) The session starts an Transaction. 2) It gets the current amount of units available. 3) It checks that the amount to order is available, and it substract the amount. 4) It updates the product table with the new "total amount" value. Here is the code very short(without using prepared statements etc. etc.). BEGIN; SELECT amount FROM products WHERE id=100; $available=$result->fetch_array(MYSQLI_NUM)[0]; if($order<=$available){ $available-=$order; UDPATE products SET amount=$available WHERE id=100; } //error checking and then ROLLBACK or COMMIT My question now is: What do i do to prevent dirty readings in step 2, and so the write back of wrong values in step 4? example: If 1 person orders 10 things of product A, and while it is at step 3, the second person also orders 5 things of product A. So in step 2 it will still get the "old" value and work with that, and thus restores an incorrect number in step 4. I know i can use "SELECT.... FOR UPDATE" which will put an exclusive lock on the row, but this also prevents an normal user who is just checking the availability(on the product page) to prevent instantaneously loading, while I rather have them to load the page quick than an on the second accurate inventory. So basically i want the read-lock only to apply to clients who will update the value in the same transaction. Is what I want possible, or do I need to work with what i got? Thanks in advance!
There are two ways you can address the problem: You can use a function in MySQL, that shall update the stocks and give an error"Sorry, you product just went out of stock!", whenever the balance after deduction goes below 0. OR (preferred way) You can use locking in MySQL. In this case, it shall be a write lock.The write lock shall disable other read requests(BY second person), till the lock is released(BY First Person). I hope that helps you!
checking number of records in mySQL table
I am looking for a way to check if there are certain number of records within mysql table. For example: After POST request been before putting data to dabase, it checks first how many records there are. If lets say there are 24 records, then it will delete record with latest date based on timestamp and then inster new value from POST request. Has anyone got idea on how to do it? Looking forward fpr your answers. Below I attached simple code i wrote to insert data from post request into table. <?php include("connect.php"); $link=Connection(); $temp1=$_POST["temp1"]; $hum1=$_POST["hum1"]; $query = "INSERT INTO `tempLog` (`temperature`, `humidity`) VALUES ('".$temp1."','".$hum1."')"; mysql_query($query,$link); mysql_close($link); header("Location: index.php"); ?>
When you say delete with the latest date I have to assume you mean the oldest record? Your description doesnt tell me the name of you date field so Lets assume its onDate. You also didnt mention what your primary key is so lets assume that is just id. if you run the below query before inserting it will purge all the oldest records leaving only the newest 23 in the database. delete from templog where id in ( select id from ( select #rownum:=#rownum+1 'rowid', t.id from templog t, (select #rownum:=0)r order by t.onDate )v where v.rowid > 23 ); Of course you should test on data you don't mind losing. It is best to do a cleanup purge each time instead of removing a single row before adding a new one because in the event of exceptions it will never clean itself down to the 24 rows you wish to truly have. I also want to note that you may want to reconsider this method all together. Instead leave the data there, and only query the most recent 24 when displaying the log. Since you are going through the trouble of collecting the data you might as well keep it for future reporting. Then later down the road if your table gets to large run a simple daily purge query to delete anything older than a certain threshold. Hope this helps.
Simple concurrency in PHP?
I have a small PHP function on my website which basically does 3 things: check if user is logged in if yes, check if he has the right to do this action (DB Select) if yes, do the related action (DB Insert/Update) If I have several users connected at the same time on my website that try to access this specific function, is there any possibility of concurrency problem, like we can have in Java for example? I've seen some examples about semaphore or native PHP synchronization, but is it relevant for this case? My PHP code is below: if ( user is logged ) { sql execution : "SELECT....." if(sql select give no results){ sql execution : "INSERT....." }else if(sql select give 1 result){ if(selected column from result is >= 1){ sql execution : "UPDATE....." } }else{ nothing here.... } }else{ nothing important here... }
Each user who accesses your website is running a dedicated PHP process. So, you do not need semaphores or anything like that. Taking care of the simultaneous access issues is your database's problem.
Not in PHP. But you might have users inserting or updating the same content. You have to make shure this does not happen. So if you have them update their user profile only the user can access. No collision will occur. BUT if they are editing content like in a Content-Management System... they can overwrite each others edits. Then you have to implement some locking mechanism. For example(there are a lot of ways...) if you write an update on the content keeping the current time and user. Then the user has a lock on the content for maybe 10 min. You should show the (in this case) 10 min countdown in the frontend to the user. And a cancel button to unlock the content and ... you probably get the idea If another person tries to load the content in those 10 min .. it gets an error. "user xy is already... lock expires at xx:xx" Hope this helps.
In general, it is not safe to decide whether to INSERT or UPDATE based on a SELECT result, because a concurrent PHP process can INSERT the row after you executed your SELECT and saw no row in the table. There are two solutions. Solution number one is to use REPLACE or INSERT ... ON DUPLICATE KEY UPDATE. These two query types are "atomic" from perspective of your script, and solve most cases. REPLACE tries to insert the row, but if it hits a duplicate key it replaces the conflicting existing row with the values you provide, INSERT ... ON DUPLICATE KEY UPDATE is a little bit more sophisticated, but is used in a similar situations. See the documentation here: http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html http://dev.mysql.com/doc/refman/5.0/en/replace.html For example, if you have a table product_descriptions, and want to insert a product with ID = 5 and a certain description, but if a product with ID 5 already exists, you want to update the description, then you can just execute the following query (assuming there's a UNIQUE or PRIMARY key on ID): REPLACE INTO product_description (ID, description) VALUES(5, 'some description') It will insert a new row with ID 5 if it does not exist yet, or will update the existing row with ID 5 if it already exists, which is probably exactly what you want. If it is not, then approach number two is to use locking, like so: query('LOCK TABLE users WRITE') if (num_rows('SELECT * FROM users WHERE ...')) { query('UPDATE users ...'); } else { query('INSERT INTO users ...'); } query('UNLOCK TABLES')
Using $this->db->insert_id() with multiple users on Codeigniter
I'm building an eCommerce site with Codeigniter which will allow users to register, buy products and then track the orders. I'm using the following in several places around the site, mainly when a user is submitting an order: $this->db->insert_id(); Basically when a user submits an order, it will add the order to one table, and then, within the same segment of code (immediately after the insert query), add each order item to another table using the ID created when the order is inserted into the first table. My question is: Out of the following, what does $this->db->insert_id(); do: 1) Does it get the ID that has just been inserted in (and only from) insert query just run? 2) Does it get the last inserted ID from the latest entry in the database regardless of what query its come from? Basically I'm trying to avoid orders being mixed up, say for example if several customers were submitting orders at the same time, I don't want one customer's order items to be added to the incorrect order. I think the answer is 1, and that there's no problem, but I wanted to be sure. Thanks!
It gets the ID that last inserted by the last query. So what you said in #1
Just a suggestion - but another way to do this is to generate a random string - and use that to associate the cart items and order together - instead of by order id. you would still use the order id as the "order number". this gives you the option of generating that random string when the shopping session first begins and using it to tie the cart items, shipping, billing etc together as the purchase is proceeding. so in that way you are starting the order immediately, but you haven't had to commit a space in the final order table until the transaction verifies.
Your question exposes a potential bug in the codeigniter environment. If two inserts are done in rapid succession, how do you have confidence that the ID returned from insert_id is the proper ID? Codeigniter documentation does not answer this question http://ellislab.com/codeigniter/user-guide/database/helpers.html A relevant blog entry from ellis lab does not resolve the question. It concludes that the appropriate resolution is to take your chances. http://ellislab.com/forums/viewthread/63052/ If this function is a wrapper function for mysqli_insert_id, the documentation at php.net is unclarified. http://www.php.net/manual/en/mysqli.insert-id.php It states the ID is from "the last query". It does not say whose last query. Two successive inserts, and the return of a wrong ID will compromise the integrity of your data. The way to be sure is lock the database. $this->db->query('LOCK TABLE (your table name) WRITE'); $this->db->insert('(your table name'); $int_id = $this->db->insert_id(); $this->db->query('UNLOCK TABLES'); This has a negative impact on execution time, but depending on your server's capacity is likely preferable to data corruption.
Update view count, most reliable way
Hello again Stackoverflow! I'm currently working on custom forumsoftware and one of the things you like to see on a forum is a viewcounter. All the approaches for a viewcounter that I found would just select the topic from the database, retrieve the number from a "views" column, add one and update it. But here's my thought: If, lets say 400, people at the exact same time open a topic, the MySQL database probably won´t count all views because it takes time for the queries to complete, and so the last person (of the 400) might overwrites the first persons (of the 400) view. Ofcourse one could argue that on a normal site this is never going to happen, but if you have ~7 people opening that topic at the exact same second and the server is struggleing at that moment, you could have the same problem. Is there any other good approach to count views? EDIT Woah, could the one who voted down specify why? I ment by "Retrieving the number of views and adding one" that I would use SELECT to retrieve the number, add one using PHP (note the tags) and updating it using UPDATE. I had no idea of the other methods specified below, that's why I asked.
If, lets say 400, people at the exact same time open a topic, the MySQL database apparently would count all the views because this is exactly what databases were invented for. All the approaches for a viewcounter that you have found are wrong. To update a field you don't need to retrieve it, but just already update: UPDATE forum SET views + 1 WHERE id = ?
So something like that will work: UPDATE tbl SET cnt = cnt+1 WHERE ... UPDATE is guaranteed to be atomic. That means no one will be able to alter cnt between the time it is read and the time it is replaced. If you have several concurrent UPDATE for the same row (InnoDB) or table (MyISAM) they have to wait their turn to update the date. See Is incrementing a field in MySQL atomic? and http://dev.mysql.com/doc/refman/5.1/en/ansi-diff-transactions.html