Database Design / Managing Data Effectively - php

I'm a bit new to coding in general and seem to be struggling to wrap my mind around how to store data effectively for my application. (I'm attempting this in Laravel, with mySql)
I realise this question might lean towards being opinion-specific, but I am really looking for obvious pointers on false assumptions I have made, or a nudge in the direction of best-practices.
I will be tracking a number of messages, as if they were credits in a bulk email management system. One message-batch could use an undetermined number of credits to fire off a full batch of messages.
Groups of users will be able to send messages if they have credits to do so, One solution I have thought of is to have a table: id, user_group_id, debt/credit, reference_code - Where user_group_id would be linked to the group to which the user belongs, the debit/credit column could hold a positive or negative number (of a message related transaction), and the reference_code would track the type of transaction. Debits/Credit transactions would come about where the user_group account received new credits (purchased block of new credits), or in the case of a debits example, where batches of messages had been sent.
All this background leads to my question .. I still don't hold a single value for the available number of credits a user_group has. Before being able to send a new batch, should I be running a database query each time that sums all the "accounting" of positive and negative transactions to determine whether a user is "in the black" to be able to send further message batches, or should I have an additional table and keeps the result of their available credits total separately?
If I do store the total-available-credits value by itself, when should this single value be updated, at the end of every message-related task my application performs ? *user adds new credits, update total - user sends batch, update total.. etc.

This is an interesting question. Opinionated as you pointed out but nonetheless interesting. Database design is not my strong suit but here is how I would do it.
First, ensure integrity with INNODB tables and foreign key constraints. I would keep the total remaining credits each user group has in the user group table. You cold then create a transaction table with a Transaction ID, the User Group ID, and the credits used for that transaction so that you could follow each user group's transaction history.
Just a thought. Like I said, I'm by no means an expert. It may be useful however, to have a log of some sort so that you could verify transactions later in case of a credit discrepancy. You could always use this later to recalculate the remaining credits to ensure the numbers align.
Since these transactions may be important for credit/billing purposes, you may also want to turn off MySQL's auto commit and use the commit and rollback features to ensure your data stays in tact in case of an error.

" should I be running a database query each time that sums all the "accounting" of positive and negative transactions to determine whether a user is "in the black" to be able to send further message batches "
YES

Related

PHP Race condition for Raffles

I need to create a Raffle system where users can enter the number of tickets they want to buy and pay with credit cards to participate in a raffle. A raffle has a limited number of tickets let's say 1000. Anyone can enter any number of tickets he wants to buy, of course the number should be less or equal to 1000. There is no login in the system so I'm seeing this following as a race condition:
One user enters 998 tickets to buy and another one enters 5 tickets, if both users click on submit on the same time and I process both requests is this going to be a race condition? If yes, has anyone came across a similar case and is there any way to avoid this?
Thanks.
Many programmers will approach this by checking the database for sufficient tickets and if found then updating the ticket sales. This leaves a window between the read and write parts of the operation where the race condition exists.
User A reads (reads 1000 tickets)
User B reads (reads 1000 tickets)
User A writes(reduces tickets left by 998, writes 2)
User B writes (reduces tickets left by 5, writes 995)
We're left with 1003 tickets 'sold', and a remaining balance of 995 tickets to sell. This is clearly not acceptable.
What's required is an atomic test-and-set operation (atomic, because it's indivisible)
Fortunately, databases treat single queries as atomic, and also provide a mechanism for the test-and-set requirement
Consider the query
UPDATE `raffles` set `ticketsLeft` = `ticketsLeft`- 995 WHERE `raffleID` = 'someId' and `ticketsLeft` >= 995
This will test for sufficient tickets and deduct the tickets sold all as part of one query. There's no window in which a race can exist.
But, how does the program know that the update succeeded?
The database handles that too. The program asks the database for the number of rows affected by the UPDATE query. If the update succeeds one row is affected. If it fails (no raffleId or insufficient tickets) no rows are affected.
Thus, the program executes its query, and checks the rows affected. If the answer is 1 the ticket sale succeeded. If the answer is zero, the ticket sale failed. The program handles these two possibilities and carries on.*
For PHP there are two interfaces to MySQL: see mysqli::affected_rows(), or PDOStatement::rowcount() for the details.
Other databases have similar constructions.
* Data integrity is assured here by applying a UNIQUE index to the raffleId column, guaranteeing there will only be one matching raffle, or none.

Issue with maintaining a MySQL WooCommerce Customer Table

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.

MySQL selling database consistency: queued transactions?

