How to block table in booking? - php

How to block table to booking? My booking table has from_datetime and to_datetime fields. I would like to avoid situation, when two users book something in the same time.
My attempts:
TERMINAL 1: - user1
LOCK TABLE booking READ;
INSERT INTO booking VALUES ('2019-01-01 12:00:00', '2019-01-01 14:00:00');
Now table booking should be locked, and other users should not be able to read from its, but...
TERMINAL 2: - user2
SELECT * from booking
I have results... Why? This should be blocked until I do in TERMINAL 1:
unlock tables;

You need to use LOCK TABLE booking WRITE;. That indicates that you're going to be writing to the table, and you need to block everyone else from reading and writing the table.
A READ lock means that you're only going to read the table. It blocks other users from writing to the table, so that all your reads access consistent table contents. But it doesn't block other users from reading the table.
However, if you're using InnoDB you should use transactions rather than table locks. This is more fine-grained, and figures out what to lock automatically. Your code would look something like this:
START TRANSACTION;
SELECT COUNT(*) FROM booking
WHERE from_datetime = '2019-01-01 12:00:00' AND to_datetime = '2019-01-01 14:00:00'
FOR UPDATE;
-- If this returns zero count:
INSERT INTO booking VALUES ('2019-01-01 12:00:00', '2019-01-01 14:00:00');
COMMIT;
The FOR UPDATE option in the SELECT causes MySQL to lock the appropriate part of the table. If another user executes the same code, they'll be blocked if they try to select the same times.

Locking tables is not such a good idea in some cases.
You should use a Transaction and check in your transaction if the ranges overlap.
Check overlap of date ranges in MySQL
https://dev.mysql.com/doc/refman/8.0/en/commit.html

Related

SQL INSERT INTO SELECT and Return the SELECT data to Create Row View Counts

