Order of deletion with foreign key constraints, - mysql

I have a schema with three tables and foreign key 'On Delete' constraints as below:
| -> FK (cascade) -> |
Organisation | | Users
| - FK (cascade) Categories -> FK(restrict) -> |
If I delete an organisation I want to delete the users and the categories related to it, but I can't allow a category to be deleted if a user refers to it except in the case where the whole organisation is being deleted.
At present if I delete an organisation the category deletion fails if there's a user referring to it. This seems to indicate that MySQl is processing the foreign key constraints on the Categories table before the Users table.
This wouldn't be a problem if the Users in the user table were cleared before the Categories.
Is there a way to tell MySQl what order to process these FK constraints so that the tables get cleared in a specified order?
Note: I could add some code to explicitly clear the user table first, but that's fiddly within the design of the code so I don't want to go there yet.
Note also that the required security limits what I can do with the schema on-the-fly, so changing the FK constraints or disabling checking of them is not really an option. I can change the security to make a one-time change. I don't want to loosen security permanently unless there's no other way. Writing extra code as above is preferred
Here are the Create statements for the tables, edited to remove unrelated fields.
CREATE TABLE `organisation` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`orgGUID` varchar(36) NOT NULL,
`archivedFlag` tinyint(3) unsigned NOT NULL DEFAULT '0',
`orgName` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
UNIQUE KEY `org_guid_UNIQUE` (`orgGUID`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userGUID` varchar(36) NOT NULL,
`name` varchar(45) NOT NULL,
`orgGUID` varchar(36) NOT NULL,
`userType` smallint(6) DEFAULT NULL,
`PwHash` varchar(255) DEFAULT NULL,
`ethnicityGUID` varchar(36) DEFAULT NULL ,
`genderGUID` varchar(36) DEFAULT NULL ,
`yearGroupGUID` varchar(36) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
UNIQUE KEY `userGUID_UNIQUE` (`userGUID`),
KEY `fk_user_org_idx` (`orgGUID`),
KEY `fk_ethnicity_category_idx` (`ethnicityGUID`),
KEY `fk_gender_category_idx` (`genderGUID`),
CONSTRAINT `fk_ethnicity_category` FOREIGN KEY (`ethnicityGUID`) REFERENCES `categories` (`id`) ON UPDATE NO ACTION,
CONSTRAINT `fk_gender_category` FOREIGN KEY (`genderGUID`) REFERENCES `categories` (`id`) ON UPDATE NO ACTION,
CONSTRAINT `fk_user_org` FOREIGN KEY (`orgGUID`) REFERENCES `organisation` (`orgGUID`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=162 DEFAULT CHARSET=utf8;
CREATE TABLE `categories` (
`id` varchar(36) NOT NULL,
`orgGUID` varchar(36) NOT NULL,
`categoryType` varchar(20) NOT NULL,
`category` varchar(45) NOT NULL,
`priority` int(11) NOT NULL,
`analysisCode` varchar(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `fk_category_org_idx` (`orgGUID`),
CONSTRAINT `fk_category_org` FOREIGN KEY (`orgGUID`) REFERENCES `organisation` (`orgGUID`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Officially, you have no control over the order of the cascaded operations. You may be able to abuse some undocumented behaviour however:
for MySQL 5.5, the foreign keys are executed in the order they got created, so dropping and recreating the fk_category_org-constraint should work
for MySQL 5.6+, the foreign keys are executed in the lexical order of their names, so renaming fk_category_org to e.g. fk_z_category_org should work
This is undocumented and can change anytime (and might be influenced by other factors).
That being said, the proper way to do this (and anything else too complicated for on cascade) would be to add a before delete-trigger on your organisation-table that "manually" deletes the users first and then the categories afterwards. before delete-triggers are executed before on cascade (so you can decide if you want to keep those or not, although it would probably be misleading).
It is not entirely clear if that is your intented behaviour, but currently, a user can have a category that belongs to organization 1 while he is assigned to organization 2. Deleting organization 1 would then still fail. It looks a bit as if that is what you want to prevent by your design, but if you want the deletion to work in this case too, you need to use the trigger to be able to incorporate that (or manually delete it in your application), cascading will not work unless you also cascade in the category table.

Related

MySQL (Percona) Error 1452: Cannot add or update a child row for no reason

I have 2 Database Tables "users" and "tasks".
The tasks table contains two foreign keys for the createdBy_user_id and updatedBy_user_id columns both referencing users.id. If I try to insert an entry to tasks (after making sure the user referenced by the foreign keys exists) like this:
INSERT INTO tasks (createdBy_user_id,updatedBy_user_id,noOfYear,createdAt,updatedAt,status,customer_id)
VALUES (1,1,1,NOW(),NOW(),"open",1)
The query fails with Error 1452:
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`tasks`, CONSTRAINT `user_id_fk_constr` FOREIGN KEY (`createdBy_user_id`) REFERENCES `users` (`id`))
I don't know why this happens, because everything works fine if I remove the constraint. The same error does not happen for the "updatedBy_user_id" column making this such confusing.
The Tables have the following DDL:
Users table:
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`active` tinyint(1) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`email_confirmed_at` datetime DEFAULT NULL,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`first_name` varchar(50) DEFAULT NULL,
`last_name` varchar(50) DEFAULT NULL,
`job` varchar(64) DEFAULT NULL,
`position` varchar(64) DEFAULT NULL,
`specialKnowledge` text,
`tasks` text,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
Tasks table:
CREATE TABLE `tasks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`noOfYear` int(11) NOT NULL,
`year` int(11) GENERATED ALWAYS AS (right(year(`createdAt`),2)) VIRTUAL NOT NULL,
`createdBy_user_id` int(11) NOT NULL,
`updatedBy_user_id` int(11) NOT NULL,
`status` enum('open','closed') NOT NULL,
`customer_id` int(11) NOT NULL,
`projectDescription` text,
PRIMARY KEY (`id`),
UNIQUE KEY `tasks_year_unique_constr` (`year`,`noOfYear`),
KEY `user_id_fk_constr` (`createdBy_user_id`),
KEY `customer_id_fk_constr` (`customer_id`),
KEY `user_up_id_fk_constr` (`updatedBy_user_id`),
CONSTRAINT `customer_id_fk_constr` FOREIGN KEY (`customer_id`) REFERENCES `Customer` (`id`),
CONSTRAINT `user_id_fk_constr` FOREIGN KEY (`createdBy_user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_up_id_fk_constr` FOREIGN KEY (`updatedBy_user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4
As you can see the datatypes match and both tables use the InnoDB Engine.
The users table contains one entry:
select id,username from users;
+----+----------+
| id | username |
+----+----------+
| 1 | admin |
+----+----------+
1 row in set (0.00 sec)
So there is no obvious reason, why the insertion should fail. Have you an idea what's wrong with my tables? Looks like a software bug.
This is a bug. MySQL 5.7 has had some troubles with generated columns and foreign keys, and I assume this is an unfixed variant of Bug #79772 Foreign key not allowed when a virtual index exists.
In your particular case, and probably depending on your exact version, any of the following modifications seems to prevent that bug from occurring:
do not use a virtual column, but make it stored
do not create a foreign key for a column directly following the virtual column, so e.g. change the column order to year, status, createdBy_user_id, updatedBy_user_id.
do not use a unique index on the virtual column, a normal index should be fine (at least in a version where the linked bug is fixed). You want a unique constraint, so this is not an option, but the fact that this fixes your problem emphasized the "bug" nature of the problem.
The second bullet point seems to be the underlying bug: I assume that some iterator doesn't count the virtual column properly, so the foreign key that shall check createdBy_user_id seems to mix up the columns and actually checks the value of year (in this case "20") against the users table. So if you have a user with id "20" in your users table, the foreign key will actually accept this, no matter what value for createdBy_user_id you are trying to insert, see the MySQL 5.7.29 fiddle.
Unless you have a specific reason to use a virtual column, using stored is probably the sane thing to do.

Simple Relation between 2 tables

I have a problem here.
I cannot add this to my db because one table is dependent of another and vice-versa.
So I get
Cannot add foreign key constraint
on the first create table that I put
How can I add this 2 tables if they both have constraints??
-- User Roles
CREATE TABLE IF NOT EXISTS `user_roles` (
`user_role_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) NOT NULL,
`role` varchar(45) NOT NULL,
PRIMARY KEY (`user_role_id`),
UNIQUE KEY `uni_username_role` (`role`,`username`),
UNIQUE KEY `ix_auth_username` (`username`,`role`),
KEY `fk_username_idx` (`username`),
CONSTRAINT `fk_username` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
-- Users
CREATE TABLE IF NOT EXISTS `users` (
`username` varchar(45) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`hashedPassword` varchar(500) NOT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`image` mediumblob,
`team` int(11) DEFAULT NULL,
`userRole` int(11) DEFAULT NULL,
PRIMARY KEY (`username`),
KEY `fkteam_idx` (`team`),
KEY `fkrole_idx` (`userRole`),
CONSTRAINT `fkrole` FOREIGN KEY (`userRole`) REFERENCES `user_roles` (`user_role_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fkteam` FOREIGN KEY (`team`) REFERENCES `team` (`idteam`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
To do this, you'll need to use deferrable constraint checks, but unfortunately MySQL does not implement this standard SQL feature.
As far as I know, only Oracle and PostgreSQL support this feature (deferrable constraints). These constraints are checked at the end of the transaction, and not on every single row insertion. That would solve your problem.
Therefore, you have two options:
Switch to Oracle or PostgreSQL (unlikely, I guess) or,
Change your table definition to allow one of the foreign key constraints to accept null values.
In the second case, you would:
Insert in the table that allow null in the FK, getting the generated ID.
Insert in the other table using the ID. Then, get the second generated ID.
Update the null in first table using the second ID.
Commit.
That's it.

SQL refer a to b and b to a

I'm using PHPMyAdmin (so mySQL) to create this.
I have 2 tables, Album and photo. Of course, a photo can be part of an album and for that I use a foreign key from photo to album.
But now I want to be able to put a photo in the album table so I can use that photo as a cover for my album. I've tried adding a foreign key but that gives me a foreign key constraint.
Here are the tables to help understand what I mean (foto = photo). The red line indicates what I want to achieve.
I'm not that good at SQL so any help is appreciated.
SQL Album:
CREATE TABLE `Albums` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Album_Naam` varchar(255) NOT NULL,
`Aanmaakdatum` datetime NOT NULL,
`FotoID` int(11) DEFAULT '1',
PRIMARY KEY (`ID`),
KEY `FotoID` (`FotoID`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1
SQL Photo
CREATE TABLE `Foto` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Foto_Naam` varchar(255) NOT NULL,
`AlbumID` int(11) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `AlbumID` (`AlbumID`),
CONSTRAINT `Foto_ibfk_1` FOREIGN KEY (`AlbumID`) REFERENCES `Albums` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=latin1
A foreign key can be made optional by making the referencing field nullable; NULL values in the field do not violate the foreign key constraints enforced on it. This can be used to represent purely optional relations or, as in the case of this question, defer setting the value in semi-cyclic dependencies.
Note that to remove the cover photo from an album, the album will first need it's cover reference to that foto set to another photo, or to null. Similarly, to delete the album, you would need to delete it's Fotos, and so first set it's cover to null.

Database model for hierarchy Country - Organization - Suborganization - User

Good day, I need to create an organizational structure which should support following hierarchies:
Country - Organization - User
Country - Organization - Suborganization - User
I thought about having three tables. Table Country (column id is three-letter ISO 3166-1 alpha-3):
CREATE TABLE `country` (
`id` CHAR(3) NOT NULL,
`country` VARCHAR(40) NOT NULL,
PRIMARY KEY (`id`)
);
Table Organization:
CREATE TABLE `organization` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`country_id` CHAR(3) NOT NULL,
`parent_id` INT(11) NULL DEFAULT NULL,
`name` VARCHAR(40) NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_organization_1` (`parent_id` ASC),
CONSTRAINT `fk_customer_country_1`
FOREIGN KEY (`country_id`)
REFERENCES `country` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_organization_organization_1`
FOREIGN KEY (`parent_id`)
REFERENCES `organization` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
);
Table User:
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`organization_id` INT(11) NULL DEFAULT NULL,
`username` VARCHAR(45) NOT NULL,
`password` CHAR(32) NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_user_organization_1` (`organization_id` ASC),
CONSTRAINT `fk_user_organization_1`
FOREIGN KEY (`organization_id`)
REFERENCES `organization` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE);
Table Country exists to prevent an organization be assigned an unsupported country code. I don't want to use MySql's enum. Although I'm working with MySql currently I need my schema to be independent on the specific DB.
Is this good design or is there a better one? I doubt because of following questions. I can do it "somehow" but I'd like to know best practice approach.
A suborganization has a country code foreign key but country code is allready specified in the parent organization. I feel it's a kind of duplicity.
If I delete a row in table country will cascade delete fail if the DB attempts to delete suborganization before deleting it's parent organization?
Is null in foreign keys supported by all DBs? Organizations have null parent_id but parent_id is used in the self referencing forign key fk_organization_organization_1.
Country code used in foreign keys is CHAR(3). Would it be faster is it would be INT(3)?
I'd appreciate any ideas. Many thanks. Vojtech
I would make country_id nullable to avoid such duplication. Also
you can implement some application-level control to assure that at
least one of (country_id, parent_id) is not null.
Deletion must
be successfull as long as the both references defined with "ON
DELETE CASCADE" clause
All the DBs I have worked with support
nullable foreign keys: Oracle, DB2, MySQL, SQL Server, Postgre
The difference is most probably negligible

MySQL foreign key restrictions are not being saved

I'm using MySQL version 5.5.25 and trying to create a foreign key from id_parent to id on the same table.
CREATE TABLE `acl_roles` (
`id` int(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) NOT NULL,
`id_parent` int(20) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_acl_roles` (`id_parent`),
CONSTRAINT `FK_acl_roles` FOREIGN KEY (`id_parent`) REFERENCES `acl_roles` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
When I do
ALTER TABLE `acl_roles` ADD CONSTRAINT `FK_acl_roles` FOREIGN KEY (`id_parent`) REFERENCES `acl_roles` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ;
For some reason the latter executes without error yet when I execute SHOW CREATE TABLE acl_roles I get the exact same schema and the restrictions are not applied no matter how many times I run the query.
ON DELETE RESTRICT ON UPDATE RESTRICT is the default behavior for FK constraints, that is why you see no difference when viewing the schema. It is implied.