how to add a constraint that checks multiple rows of information? - mysql

I need to make a CONSTRAINT that checks data on multiple rows, an indefinite amount of them.
I've seen that CHECK can only reach data inside the current row, and I have also seen that to solve this problem you need to declare a function (or something that looks just like one).
For clarification:
CREATE TABLE IF NOT EXISTS `ownership`(
`owner_id` INT,
`property_id` INT,
`share` DECIMAL(5,4),
CONSTRAINT `chk_share` CHECK (`share` <= 1.0 AND `share` >= 0),
--CONSTRAINT
-- `share` should show the percentage of the property owner owns
-- I want to add a constraint that does not allow the sum of all
-- ownership rows with the same property_id be greater than 1.
PRIMARY KEY(`owner_id`, `property_id`),
CONSTRAINT `fk_owner`
FOREIGN KEY(`owner_id`)
REFERENCES `market`.`entity` (`entity_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `fk_property`
FOREIGN KEY(`property_id`)
REFERENCES `market`.`entity` (`entity_id`)
ON UPDATE CASCADE
ON DELETE CASCADE
)
I did not find this question answered on this site, so I am asking politely.
I use MySQL Workbench.

Like #Schwern said I used a trigger.
DELIMITER |
CREATE TRIGGER `tgr_share`
BEFORE INSERT
ON `market`.`ownership`
FOR EACH ROW
BEGIN
IF ( SELECT SUM(`share`) + NEW.`share`
FROM `market`.`ownership`
WHERE `property_id` = NEW.`property_id`
GROUP BY `property_id` > 1.0 )
THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'ownership total shares > 1';
END IF;
END;
|
DELIMITER ;
I tested it and it works as intended.
Since I needed to use it with both insert and update, the code bellow works even better:
DELIMITER |
CREATE PROCEDURE `fn_share` (`new_share` DECIMAL(5,4), `new_property_id` INT)
BEGIN
IF ( SELECT SUM(`share`) + `new_share`
FROM `market`.`ownership`
WHERE `property_id` = `new_property_id`
GROUP BY `property_id` > 1.0)
THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'ownership total shares > 1';
END IF;
END;
|
DELIMITER ;
CREATE TRIGGER `tgr_share_insert`
BEFORE INSERT
ON `market`.`ownership`
FOR EACH ROW
CALL `fn_share`(NEW.`share`, NEW.`property_id`);;
CREATE TRIGGER `tgr_share_update`
BEFORE UPDATE
ON `market`.`ownership`
FOR EACH ROW
CALL `fn_share`(NEW.`share`, NEW.`property_id`);;

Related

Is there a way to stop incrementing rows at n when the primary key is set to AUTO_INCREMENT?

I want only 6400 number of rows in my newtable. How do I do this?
I have a table that looks like this:
CREATE TABLE `newtable` (
ID_NT int(10) NOT NULL AUTO_INCREMENT
)ENGINE=InnoDB DEFAULT CHARSET=latin1;
You can use a trigger on your table. This works of course only if you don't delete rows
DELIMITER $$
CREATE TRIGGER InsertPreventTrigger BEFORE INSERT ON yourtable
FOR EACH ROW
BEGIN
DECLARE idcount INT;
set idcount = ( select count(*) from id where request = new.request );
IF idcount> 6400
THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'You can not insert record';
END $$
DELIMITER ;
And you have ti change yourtable and id ti fit to your needs
Updates to with count rows, that fits the request better

MySQL which trigger to use to limit entry number?

I have a table Products
CREATE TABLE `products` (
`transactionnumber` int(11) NOT NULL AUTO_INCREMENT,
`customerID` mediumint(6) unsigned zerofill NOT NULL,
`product` varchar(100) NOT NULL,
`datebought` date NOT NULL,
PRIMARY KEY (`transactionnumber`),
UNIQUE KEY `product_unique` (`customerID`,`product`,`datebought`),
UNIQUE KEY `product_unipr` (`customerID`,`product`),
KEY `product` (`product`),
CONSTRAINT `prod_ibfk_1` FOREIGN KEY (`customerID`) REFERENCES `custdetails` (`ID`),
CONSTRAINT `prod_ibfk_2` FOREIGN KEY (`product`) REFERENCES `product catalogue` (`Itemdescription`)
) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=latin1 |
I am trying to add a trigger which would prevent anyone inserting any products if a customerID is already associated with 10 products.
I tried writing something like this
Delimiter ^^
CREATE TRIGGER maxfiveproducts BEFORE INSERT on Products
FOR EACH ROW BEGIN
if ((SELECT CustomerID, Count(*) from products GROUP BY CustomerID)>10) then
signal allstate '45000' SET MESSAGE_TEXT = 'No more than 10 products per customer!';
end if;
end
^^
Delimiter ;
I am aware that my code may be bad as I have just started learning about triggers and can't find this specific example anywhere
It would look more like this:
Delimiter ^^
CREATE TRIGGER maxfiveproducts BEFORE INSERT on Products
FOR EACH ROW BEGIN
set #cnt = (select count(*) from products p where p.customerId = new.customerId);
if (#cnt >= 10) then
signal allstate '45000' SET MESSAGE_TEXT = 'No more than 10 products per customer!';
end if;
end;
Delimiter ;
Why is the name "maxfiveproducts"?
You can use DECLARE to declare the variable, store the COUNT in it and write an IF condition, e.g.:
CREATE TRIGGER pgl_new_user
BEFORE INSERT ON Products FOR EACH ROW
BEGIN
DECLARE existing_products integer;
SET #existing_products := (SELECT COUNT(*) FROM Products WHERE customerID = NEW.customerID);
IF(#existing_products >= 10) THEN
SIGNAL ALLSTATE '45000' SET MESSAGE_TEXT = 'No more than 10 products per customer!';
END IF;
END//

How to build a relationship between multiple sub-entities and a list attribute?

The model of the (PHP & MySQL based) application I'm currently working on contains a inheritance similar to the one described here. Simplified for the purpose of this question the classes structure can look like this:
To map this to the database I'm using the Class Table Inheritance design pattern. Here is the physical data model:
The most specific attributes are actually specific for every single subclass. But there are some attributes, that are needed in several classes (but also not in all of them -- otherwise they could be managed in the Foo class/table). When it's a simple attribute, it causes some code duplication, but isn't a big problem. But there are also some cases with complex attributes.
E.g.: FooTypeBaz and FooTypeBuz should contain a list of Whatever elements.
Normally I would implement this 1:n relationship with a table whatever containing FOREIGN KEY. But in this case I would need multiple FOREIGN KEY columns whatever (for foo_type_baz, foo_type_buz, and maybe some tables more). It's dirty.
Another solution: Something like a "facade" table for the table whatever:
Looks better (for me), but I'm still not happy with this model.
How to build a relationship between multiple sub-entities and a collection/list attribute? Is there an elegant solution for this problem? Maybe a best practice / design pattern?
Recording the relationship is easy enough - you could make a table foo_whatever (foo_id PK, whatever_set_id FK) and insert rows only for appropriate foo ids. However, that schema doesn't enforce any constraint on the subtypes you can associate with whatever sets, but neither does your existing schema enforce that subtypes are mutually exclusive. It's possible to enforce both with the same technique.
Consider including a type indicator on all the foo_* tables, e.g. using an enum('bar', 'baz', 'buz'). This provides subtype information in foo (which can be more convenient than joining 3 tables to find a match) and allows foreign key constraints and check constraints to enforce exclusive subtypes and restrict the types that can be recorded in foo_whatever. Yes, it involves a bit of redundant information, but it's small and there's no risk of update anomalies.
Using composite foreign key constraints that involve a type indicator, together with check constraints that limit the value of the type indicator for each subtype table, should do the trick. Here's the schema I suggest:
CREATE TABLE `foo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` enum('bar','baz','buz') NOT NULL,
PRIMARY KEY (`id`),
KEY `foo_id` (`id`,`type`)
);
CREATE TABLE `foo_type_bar` (
`foo_id` int(11) NOT NULL,
`foo_type` enum('bar','baz','buz') NOT NULL CHECK (foo_type = 'bar'),
PRIMARY KEY (`foo_id`),
KEY `foo_bar_fk` (`foo_id`,`foo_type`),
CONSTRAINT `foo_bar_fk` FOREIGN KEY (`foo_id`, `foo_type`)
REFERENCES `foo` (`id`, `type`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE `foo_type_baz` (
`foo_id` int(11) NOT NULL,
`foo_type` enum('bar','baz','buz') NOT NULL CHECK (foo_type = 'baz'),
PRIMARY KEY (`foo_id`),
KEY `foo_baz_fk` (`foo_id`,`foo_type`),
CONSTRAINT `foo_baz_fk` FOREIGN KEY (`foo_id`, `foo_type`)
REFERENCES `foo` (`id`, `type`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE `foo_type_buz` (
`foo_id` int(11) NOT NULL,
`foo_type` enum('bar','baz','buz') NOT NULL CHECK (foo_type = 'buz'),
PRIMARY KEY (`foo_id`),
KEY `foo_buz_fk` (`foo_id`,`foo_type`),
CONSTRAINT `foo_buz_fk` FOREIGN KEY (`foo_id`, `foo_type`)
REFERENCES `foo` (`id`, `type`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE `foo_whatever` (
`foo_id` int(11) NOT NULL,
`foo_type` enum('bar','baz','buz') NOT NULL CHECK (foo_type IN ('baz', 'buz')),
`whatever_set_id` int(11) NOT NULL,
PRIMARY KEY (`foo_id`),
KEY `whatever_foo_fk` (`foo_id`,`foo_type`),
KEY `whatever_set_fk` (`whatever_set_id`),
CONSTRAINT `whatever_foo_fk` FOREIGN KEY (`foo_id`, `foo_type`)
REFERENCES `foo` (`id`, `type`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `whatever_set_fk` FOREIGN KEY (`whatever_set_id`)
REFERENCES `whatever_set` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);
However, since MySQL ignores check constraints, you would need to use triggers to achieve the same:
DELIMITER ;;
CREATE TRIGGER foo_bar_insert_type_check
BEFORE INSERT ON foo_type_bar
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'bar' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_bar';
END IF;
END;;
CREATE TRIGGER foo_bar_update_type_check
BEFORE UPDATE ON foo_type_bar
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'bar' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_bar';
END IF;
END;;
CREATE TRIGGER foo_baz_insert_type_check
BEFORE INSERT ON foo_type_baz
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'baz' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_baz';
END IF;
END;;
CREATE TRIGGER foo_baz_update_type_check
BEFORE UPDATE ON foo_type_baz
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'baz' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_baz';
END IF;
END;;
CREATE TRIGGER foo_buz_insert_type_check
BEFORE INSERT ON foo_type_buz
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'buz' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_buz';
END IF;
END;;
CREATE TRIGGER foo_buz_update_type_check
BEFORE UPDATE ON foo_type_buz
FOR EACH ROW
BEGIN
IF NEW.foo_type != 'buz' THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_type_buz';
END IF;
END;;
CREATE TRIGGER foo_whatever_insert_type_check
BEFORE INSERT ON foo_whatever
FOR EACH ROW
BEGIN
IF NEW.foo_type NOT IN ('baz', 'buz') THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_whatever';
END IF;
END;;
CREATE TRIGGER foo_whatever_update_type_check
BEFORE UPDATE ON foo_whatever
FOR EACH ROW
BEGIN
IF NEW.foo_type NOT IN ('baz', 'buz') THEN
SIGNAL SQLSTATE '12345'
SET MESSAGE_TEXT = 'Invalid foo_type in foo_whatever';
END IF;
END;;
DELIMITER ;

How to use trigger in MySql to make foreign key

I want to use trigger to make foreign key in MySql. I have the following tables:
1) 'content' table:
teacher_id varchar(20)
sub_id varchar(20)
path varchar(100)
file_name varchar(100)
2) 'teacher' table:
teacher_id varchar(20)
teacher_name varchar(45)
and I am using the following code for trigger(delimiter //):
CREATE TRIGGER fk_content_teacher_temp BEFORE INSERT ON `content`
FOR EACH ROW
BEGIN
DECLARE has_row TINYINT;
SET has_row = 0;
SELECT 1 INTO has_row FROM `teacher` INNER JOIN `content` ON content.teacher_id=teacher.teacher_id;
IF has_row=0 THEN
INSERT error_msg VALUES ('Foreign Key Constraint Violated!');
END IF;
END//
The problem is, when am trying to insert in content table for a teacher_id which is not present in teacher table, I get the following error:
1172 - Result consists of more than one row
What can I do to make it work fine, or any other way i can use trigger to make foreign keys?
Thank you in advance!
While it is not clear what exactly you intend with the statement "use trigger to make foreign key", your current issue is that SELECT INTO cannot be used in queries that return more than one result.
SELECT 1 INTO has_row FROM teacher INNER JOIN content ON content.teacher_id=teacher.teacher_id;
returns EVERY match between the two tables.
If you were trying to check if teacher contains the teacher_id value being used in the new content record, you should just be able to drop the JOIN clause completely and just query like so:
SELECT 1 INTO has_row FROM `teacher` WHERE `teacher_id` = NEW.`teacher_id`;
While this is an oldish question I would like to provide some insight for future searchers on how one might deal with such issue.
In a recent project I was unable to use InnoDB but had to use the MyISAM engine (in reality it was MariaDB's Aria engine) for a database transfer which contained foreign keys.
I opted for implementing foreign keys using triggers as described here.
A great intro into the subject is provided here: https://dev.mysql.com/tech-resources/articles/mysql-enforcing-foreign-keys.html
However, I will outline my solution as I found some thing not fully workable for me in the above. E.g. Any update to a parent table was completely prohibited in their "restrict" example when a foreign child key existed even though the child was not affected.
For demonstration I use the following table definitions and test data:
CREATE TABLE `__parent` (`id` int UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) ENGINE=`Aria`;
CREATE TABLE `__child` (`id` int UNSIGNED NOT NULL AUTO_INCREMENT,`parent_id` int UNSIGNED, PRIMARY KEY (`id`), INDEX `parent_id_idx` USING BTREE (`parent_id`) ) ENGINE=`Aria`;
INSERT INTO __parent VALUES (1), (2), (3);
INSERT INTO __child VALUES (1,1), (2,2), (3,1), (4,2), (5,3), (6,1);
Prevent inserts into a child table when no corresponding linked parent entry exists:
DELIMITER //
CREATE TRIGGER __before_insert_child BEFORE INSERT ON __child FOR EACH ROW
BEGIN
IF (SELECT COUNT(*) FROM __parent WHERE __parent.id=new.parent_id) = 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t insert record. Foreign parent key does not exist!';
END IF;
END //
DELIMITER ;
Prevent updates to a child table where it would unlink a child record:
DELIMITER //
CREATE TRIGGER __before_update_child BEFORE UPDATE ON __child FOR EACH ROW
BEGIN
IF (SELECT COUNT(*) FROM __parent WHERE __parent.id = new.parent_id) = 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t update record. Foreign parent key does not exist!';
END IF;
END //
DELIMITER ;
Cascading updates to the child table when the parent is updated:
DELIMITER //
CREATE TRIGGER __after_update_parent AFTER UPDATE ON __parent FOR EACH ROW
BEGIN
UPDATE __child SET __child.parent_id=new.id WHERE __child.parent_id=old.id;
END //
DELIMITER ;
Cascade deletes to the child table when a parent is deleted:
DELIMITER //
CREATE TRIGGER __after_delete_parent AFTER DELETE ON __parent FOR EACH ROW
BEGIN
DELETE FROM __child WHERE __child.parent_id=old.id;
END;
END //
DELIMITER ;
Sometime you don't want to cascade but restrict. In this case use the following instead:
Restrict parent updates to the child table:
DELIMITER //
CREATE TRIGGER __before_update_parent BEFORE UPDATE ON __parent FOR EACH ROW
BEGIN
IF ( old.id <> new.id AND (SELECT COUNT(*) FROM __child WHERE __child.parent_id = old.id) <> 0 ) THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t update record. Foreign key updates to child table restricted!';
END IF;
END //
DELIMITER ;
Restrict parent deletes from the child table:
DELIMITER //
CREATE TRIGGER __before_delete_parent BEFORE DELETE ON __parent FOR EACH ROW
BEGIN
IF ( SELECT COUNT(*) FROM __child WHERE __child.parent_id = old.id) <> 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 30001, MESSAGE_TEXT = 'Can\'t delete record. Foreign key exists in child table!';
END IF;
END //
DELIMITER ;
Hope this helps someone.

MySQL: Custom row validation

I have such example:
CREATE TABLE a(
id INT PRIMARY KEY AUTO_INCREMENT,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
DELIMITER ;;
CREATE TRIGGER a_insert BEFORE INSERT ON a
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '01431' SET MESSAGE_TEXT = 'The foreign data source you are trying to reference does not exist.';
END;;
DELIMITER ;
INSERT INTO a(parent_id) VALUES (NULL);
INSERT INTO a(parent_id) VALUES (1);
INSERT INTO a(parent_id) VALUES (2);
INSERT INTO a(parent_id) VALUES (4);
INSERT INTO a(parent_id) VALUES (999);
SELECT * FROM a
This end up with 4 recods:
----------------
id parent_id
----------------
1 NULL
2 1
3 2
4 4
I found post online that MySQL does not support rollbacks in triggers. That is a problem because I want such hierarchy where no row points to it self and inserts like the one with ID=4 works just fine. How do I ensure there are no such records in database?
Well, the problem is with auto_increment, because you in BEFORE INSERT event you don't have that value assigned yet. On the other hand in AFTER INSERT event you can't do anything with it.
If you want to use auto_increment id column a possible solution is to use separate table for sequencing.
Your schema would look like
CREATE TABLE a_seq(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE a(
id INT NOT NULL PRIMARY KEY DEFAULT 0,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
And your trigger
DELIMITER $$
CREATE TRIGGER a_insert
BEFORE INSERT ON a
FOR EACH ROW
BEGIN
INSERT INTO a_seq VALUES(NULL);
SET NEW.id = LAST_INSERT_ID();
IF NEW.id = NEW.parent_id THEN
SET NEW.id = NULL;
END IF;
END$$
DELIMITER ;
If id=parent_id the trigger deliberately violates NOT NULL constraint assigning NULL value. Therefore this record won't be inserted.
Here is SQLFiddle demo. Uncomment last insert statement. It won't allow you to make such insert.