So I'm creating a system that will be pulling 50-150 records at a time from a table and display them to the user, and I'm trying to keep a view count for each record.
I figured the most efficient way would be to create a MEMORY table that I use an INSERT INTO to pull the IDs of the rows into and then have a cron function that runs regularly to aggregate the view ID counts and clears out the memory table, updating the original one with the latest view counts. This avoids constantly updating the table that'll likely be getting accessed the most, so I'm not locking 150 rows at a time with each query(or the whole table if I'm using MyISAM).
Basically, the method explained here.
However, I would of course like to do this at the same time as I pull the records information for viewing, and I'd like to avoid running a second, separate query just to get the same set of data for its counts.
Is there any way to SELECT a dataset, return that dataset, and simultaneously insert a single column from that dataset into another table?
It looks like PostgreSQL might have something similar to what I want with the RETURNING keyword, but I'm using MySQL.
First of all, I would not add a counter column to the Main table. I would create a separate Audit table that would hold ID of the item from the Main table plus at least timestamp when that ID was requested. In essence, Audit table would store a history of requests. In this approach you can easily generate much more interesting reports. You can always calculate grand totals per item and also you can calculate summaries by day, week, month, etc per item or across all items. Depending on the volume of data you can periodically delete Audit entries older than some threshold (a month, a year, etc).
Also, you can easily store more information in Audit table as needed, for example, user ID to calculate stats per user.
To populate Audit table "automatically" I would create a stored procedure. The client code would call this stored procedure instead of performing the original SELECT. Stored procedure would return exactly the same result as original SELECT does, but would also add necessary details to the Audit table transparently to the client code.
So, let's assume that Audit table looks like this:
CREATE TABLE AuditTable
(
ID int
IDENTITY -- SQL Server
SERIAL -- Postgres
AUTO_INCREMENT -- MySQL
NOT NULL,
ItemID int NOT NULL,
RequestDateTime datetime NOT NULL
)
and your main SELECT looks like this:
SELECT ItemID, Col1, Col2, ...
FROM MainTable
WHERE <complex criteria>
To perform both INSERT and SELECT in one statement in SQL Server I'd use OUTPUT clause, in Postgres - RETURNING clause, in MySQL - ??? I don't think it has anything like this. So, MySQL procedure would have several separate statements.
MySQL
At first do your SELECT and insert results into a temporary (possibly memory) table. Then copy item IDs from temporary table into Audit table. Then SELECT from temporary table to return result to the client.
CREATE TEMPORARY TABLE TempTable
(
ItemID int NOT NULL,
Col1 ...,
Col2 ...,
...
)
ENGINE = MEMORY
SELECT ItemID, Col1, Col2, ...
FROM MainTable
WHERE <complex criteria>
;
INSERT INTO AuditTable (ItemID, RequestDateTime)
SELECT ItemID, NOW()
FROM TempTable;
SELECT ItemID, Col1, Col2, ...
FROM TempTable
ORDER BY ...;
SQL Server (just to tease you. this single statement does both INSERT and SELECT)
MERGE INTO AuditTable
USING
(
SELECT ItemID, Col1, Col2, ...
FROM MainTable
WHERE <complex criteria>
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
(ItemID, RequestDateTime)
VALUES
(Src.ItemID, GETDATE())
OUTPUT
Src.ItemID, Src.Col1, Src.Col2, ...
;
You can leave Audit table as it is, or you can set up cron to summarize it periodically. It really depends on the volume of data. In our system we store individual rows for a week, plus we summarize stats per hour and keep it for 6 weeks, plus we keep daily summary for 18 months. But, important part, all these summaries are separate Audit tables, we don't keep auditing information in the Main table, so we don't need to update it.
Joe Celko explained it very well in SQL Style Habits: Attack of the Skeuomorphs:
Now go to any SQL Forum text search the postings. You will find
thousands of postings with DDL that include columns named createdby,
createddate, modifiedby and modifieddate with that particular
meta data on the end of the row declaration. It is the old mag tape
header label written in a new language! Deja Vu!
The header records appeared only once on a tape. But these meta data
values appear over and over on every row in the table. One of the main
reasons for using databases (not just SQL) was to remove redundancy
from the data; this just adds more redundancy. But now think about
what happens to the audit trail when a row is deleted? What happens to
the audit trail when a row is updated? The trail is destroyed. The
audit data should be separated from the schema. Would you put the log
file on the same disk drive as the database? Would an accountant let
the same person approve and receive a payment?
You're kind of asking if MySQL supports a SELECT trigger. It doesn't. You'll need to do this as two queries, however you can stick those inside a stored procedure - then you can pass in the range you're fetching, have it both return the results AND do the INSERT into the other table.
Updated answer with skeleton example for stored procedure:
DELIMITER $$
CREATE PROCEDURE `FetchRows`(IN StartID INT, IN EndID INT)
BEGIN
UPDATE Blah SET ViewCount = ViewCount+1 WHERE id >= StartID AND id <= EndID;
# ^ Assumes counts are stored in the same table. If they're in a seperate table, do an INSERT INTO ... ON DUPLICATE KEY UPDATE ViewCount = ViewCount+1 instead.
SELECT * FROM Blah WHERE id >= StartID AND id <= EndID;
END$$
DELIMITER ;

How to program a recurring billing/invoice system using PHP and MySQL

I have already programmed a basic invoicing system using PHP/MySQL which has a table structure as follows;
Invoice table; invoice_id, invoice_date, customer_id, etc
Invoice line table; invoice_line_id, invoice_id, quantity, price, description, etc
I need the system to automatically generate future invoices at set intervals (for example every 1 week or every 2 months, etc). I was thinking of creating a new table as follows;
Invoice schedule table; invoice_schedule_id, invoice_id, interval (e.g. 1), interval_unit (months), start date, next_processing_date
My idea was to then setup a cron job which would execute a PHP file once a day. The PHP file would then generate an invoice when the next_processing_date matched today's date and update the next_processing_date in the database. I'm comfortable on how to achieve this but what I'm stuck with is how to actually insert the new invoice into the table/database. Does MySQL have any type of 'copy row' feature as the new invoice would be identical to the original one except for the invoice_date having to be updated.
Cron sounds good. (Also it is worth to mention the MySQL Event Scheduler, but again I would go for a Cronjob)
A copy would be something like this SQLFIDDLE:
create table t ( id int, d date );
insert into t values( 0, CURDATE() );
insert into t values( 2, CURDATE() );
insert into t values( 1, CURDATE() );
insert into t ( select id+1,curdate() from t order by id desc limit 1 );
Above example is to copy the latest order as a copy, of course you could put a where clause where id=1 or what id your reference order is.
BigScar's reference of "How to copy a row and insert in same table with a autoincrement field in MySQL?" seems to solve your copy-insert problem.
However, since you are mostly doing a specific group of DB queries, instead of cronjobs, you may use MySQL events. If your MySQL version supports them (check in phpmyadmin: select a DB and look to the top menu bar, you can create them there without even have to know the syntax), it's a good practical alternative.

Mysql insert only if limit has not been reached

I hope I have got the title right as I don't really know how to word this!!
Here's the background...
I have a app inserting data to a db. The db holds a date field and data field (there are others but for simplicity these two are the only ones needed). There can only be 8 entries on the same date, no more. In normal operation this is not really an issue but twice a month the database gets hit hard towards the end of the day.
The way I currently do it, is to query how many records there are for a date. If that's less than 9 I insert the data.
It has never happened yet but my concern is that request A comes in and checks the DB and finds there are 7 records. OK to insert. But before A can insert, request B comes in and finds only 7 records. OK to insert. But that would then enter 9 records on one date. This can't happen.
I think there are two solutions to this but would like to know A, if I'm right at all! or B is there a better solution.
I think...
A) I could do it in a transaction. However, would I still no encounter the same problem? As far as I am aware as long as no queries in a transaction fail then it runs anyway.
or
B) Use a stored procedure to check first then insert. I have had very little experience with stored procedures so I must admit I have no idea what I'm talking about here!
Alternatively, is there a way to get a single query to check first if there is less than 9 entries??
Many Thanks in advance,
Gavin
You are afraid of that someone would insert a new record before other one insert it even in the million second?
I have this question too, so I just searched something for you, you can do this with two ways:
For example if you have a table which named date_table and it looks like:
date_field
----------
1998-07-13
1992-03-23
1998-09-17
1994-02-30
A) Count the rows, then insert it when it's under 8 rows. (Using PHP and MySQL)
First, get how many rows are there by this:
SELECT COUNT(*) AS Num FROM date_table WHERE date_field = '1998-07-13'
so you'll get a Num field which told you how many rows were 1998-07-13,
then using PHP to prevent user insert the same date when it's equal 8 by:
<?php if($DB['Num'] >= 8) exit('Sorry, no more same value'); ?>
otherwise insert the row.
Or you don't trust PHP, or you think someone would insert the row more earlier than 1 million second
B) Insert it when it's only under 8 rows were same with only one query:
INSERT INTO date_table (date_field)
SELECT '1998-07-13'
FROM dual
WHERE (SELECT COUNT(*) FROM date_table WHERE date_field = '1998-07-13') < 9;
BEWARE: dual is a mysql system table, so you don't need to create it
If you don't understand how the above query works:
insert into TABLE_NAME (TABLE_FIELD)
select VALUE_YOU_WANT_TO_INSERT
from dual
where (select count(*)
from TABLE_NAME
where THE_FIELD_YOU_WANT_TO_CHECK = '1998-07-13'
) < LESS_THEN_HOW_MUCH;
EDIT: add more fields change the following lines:
INSERT INTO date_table (field1, field2, field3)
SELECT value1, value2, value3