I'm trying to create an application that has the ability to sell gift cards but I'm fearing the consistency of data, so let me explain what I mean:
I have 3 tables
transactions(id, created_date),
cards(id, name) and
vouchers(id, code, card_id, transaction_id).
The database contains many cards and vouchers and each voucher belongs to one card.
The user will want to select a card to buy and choose a quantity.
So, the application will select vouchers according to the selected card and LIMIT according to quantity then creating a new transaction and adding the transaction_id into the selected vouchers to flag them as bought cards for this new transaction.
So, what I am afraid of is what if multiple users sent the same buying request for the same card at the exact same time, then will some data collision occur? and what is the best approach to fix this?
I am currently using MySQL 8 Community Edition with InnoDB engine for all tables.
What I am searching for is if I can create some kind of a queue that activates transactions one by one without collision even if it means that the users will have to wait until they get to their turn in the queue.
Thanks in advance
This is a job for MySQL transactions. With SQL you can do something like this.
START TRANSACTION;
SELECT id AS card_id FROM cards WHERE name = <<<<chosen name>>>> FOR UPDATE;
--do whatever it takes in SQL to complete the operation
COMMIT;
Multiple php processes, may try to do something with the same row of cards concurrently. Using START TRANSACTION combined with SELECT ... FOR UPDATE will prevent that by making the next process wait until the first process does COMMIT.
Internally, MySQL has a queue of waiting processes. As long as you do the COMMIT promptly after you do BEGIN TRANSACTION your users won't notice this. This most often isn't called "queuing" at the application level, but rather transaction consistency.
Laravel / eloquent makes this super easy. It does the START TRANSACTION and COMMIT for you, and offers lockForUpdate().
DB::transaction(function() {
DB::table('cards')->where('name', '=', $chosenName)->lockForUpdate()->get();
/* do whatever you need to do for your transaction */
});

Generate a continuous invoice number in PHP & MySQL (concurrency)

The system is used by many shops, and each shop has its own invoice number, e.g.
SHOPA-0001
SHOPA-0002
SHOPA-0003
SHOPB-0001
SHOPB-0002
...
Now what I did is, select the last ID in MySQL table, and +1 to the invoice number. My problem is, 1 shop has multiple PC running this system, if 2 cashier submit the form at same time, it will has duplication.
Any suggestion to this problem?
Utilize safe Intention Locks like my answer Here. But where I have sections like Chassis and Brakes, yours would be SHOPA and SHOPB etc. You could decide whether you want client-side to handle the left padding of zeros, or if you want mysql to handle it with a column width int(4) or use LPAD() with a CONCAT.
As mentioned in that answer, it is the safe way to do it for concurrency and the shops are segmented off from one another. The lock is uber fast as in momentary if done correctly.

Optimizing the friend-relationship storage in MySQL

Assumptions
If A is a friend of B, B is also a friend of A.
I searched for this question and there are already lots of questions on Stack Overflow. But all of them suggest the same approach.
They are creating a table friend and have three columns from, to and status. This serves both purposes : who sent friend request as well as who are friends if status is accepted.
But this means if there are m users and each user has n friends, then I will have mn rows in the friends table.
What I was thinking is to store friends list in a text column. For every user I have a single row and a friends column which will have all accepted friends' IDs separated by a character, say | which I can explode to get all friends list. Similarly, I will have another column named pending requests. When a request is accepted, IDs will move from pending requests to friends column.
Now, this should significantly reduce the entries in the table and the search time.
The only overhead will be when I will have to delete a friend, I will have to retrieve the friend string, search the ID of the friend to be deleted, delete the ID and update the column. However, this is almost negligible if I assume a user cannot have more than 2000 friends.
I assume that I will definitely be forgetting some situations or this approach will have certain pitfalls. So please correct if so.
The answer is NO! Do not try to implement this idea - its complete disaster.
I am going to describe more precise why:
Relations. You are storing just keys separeted with |. What if you want to display list with names of friends? You will have to get list, explode it and make another n queries to DB. With relation table from | to | status you will be able to do that with one JOIN.
Deletions. Just horrible.
Inserts. For every insert you will need to do SELECT + UPDATE instead of INSERT.
Types. You should keep items in DB as they are, so integers as integers. Converting ints into string and back could cause some errors, bugs etc.
No ORM support. In future you will probably leave plain PHP for some framework. Take in mind that none of them will support your idea.
Search time?
Please do some tests. Search with WHERE + PRIMARY KEY is very fast.

Categories