I'm new to mySQL and I'd like to add a integrity check (or constraint? Sorry I'm italian) in my database.
Let me explain: I have two tables
Workshop (location, numPlaces, numOperations)
Operation (idoperation, workLocation, ...)
numPlaces represents the maximum number of operations the workshop can afford. I created a trigger that everytime I insert a new record in Operation, the numOperations of Workshop referred to that specific location is increased by 1.
Now what I'd like to do is: when numOperations = numPlaces, if I try to insert a new record to Operation, the system must tell me that I can't. Basically, it can't be possible that numOperations > numPlaces
Is there a way to achieve that? Sorry if I can't provide codes, but I literally have no idea where should I go to create these types of CHECKS. Hope you can help me!
For this to work, you must have set the workshop with the correct number of places.
And you should have a routine, that diminishes the number of operations so that you can enter new operation in one workshop
CREATE TABLE Workshop
(location Text, numPlaces int , numOperations int
)
INSERT INTO Workshop VALUES ('A',9,8)
CREATE TABLE Operation (idoperation int, workLocation Text)
CREATE TRIGGER before_Operation_insert
BEFORE INSERT
ON Operation FOR EACH ROW
BEGIN
DECLARE Placescount_ INT;
DECLARE Operationscount_ INT;
SELECT numPlaces, numOperations
INTO Placescount_,Operationscount_
FROM Workshop WHERE location = NEW.workLocation;
IF Placescount_ < Operationscount_ THEN
UPDATE Workshop
SET numOperations = numOperations + 1 WHERE location=new.workLocation ;
ELSE
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Maximum Number of Operarations reached in location ';
END IF;
END
INSERT INTO Operation VALUES (1,'A')
Maximum Number of Operarations reached in location
db<>fiddle here
Related
I want to add a limit to how many items my table can have. I want the maximum amount of items to be 10. I want it to only be 10 people in my table. I dont want it to be able to add items after the 10th person. Here is my code:
CREATE TABLE person (
name VARCHAR(233) NOT NULL,
number int NOT NULL AUTO_INCREMENT,
PRIMARY KEY(number),
Check(number>10))
delimiter //
create trigger limit_persons before insert on person for each row
begin
declare count integer
select COUNT(*) FROM person INTO count;
if count>=10 then
signal sqlstate '45000' set message_text = 'Limit exceeded';
end if;
end
//
I would advise to handle such stuff as limitations in the software itself. So you have control over it later and it is overall a cleaner solution. But you can try this, if you really want to limit it in mysql:
ALTER TABLE tbl_name MAX_ROWS=10 AVG_ROW_LENGTH=nnn;
You can also check out triggers and signals:
https://dev.mysql.com/doc/refman/5.5/en/signal.html
https://dev.mysql.com/doc/refman/5.5/en/trigger-syntax.html
You can set up a trigger (to be specific, an Insert trigger) that counts the records and, if count is more than 10, it does not allow the insert operation.
Following code will be helpful to you,
DELIMITER $$
CREATE TRIGGER LimitRowCountTrigger
BEFORE INSERT
ON person
FOR EACH ROW
BEGIN
SELECT COUNT(*) INTO #cnt FROM person;
IF #cnt > 10 THEN
CALL sth(); -- raise an error
END IF;
END
$$
DELIMITER ;
I have a MySQL table called flights that includes fields called origin and destination which are the airport codes referenced from the airports table and which i have defined as CHAR(3). I want to be able to insert records where for obvious reasons the values in the origin and departure columns cannot be the same.
How do i accomplish this with an INSERT INTO TABLE SQL statement?
The only way you can do this in MySQL is using a trigger.
Other databases support a check constraint where you can say check (origin <> destination). MySQL accepts this syntax but unfortunately does not enforce the constraint.
You can also do this at the application layer.
Create a trigger:
CREATE TRIGGER `travels_before_insert` BEFORE INSERT ON `travels` FOR EACH ROW BEGIN
DECLARE msg VARCHAR(255);
IF NEW.source=NEW.destination THEN
set msg = "Error: You can't have same source and destination";
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END;
What it does is prevent the insert from occuring if source and destination are the same.
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’ve got a table of bookings with start and end times, and no two bookings can overlap.
I need to check that a new booking won’t overlap with any existing bookings. However we’ve got very high load so there’s a race condition: two overlapping bookings can be both successfully inserted because the first booking was inserted after the second booking checked for overlaps.
I’m trying to solve this by taking a lock on a related resource using a BEFORE INSERT database trigger.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
DECLARE ignored INT DEFAULT NULL;
SELECT resource_id INTO ignored
FROM resource
WHERE resource_id = NEW.resource_id
FOR UPDATE;
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
If I put this trigger in the database and insert two bookings asynchronously from the command line, this trigger successfully blocks the overlapping booking. I’ve tried this with putting a SLEEP before the last IF statement in the trigger, to make sure that the lock has really been taken out.
However, I have a load testing environment in Jenkins which runs a lot of bookings concurrently using jMeter. When I put this trigger there and run the load tests, no overlapping bookings are caught, i.e. double bookings are made.
Some checks I’ve done:
I’ve logged out the SQL queries that the load test script generates when creating a booking, and it is the same as the SQL I use in the command line.
The trigger is definitely being triggered in the load test environment, and it is definitely not catching any overlapping bookings. I ascertained this by inserting the “overlapping_booking_resource_id” variable from the trigger into another table. All the values were null.
The trigger works in the load test environment when inserting bookings from the command line, i.e. it prevents the overlapping booking from being inserted.
If I make the constraint for what a “double booking” is slightly too strict, i.e. adjacent bookings count as double bookings, then I do see things being caught by the trigger – that is, the apache log records several errors with the message ‘The inserted times overlap with booking_resource_id:’
I’m wondering if maybe the lock is only taken out until the end of the trigger, and there is still a race condition between the end of the trigger and actually inserting into the table. However this doesn’t explain why none of the overlapping bookings are ever caught.
I’m really stuck now. Does anyone have any ideas as to what I have done wrong?
A less elegant but more robust method would be to use a table made for locking records accross the system.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
---CHANGED HERE
REPEAT
BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING
BEGIN
SET locked = FALSE;
END;
locked=TRUE;
INSERT INTO lockresource values(NEW.resource_id);
END;
UNTIL LOCKED END REPEAT;
---TIL HERE
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
---ADDED FROM HERE
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard_after AFTER INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING BEGIN END;
delete from lockresource where lockid=NEW.resource_id;
END
//
Anyway, that'd be the idea and would certainly prevent any loss of lock until completion of your validation.