SELECT and UPDATE statements one after other

I am working on an Online Booking System. In a normal process when a user selects a room which was marked as 'Available' at a particular time and submits his choice. Now at back-end, just to confirm, I run a SELECT statement to verify the room's status is available or not as follows:
SELECT room_status FROM rooms WHERE room_number = 'XXX'
then I run a UPDATE query to mark it as 'UNAVAILABLE':
UPDATE rooms SET room_status = 'UNAVAILABLE' WHERE room_number = 'XXX' AND room_status = 'AVAILABLE'
Now when I ran SELECT query, the room was available and as I proceed to UPDATE query, the room is taken by other user. So the UPDATE query will fail. I want the room to be taken by first user only. What should I do ?
UPDATE: There can also be a case when a user has to book multiple rooms. For instance user selects 8 rooms. My code was at 4th room checking its status and as it reached 8th room the first room is gone. Also it may happen, while I was checking 4th room and upto now 8th room is gone.
I am using : PHP and MySQL
When selecting the rows, you need to lock them, so that other queries can't read or write from them until you finish updating them. In MySQL, this can be done inside a transaction using the SELECT ... FOR UPDATE statement:
START TRANSACTION;
SELECT room_status FROM rooms WHERE room_number = 'XXX' FOR UPDATE;
All the rows returned from the previous statement will be locked until the end of the current transaction. You can now mark them as unavailable:
UPDATE rooms SET room_status = 'UNAVAILABLE' WHERE room_number = 'XXX';
Remember to close the transaction by commiting the changes. This will release the lock on the rows.
COMMIT;
In this situation, submit the UPDATE first:
UPDATE rooms
SET room_status = 'UNAVAILABLE'
WHERE room_number = 'XXX' AND room_status = 'AVAILABLE'
Then check the number of affected rows. If the number of affected rows is 1, then you successfully booked the room.
If anyone else submits the exact same query, only one will succeed.
I find that on Insert, Update and Delete you need to add with NC (no commit) to the end of the statement, otherwise it doesn't work.
That will hopefully fix it.

