I have an auction application with these two tables (this is highly simplified, obviously):
create table auctions (
auction_id int,
end_time datetime
);
create table bids (
bid_id int,
auction_id int,
user_id int,
amount numeric,
bid_time timestamp,
constraint foreign key (auction_id) references auctions (auction_id)
);
I don't want bids on an auction after that auction has ended. In other words, rows in the bids table should be allowed only when the bid_time is earlier than the end_time for that auction. What's the simplest way to do this in MySQL?
Ufortunately MySQL does not have a CHECK constraint feature. But You should be able to enforce this using a trigger. However, MySQL trigger support isn't as advanced or well optimized as it is in other RDBMS-es, and you will suffer a considerable performance hit if you do it this way. So if this is a real-time trading system with massive amounts of concurrent users, you should look for another solution.
CREATE TRIGGER bir_bids
BEFORE INSERT ON bids
FOR EACH ROW
BEGIN
DECLARE v_end_time datetime;
-- declare a custom error condition.
-- SQLSTATE '45000' is reserved for that.
DECLARE ERR_AUCTION_ALREADY_CLOSED CONDITION FOR SQLSTATE '45000';
SELECT end_time INTO v_end_time
FROM auctions
WHERE auction_id = NEW.auction_id;
-- the condition is written like this so that a signal is raised
-- only in case the bid time is greater than the auction end time.
-- if either bid time or auction end time are NULL, no signal will be raised.
-- You should avoid complex NULL handling here if bid time or auction end time
-- must not be NULLable - simply define a NOT NULL column constraint for those cases.
IF NEW.bid_time > v_end_time THEN
SIGNAL ERR_AUCTION_ALREADY_CLOSED;
END IF;
END:
Note that the SIGNAL syntax is available only since MySQL 5.5 (currently GA). Triggers are available since MySQL 5.0. So if you need to implement this in a MySQL version prior to 5.5, you need to hack your way around not being able to raise a signal. You can do that by causing some change in the data that will guarantee the INSERT to fail. For instance you could write:
IF NEW.bid_time > v_end_time THEN
SET NEW.auction_id = NULL;
END IF;
Since acution_id is declared NOT NULL in the table, the state of the data will be such that it cannot be inserted. The drawback is that you will get a NOT NULL constraint violation, and the application will have to guess whether this is due to this trigger firing or due to a "real" NOT NULL constraint violation.
For more information, see: http://rpbouman.blogspot.nl/2009/12/validating-mysql-data-entry-with_15.html and http://rpbouman.blogspot.nl/2006/02/dont-you-need-proper-error-handling.html
Insert into bids (auction_id, user_id, amount, bid_time)
Select auction_id, [USER_ID], [AMOUNT], [TIMESTAMP]
From auctions
WHERE UNIX_TIMESTAMP() <= UNIX_TIMESTAMP(end_time)
Of course you have to replace the '[]' values
instead do a simple thing create a column name status. set its type to enum. when you want to block it update its value to 0. default should be 1 means open. 0 means closed
Related
I am trying to make a MySQL stored procedure that processes a book purchase and inserts records into other tables about the purchase. However, these insertions can only happen if three conditions are met: the customer is in the system, the book is in the system, and there is enough quantity.
I want to check for each condition individually, and if it passes the first condition, it moves to the next, but if it doesn't, I want it to end the procedure and return a value, and so on for each condition. If it passes all three conditions, the insertions can happen. Here's how I coded it:
DELIMITER //
CREATE PROCEDURE process_purchase(
IN book_key INT,
IN customer_key INT,
IN quantity INT
)
BEGIN
DECLARE book_inventory_key_var INT;
DECLARE purchase_key_var INT;
SELECT book_inventory_key
INTO book_inventory_key_var
FROM book_inventory
WHERE book_key = book_key.book_inventory_key;
SELECT purchase_key
INTO purchase_key_var
FROM purchases
WHERE customer_key = customer_key.purchases;
IF customer_key != customer_key.customers THEN
SELECT '-1';
ELSEIF book_key != book_key.books THEN
SELECT '-2';
ELSEIF quantity < quantity_on_stock(book_key) THEN
SELECT '-3';
ELSE
INSERT INTO purchases VALUES (customer_key, CURDATE());
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity) VALUES (book_inventory_key_var, purchase_key_var, quantity);
SELECT '1';
END IF;
END//
DELIMITER ;
I compare the customer and book keys to their values in the other tables, and the quantity to the quantity_on_stock stored function I previously made. I use a chain of IF-ELSEIF to go through each condition one by one, and if all of them are passed, the insertions occur. If not, it won't go to the next condition, and will return the SELECT message, correct? The procedure runs without errors, but I am unsure if this is the correct method, or if there's a better way of going about this.
Checking sequentially is subject to race conditions. Breaking this paradigm is key to moving from a procedural to SQL based method. Use the database features of to obtain consistency rather than procedural code.
purchase_items should have foreign key constraints to book_key and customer_key tables. If an insert generates a FK exception then one of these apply depending on the error. DECLARE HANDLER will help catch these errors.
For the quantity:
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity)
SELECT book_key, purchase_key, quantity
FROM books WHERE book_key = book.id AND book.available >= quantity
If there are no ROW_COUNT for this, then there wasn't sufficient quantity.
You will also need to reduce the amount of books available in the same SQL transaction.
If you don't have to do this in a STORED PROCEDURE, don't. A lot of the constructs here are easier in application code. If this is an assignment, where store procedures are require to get extra marks, get through it, and never write a stored procedure again.
First, this is OpenCart
I have two tables:
1. oc_product (product_id, model, price, event_start, event_end and etc.)
2. oc_product_to_category (product_id, category_id)
Every product has Start Date and End Date. I created MYSQL event that catch every product with expired date (event_end < NOW()) to store it in category "Archive" with id = 68
Here is the code of MYSQL EVENT
CREATE EVENT move_to_archive_category
ON SCHEDULE EVERY 1 MINUTE
STARTS NOW()
DO
INSERT INTO `oc_product_to_category` (product_id, category_id)
SELECT product_id, 68 as category_id
FROM oc_product p WHERE p.event_end < NOW() AND p.event_end <> '0000-00-00';
When event starts it works properly! BUT, when I got to administration and publish new product with expired date I'm waiting 1 minute to see the product in "Archive" category but nothing happens.
I saw in "SHOW PROCESSLIST" and everything is OK:
event_scheduler localhost NULL Daemon 67 Waiting for next activation NULL
and also "SHOW EVENTS" looks good
Db Name Definer Time zone Type Execute at Interval value Interval field Starts Ends Status Originator character_set_client collation_connection Database Collation
events move_to_archive_category root#localhost SYSTEM RECURRING NULL 1 MINUTE 2016-08-15 13:37:54 NULL ENABLED 1 utf8 utf8_general_ci utf8_general_ci
I'm working locally, not live
Any ideas?
Thanks in advance! :)
I suggest turning on the sonar. I have 3 event links hanging off my profile page. So I created a few helper tables (that can also be seen in those links) to assist is turning on the sonar to see what is up in your events. Note you can expand on it for performance tracking as I did in those links.
Remember that Events succeed or fail (in your mind) based on the data and they do so silently. But tracking what is going on, you can vastly increase you happiness level when developing in them.
Event:
DROP EVENT IF EXISTS move_to_archive_category;
DELIMITER $$
CREATE EVENT move_to_archive_category
ON SCHEDULE EVERY 1 MINUTE STARTS '2015-09-01 00:00:00'
ON COMPLETION PRESERVE
DO
BEGIN
DECLARE incarnationId int default 0;
DECLARE evtAlias varchar(20);
SET evtAlias:='move_2_archive';
INSERT incarnations(usedBy) VALUES (evtAlias);
SELECT LAST_INSERT_ID() INTO incarnationId;
INSERT EvtsLog(incarnationId,evtName,step,debugMsg,dtWhenLogged)
SELECT incarnationId,evtAlias,1,'Event Fired, begin looking',now();
INSERT INTO `oc_product_to_category` (product_id, category_id)
SELECT product_id, 68 as category_id
FROM oc_product p WHERE p.event_end < NOW() AND p.event_end <> '0000-00-00';
-- perhaps collect metrics for above insert and use that in debugMsg below
-- perhaps with a CONCAT into a msg
INSERT EvtsLog(incarnationId,evtName,step,debugMsg,dtWhenLogged)
SELECT incarnationId,evtAlias,10,'INSERT finished',now();
-- pretend there is more stuff
-- ...
-- ...
INSERT EvtsLog(incarnationId,evtName,step,debugMsg,dtWhenLogged)
SELECT incarnationId,evtAlias,99,'Event Finished',now();
END $$
DELIMITER ;
Tables:
create table oc_product_to_category
( product_id INT not null,
category_id INT not null
);
create table oc_product
( product_id INT not null,
event_end datetime not null
);
drop table if exists incarnations;
create table incarnations
( -- NoteA
-- a control table used to feed incarnation id's to events that want performance reporting.
-- The long an short of it, insert a row here merely to acquire an auto_increment id
id int auto_increment primary key,
usedBy varchar(50) not null
-- could use other columns perhaps, like how used or a datetime
-- but mainly it feeds back an auto_increment
-- the usedBy column is like a dummy column just to be fed a last_insert_id()
-- but the insert has to insert something, so we use usedBy
);
drop table if exists EvtsLog;
create table EvtsLog
( id int auto_increment primary key,
incarnationId int not null, -- See NoteA (above)
evtName varchar(20) not null, -- allows for use of this table by multiple events
step int not null, -- facilitates reporting on event level performance
debugMsg varchar(1000) not null,
dtWhenLogged datetime not null
-- tweak this with whatever indexes your can bear to have
-- run maintenance on this table to rid it of unwanted rows periodically
-- as it impacts performance. So, dog the rows out to an archive table or whatever.
);
Turn on Events:
show variables where variable_name='event_scheduler'; -- OFF currently
SET GLOBAL event_scheduler = ON; -- turn her on
SHOW EVENTS in so_gibberish; -- confirm
Confirm Evt is firing:
SELECT * FROM EvtsLog WHERE step=1 ORDER BY id DESC; -- verify with our sonar
For more details of those helper tables, visit those links off my profile page for Events. Pretty much just the one link for Performance Tracking and Reporting.
You will also note that it is of no concern at the moment of having any data in the actual tables that you were originally focusing on. That can come later, and can be reported on in the evt log table by doing a custom string CONCAT into a string variable (for the counts etc). And reporting that in a step # like step 10 or 20.
The point is, you are completely blind without something like this as to know what is going on.
So,
I saw in mysqlog the following errors
160816 10:18:00 [ERROR] Event Scheduler: [root#localhost][events.move_to_archive_category] Duplicate entry '29-68' for key 'PRIMARY'
160816 10:18:00 [Note] Event Scheduler: [root#localhost].[events.move_to_archive_category] event execution failed.
and I just add INGORE in SQL INSERT... so the finally result is
INSERT IGNORE INTO `oc_product_to_category` (product_id, category_id)
I currently have two separate tables with a third to be added which store recipes, items and custom_meal (the one to be created).
I am looking to create a Meal Planner where the user may add either a recipe, item or custom item as their breakfast/lunch/dinner for a specific day.
I have an idea for my new tables below although going down this path I would have no way of ensuring that the meal_planner.id can only be either a recipe, item or custom.
With well written queries I realise that this should never be allowed to be an issue, although as someone with a great deal still to learn about databases I would much prefer a solution that ensures data may never be entered when it shouldn't be.
The current columns of interest on tables I'm already using are:
recipe.id
item.id
user.id
My current thoughts on the "Meal Planner" tables would be:
Table: meal_planner
id - primary key
user_id - foreign key user.id
mdate - the day which the meal is planned for
mtime - whether the meal is breakfast, lunch or dinner.
mcomment (can be null)
Table: meal_is_recipe
meal_id - foreign key meal_planner.id
recipe_id - foreign key recipe.id
Table: meal_is_custom
meal_id - foreign key meal_planner.id
custom_id - foreign key custom_meal.id
Table: meal_is_item
meal_id - foreign key meal_planner.id
item_id - foreign key item.id
Table: custom_meal
id - primary key
user_id - foreign key user.id
name - varchar to hold the name of the meal
This should allow me to use joins to grab all the required data for display although as mentioned it bugs me that their is no constraint stopping a meal.id being used in recipe, item and/or custom.
Ok so since posting this I have discovered "triggers".
Triggers are similar to a stored procedure although in a triggers case they will fire automatically when the set condition is met, for example before completing an insert, update or delete on a table.
In my case I have created six triggers, two for each meal_is_x table to check if the meal_id already exists in one of the other tables and return an error instead of the insert if it does.
I didn't have any luck through the phpmyadmin query using examples so I created my triggers in phpmyadmin by selecting the database, clicking on the Triggers tab, clicking add new trigger and then entering the following (modified slightly for each different table and whether it was for insert or update).
Trigger name: meal_is_recipe_BeforeInsert
Table: meal_is_recipe
Time: BEFORE
Event: INSERT
Definition:
BEGIN
DECLARE custom_match INT;
DECLARE item_match INT;
SELECT COUNT(1) INTO custom_match FROM meal_is_custom
WHERE meal_id = NEW.meal_id;
IF custom_match > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'A Custom record with that meal id already exists';
ELSE
SELECT COUNT(1) INTO item_match FROM meal_is_item
WHERE meal_id = NEW.meal_id;
IF item_match > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'An Item record with that meal id already exists';
END IF;
END IF;
END
Definer: I left this blank as the user that made the query if fine to run the trigger.
I believe I should also have been able to get the same result with the MySQL query but as I mention this did not work straight away for me in phpmyadmin and rather than look into it further the triggers tab was simple enough:
DELIMITER //
CREATE TRIGGER meal_is_recipe_BeforeInsert BEFORE INSERT ON meal_is_recipe
BEGIN
DECLARE custom_match INT;
DECLARE item_match INT;
SELECT COUNT(1) INTO custom_match FROM meal_is_custom
WHERE meal_id = NEW.meal_id;
IF custom_match > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'A Custom record with that meal id already exists';
ELSE
SELECT COUNT(1) INTO item_match FROM meal_is_item
WHERE meal_id = NEW.meal_id;
IF item_match > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'An Item record with that meal id already exists';
END IF;
END IF;
END//
DELIMITER ;
As I mentioned in the question my knowledge working with databases still has a long way to go so whilst I have tested this with my initial question schema and it works, I cannot guarantee this is the most efficient way of achieving the outcome.
I have table called company. Structure as follows:
company
-----------------------------
company_id integer
company_name varchar
fk_company_type varchar
created_date date
fk_company_type is foreign key with the following values:
HQ
SITE
CUSTOMER
SUPPLIER
My issue I only want one record in company table to be HQ (HeadQuarters). Therefore I need a trigger that will count how many HQ in the company table. If it the count returns 1 and the new record being inserted has value of fk_company_type = HQ then the insert is aborted.
Any help on the best way to do this will be much appreciated. Also I already have a trigger which generates a UUID for the company_id and date. Hopefully this does not effect what I am trying to achieve.
phpadmin trigger (time: BEFORE, event: INSERT)
BEGIN
SET NEW.company_id=UUID_SHORT();
SET NEW.created_date=current_timestamp();
END
I've tried the basics layout and got as far as this but it produces a syntax error, here is how far I got.
BEGIN
IF fk_company_type = "HQ" THEN
DECLARE valid_number int;
SELECT COUNT(*) into valid_number FROM company WHERE fk_company_type = "HQ";
IF valid_number > 0 THEN
-- some error message
END IF;
END IF;
SET NEW.company_id=UUID_SHORT();
SET NEW.created_date=current_timestamp();
END
A simple way to abort an insert is to set a value to an illegal one.
for example if the column is set to NOT NULL then set the value to NULL and the insert will fail.
The question/issue of how you tell people it has failed depends on your application.
I am wondering if there is a way to restrict MySQL INSERTs on a given condition, for example:
We have a table with projects and another table with employees. The maximum allowed people in a project is restricted to 5. How can we do this in MySQL?
Thanks in advance,
Marley
You can't enforce this constraint via the schema without breaking the relation (i.e. having 5 employees columns). MySQL does not allow CHECK constraints.
You can SELECT the number of employees separately before doing the query, or you can use INSERT ... SELECT with the restriction.
INSERT INTO Employees
(employee, column, names)
SELECT
?, ?, ?
FROM
Projects
NATURAL JOIN Employees
WHERE
projID = ?
GROUP BY
projID
HAVING
COUNT(empID) < 5
You have to read about constraint, check (or value based) type.
CREATE TABLE tbl (
...
numPeople INT CHECK (numPeople <= 5)
...
)
Yeah, sorry, check constraint doesn't work in MySQL
Possibly use a trigger to check the number of users prior to insert:-
DELIMITER $$
CREATE TRIGGER project_before_insert_allow_only_five_employees
BEFORE INSERT ON project FOR EACH ROW
BEGIN
IF (SELECT COUNT(employee_id) FROM project WHERE project_id=NEW.project_id) > 4
THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot add or update row: only 5 employees on a project';
END IF;
END;
Not tested, and assuming that your project table has a row per employee (easy enough to modify if the project id is instead stored on the employees table for those associated with it).