Keep only 10 records per user

I run points system on my site so I need to keep logs of different action of my users into database. The problem is that I have too many users and keeping all the records permanently may cause server overload... I there a way to keep only 10 records per user and automatically delete older entries? Does mysql have some function for this?
Thanks in advance
You can add a trigger that takes care of removing old entries.
For instance,
DELIMITER //
CREATE definer='root'#'localhost' TRIGGER afterMytableInsert AFTER INSERT ON MyTable
FOR EACH ROW
BEGIN
DELETE FROM MyTable WHERE user_id = NEW.user_id AND id NOT IN
(SELECT id FROM MyTable WHERE user_id = NEW.user_id ORDER BY action_time DESC LIMIT 10);
END//
Just run an hourly cron job that deletes the 11th - n records.
Before insert a record you could check how many the user has first. If they have >=10 delete the oldest one. Then insert the new one.
If your goal is to have the database ensure that for a given table there are never more than N rows per a given subkey (user) then the correct way to solve this will be either:
Use stored procedures to manage inserts in the table.
Use a trigger to delete older rows after an insert.
If you're already using stored procedures for data access, then modifying the insert procedure would make the most sense, otherwise a trigger will be the easiest solution.
Alternately if your goal is to periodically remove old data, then using a cron job to start a stored procedure to prune old data would make the most sense.
When you are inserting a new record for a user. Just do a query like this before (Don't forget the where-condition):
DELETE FROM tablename WHERE userID = 'currentUserId' LIMIT 9, 999999
After that you can insert new data. This keeps the data always to ten records for each user.
INSERT INTO tablename VALUES(....)
DELETE FROM Table WHERE ID NOT IN (SELECT TOP 10 ID FROM Table WHERE USER_ID = 1) AND USER_ID = 1
Clearer Version
DELETE FROM Table
WHERE ID NOT IN
(
SELECT TOP 10 ID FROM Table WHERE USER_ID = 1
)
AND USER_ID = 1

Categories