Enforcing foreign key constraints based on additional column - mysql

I have the following business rules:
An account can have zero or more licenses.
Each license has an unique guid.
Each license is preassigned to a given account.
All installations belong to a given account.
All commercial_installations require a license.
No two commercial_installations can use the same license.
A commercial_installation can be deleted and the license can be then used on a new commercial_installation.
A commercial_installation can only use a license which has been assigned to the installation's account.
How can I enforce the last rule? A commercial_installation can only use a license which has been assigned to the installation's account. Or in other words, for a given guid to be stored in commercial_installations, licenses.accounts_id must equal installations.accounts_id. See sample data at the bottom.
CREATE TABLE IF NOT EXISTS accounts (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(45) NOT NULL,
otherData VARCHAR(45) NULL,
PRIMARY KEY (id))
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS licenses (
guid CHAR(36) NOT NULL,
accounts_id INT NOT NULL,
otherData VARCHAR(45) NULL,
PRIMARY KEY (guid),
INDEX fk_licenses_accounts1_idx (accounts_id ASC),
CONSTRAINT fk_licenses_accounts1
FOREIGN KEY (accounts_id)
REFERENCES accounts (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS installations (
id INT NOT NULL AUTO_INCREMENT,
accounts_id INT NOT NULL,
otherData VARCHAR(45) NULL,
PRIMARY KEY (id),
INDEX fk_installations_accounts1_idx (accounts_id ASC),
CONSTRAINT fk_installations_accounts1
FOREIGN KEY (accounts_id)
REFERENCES accounts (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS open_installations (
installations_id INT NOT NULL,
otherData VARCHAR(45) NULL,
PRIMARY KEY (installations_id),
CONSTRAINT fk_open_installations_installations1
FOREIGN KEY (installations_id)
REFERENCES installations (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS commercial_installations (
installations_id INT NOT NULL,
licenses_guid CHAR(36) NOT NULL,
otherData VARCHAR(45) NULL,
PRIMARY KEY (installations_id),
UNIQUE INDEX fk_commercial_installations_licenses1_idx (licenses_guid ASC),
CONSTRAINT fk_commercial_installations_installations1
FOREIGN KEY (installations_id)
REFERENCES installations (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT fk_commercial_installations_licenses1
FOREIGN KEY (licenses_guid)
REFERENCES licenses (guid)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
Given the following sample data:
b5060518-f87e-4acc-82c8-adb5750685a9 and d6f23460-0d77-400e-ae96-13f436e40245 can only exist in commercial_installations where commercial_installations.id is 1 or 2.
Similarly, 8739ef62-7fff-4913-81de-3d00e8f50ecb for 3.
Similarly, 36cc0787-5cb9-4c3a-b79d-1dcfb83d2794 for 4.
accounts
+----+-----------+
| id | name |
+----+-----------+
| 1 | Account 1 |
| 2 | Account 2 |
| 3 | Account 3 |
| 4 | Account 4 |
+----+-----------+
licenses
+--------------------------------------+-------------+
| guid | accounts_id |
+--------------------------------------+-------------+
| b5060518-f87e-4acc-82c8-adb5750685a9 | 1 |
| d6f23460-0d77-400e-ae96-13f436e40245 | 1 |
| 36cc0787-5cb9-4c3a-b79d-1dcfb83d2794 | 2 |
| 8739ef62-7fff-4913-81de-3d00e8f50ecb | 3 |
+--------------------------------------+-------------+
installations
+----+-------------+
| id | accounts_id |
+----+-------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 3 |
| 4 | 2 |
+----+-------------+

Of course you can enforce it in the logic of your application, but I guess you want to do it at the database level.
The only option I see here is to add triggers for INSERT, UPDATE, and DELETE operations. Normal foreign keys will not do this for you.
And I like this question, since I've had this problem before and didn't really take the time of formally asking about it here in SO.

Simply propagate account_ID to commercial_installations. It also takes few more unique constraints, marked as SK (superkey, superset of a key) to be used as targets for FKs in the model.
accounts {account_ID, account_name}
PK {account_ID}
licenses {license_ID, account_ID}
PK {license_ID}
SK {license_ID, account_ID}
FK {account_ID} REFERENCES accounts {account_ID}
installations {installation_ID, account_ID, installation_TYPE}
PK {installation_ID}
SK {installation_ID, account_ID}
FK {account_ID} REFERENCES accounts {account_ID}
open_installations {installation_ID}
PK {installation_ID}
FK {installation_ID} REFERENCES installations {installation_ID}
commercial_installations {installation_ID, account_ID, license_ID}
PK {installation_ID}
AK {license_ID}
FK1 {installation_ID, account_ID}
REFERENCES installations {installation_ID, account_ID}
FK2 {license_ID, account_ID}
REFERENCES licenses {license_ID, account_ID}
Notes:
All attributes (columns) NOT NULL
PK = Primary Key
SK = Superkey (Unique)
AK = Alternate Key (Unique)
FK = Foreign Key

Related

Add or Add Constraint for Foreign Key Constraints

The term 'Foreign Key Constraint' is often tossed around. And I just want to clarify it's exact meaning. We have an employees table and a branches table. The employees table was created first but it should have a foreign key constraint on branch_id, which references the primary (surrogate) key of id on branches table:
CREATE TABLE employees (
id INT AUTO_INCREMENT,
first_name VARCHAR(40),
last_name VARCHAR(40),
birth_day DATE,
sex BOOLEAN,
salary INT,
supervisor_id INT,
branch_id INT,
PRIMARY KEY(id)
)
CREATE TABLE branches (
id INT AUTO_INCREMENT,
branch_name VARCHAR(40),
manager_id INT,
manager_start_date DATE,
PRIMARY KEY(id),
FOREIGN KEY(manager_id) REFERENCES employees(id) ON DELETE SET NULL
)
And now we add the Foreign Key Constraints:
ALTER TABLE employees
ADD FOREIGN KEY(branch_id)
REFERENCES branches(id)
ON DELETE SET NULL;
ALTER TABLE employees
ADD FOREIGN KEY(supervisor_id)
REFERENCES employees(id)
ON DELETE SET NULL;
Notice here I use Add Foreign Key and not Add Constraint constraint_name. Here would be an example of using ADD CONSTRAINT:
ALTER TABLE users
ADD CONSTRAINT check_users_age
CHECK (age>=18 AND city='Philadelphia');
Is ADD FOREIGN KEY and ADD CONSTRAINT constraint_name synonymous? Does ADD FOREIGN KEY, in effect, add a constraint without a name? And if ADD FOREIGN KEY does add a name, how can I find it in mysql?
Explicitly naming the constraint, i.e. use CONSTRAINT <name> FOREIGN KEY ... is optional. If you don't do it, i.e. just go with FOREIGN KEY ... the system generates a name.
The effects a constraint have are independent of its name. So both, explicitly naming the constraint and not doings so are synonym-ish -- that is except regarding the name.
You can query the constraints from the catalog, e.g. from information_schema.key_column_usage.
Consider the following example:
CREATE TABLE a
(id integer,
PRIMARY KEY (id));
CREATE TABLE b
(id integer,
PRIMARY KEY (id));
CREATE TABLE x
(a integer,
b integer,
FOREIGN KEY (a)
REFERENCES a
(id),
CONSTRAINT fancy_name
FOREIGN KEY (b)
REFERENCES b
(id));
SELECT table_name,
column_name,
constraint_name,
referenced_table_name,
referenced_column_name
FROM information_schema.key_column_usage
WHERE table_schema = database()
AND table_name = 'x';
This will result in something like:
| table_name | column_name | constraint_name | referenced_table_name | referenced_column_name |
| ---------- | ----------- | --------------- | --------------------- | ---------------------- |
| x | b | fancy_name | b | id |
| x | a | x_ibfk_1 | a | id |
DB Fiddle
You can see, the system picked a name for the constraint.

Unable to insert rows and getting foreign key constraint fails error

I have the following table:
CREATE TABLE IF NOT EXISTS profile_claim_ruling_tasks (
profile_claim_ruling_task_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
account_id BIGINT UNSIGNED NOT NULL,
profile_id BIGINT UNSIGNED NOT NULL,
admin_task_status_id BIGINT UNSIGNED NOT NULL,
profile_claim_ruling_task_ref_id VARCHAR(36) NOT NULL,
profile_claim_ruling_task_requested_at DATETIME NOT NULL,
CONSTRAINT pk_profile_claim_ruling_tasks PRIMARY KEY (profile_claim_ruling_task_id),
CONSTRAINT fk_profile_claim_ruling_task_account_id FOREIGN KEY (account_id) REFERENCES accounts (account_id),
CONSTRAINT fk_profile_claim_ruling_task_profile_id FOREIGN KEY (profile_id) REFERENCES accounts (profile_id),
CONSTRAINT fk_profile_claim_ruling_task_admin_task_status_id FOREIGN KEY (admin_task_status_id) REFERENCES admin_task_statuses (admin_task_status_id),
INDEX idx_profile_claim_ruling_tasks_admin_task_status_id(admin_task_status_id),
CONSTRAINT uc_profile_claim_ruling_tasks_profile_claim_ruling_tasks_ref_id UNIQUE (profile_claim_ruling_task_ref_id)
);
When I do a SELECT * on accounts:
+------------+--------------------------------------+------------+
| account_id | account_ref_id | profile_id |
+------------+--------------------------------------+------------+
| 1 | 521ef2cb-01f9-49f3-a214-42e1514d7dc2 | 1 |
+------------+--------------------------------------+------------+
And when I do a SELECT * on profiles:
+------------+--------------------------------------+
| profile_id | profile_ref_id |
+------------+--------------------------------------+
| 2 | 1d8caa66-e080-4cc6-88ff-a063e576bafa |
| 3 | 619a7ec6-813a-41f0-a1f9-16289893df5d |
| 4 | c50ceb2f-49f0-4319-b115-0a1454593c46 |
| 1 | d6369f9b-b66a-468c-86f9-a7e0abc75b65 |
+------------+--------------------------------------+
So far, so good! But when I run the following insert:
INSERT INTO profile_claim_ruling_tasks (
account_id,
profile_id,
admin_task_status_id,
profile_claim_ruling_task_ref_id,
profile_claim_ruling_task_requested_at
) VALUES (
1,
4,
1,
'4bed7334-e17b-462f-a7e6-454c3b2f5235',
'2018-01-29 13:12:57'
);
I get the following error:
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`myapp_db`.`profile_claim_ruling_tasks`, CONSTRAINT `fk_profile_claim_ruling_task_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `accounts` (`profile_id`))
What's going on here?!
FOREIGN KEY (profile_id) REFERENCES accounts (profile_id)
You are trying to insert data with profile_id=4 in table profile_claim_ruling_tasks, which is referring to accounts (profile_id).
But, you don't have profile_id=4 in accounts table. You need to populate accounts table first to resolve this issue.
You appear to have an error in your first SHOW CREATE TABLE,
CONSTRAINT fk_profile_claim_ruling_task_profile_id FOREIGN KEY (profile_id) REFERENCES accounts (profile_id),
should be
CONSTRAINT fk_profile_claim_ruling_task_profile_id FOREIGN KEY (profile_id) REFERENCES profiles (profile_id),
IMO.

Referencing table with generated primary keys

I am trying to increase the constraint of my MySQL database schema adding foreign key constraint to each table.
Table 1: USERS
+---------+----------+-------------
| id | username | Other fields
+---------+----------+-------------
| 1 | John |
| 2 | Mark |
+---------+----------+-------------
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
username` VARCHAR(50) NOT NULL
PRIMARY KEY (id)
Table 2: DISKS (This has a one to many relationship with USERS)
+---------+----------+-----------+-------------
| id | id_user | disk_name | Other fields
+---------+----------+-----------+-------------
| 1 | 1 | disk A |
| 2 | 2 | disk B |
+---------+----------+-----------+-------------
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
id_user INT(11) NOT NULL,
PRIMARY KEY (id,id_user)
INDEX fk_disks_idx (id ASC)
CONSTRAINT fk_disks
FOREIGN KEY (id)
REFERENCES database.USERS (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
Table 3: FILES (This has a one to many relationship with DISKS)
+---------+----------+----------+-----------+-------------
| id | id_disk | id_user | file_name | Other fields
+---------+----------+----------+-----------+-------------
| 1 | 1 | 1 | |
| 2 | 2 | 2 | |
+---------+----------+----------+-----------+-------------
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
id_user INT(11) NOT NULL
id_disk INT(11) NOT NULL
PRIMARY KEY (id,id_disk,id_user )
INDEX fk_files_idx (id ASC, id_user ASC)
CONSTRAINT fk_files
FOREIGN KEY (id_disk, id_user, id_user)
REFERENCES database.DISKS (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
Table 2: FILES_ON_NAS (This has a one to one relationship with FILES)
+-------+----------+----------+----------+-----------+-------------
| id | id_files | id_user | id_disk | file_name | Other fields
+-------+----------+----------+----------+-----------+-------------
| 1 | 1 | 1 | 1 | |
| 2 | 1 | 2 | 2 | |
+-------+----------+----------+----------+-----------+-------------
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
id_files INT(11) NOT NULL,
id_user INT(11) NOT NULL,
id_disk INT(11) NOT NULL,
PRIMARY KEY (id,id_files,id_user,id_disk )
INDEX fk_files_on_nas_idx (id ASC)
CONSTRAINT fk_files_on_nas
FOREIGN KEY (id_files,id_user,id_disk)
REFERENCES database.FILES (id,id_user, id_disk)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
Question:
As you can see the more I reference table in cascade the more primary keys I get. How can I design the database to avoid the replication of primary keys and therefore data duplication as well? Should I delete the auto incremented key for each table? Is it a good practice?
Thanks
The ID of the disk is sufficient to uniquely identify a disk. So there's no reason to include the ID of the user into the disk's primary key. It would even be an extremely bad idea, because that means that if a disk's user changes, you would need to modify the primary key.
Same for a file. A file ID uniquely identifies a file. So there's no reason to add the disk ID into the primary key of a file.
I would strongly advise against deleting the auto-incremented keys.
However you don't need to make a new primary key everytime :
If you want that multiple users share one disk, just put a foreign key of id_disk in USERS
If you want that one user can have multiple disks, then put a foreign key of id_user in DISKS instead.
Only use primary keys like that when you face a Many-to-Many relationship. In this case, you need to create a new table to join both tables; with the primary keys of both tables as foreign keys making a coupled primary key like you did.
You might want to read a little about Database Normalization. In your case, i would make the surrogate key id the only primary key in the tables. Something like:
create table users (
id integer not null auto_increment,
username varchar(50),
...,
primary key (id)
);
create table disks (
id integer not null auto_increment,
user_id integer,
diskname varchar(50),
....,
primary key (id),
foreign key (user_id) references users (id)
);
For files you are going to have to answer the question: does file ownership depend on the file directly, or transitively on the disk ownership, or are the ownerships independent? A file owned by John on a disk owned by Jack? Seems ok to me, but your domain might have different rules. In that case, drop the user_id from the files table (otherwise your database won't be in Third normal form).
create table files (
id integer not null auto_increment,
disk_id integer,
user_id integer, -- you have to decide whether this is necessary
filename varchar(50),
....,
primary key (id),
foreign key (disk_id) references disks (id),
foreign key (user_id) references users (id)
);

PHP MySQL Delete parent and child rows

I have 1 MySQL Table. It looks like this:
+---------+-------------+--------+
| item_id | parent_id | Name |
+---------+-------------+--------+
| 1 | 0 | Home |
+---------+-------------+--------+
| 2 | 1 | Sub |
+---------+-------------+--------+
| 3 | 2 | SubSub |
+---------+-------------+--------+
If I DELETE item_id 1, I want to delete the rest of the sub also but how can I do it?
I have tried the Foreign Key but it works only if you have 2 tables??
I hope someone can help me in MySQL maybe PHP?
You can, most definitely, use self-referencing foreign keys with MySQL (you don't need multiple tables). However, for any kind of foreign key support, you need to use the InnoDB engine. And my guess is, that you are using the MyISAM engine.
With InnoDB you could create a table, similar to what you have already, including the self-referencing foreign key, like this:
CREATE TABLE `yourTable` (
`item_id` int(10) unsigned NOT NULL auto_increment,
`parent_id` int(10) unsigned default NULL,
`Name` varchar(50) NOT NULL,
PRIMARY KEY (`item_id`),
KEY `FK_parent_id` (`parent_id`),
CONSTRAINT `FK_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `yourTable` (`item_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Then, when you issue a DELETE statement, like:
DELETE FROM `yourTable` WHERE `item_id` = 1;
... it would delete each 'child' row, that has a parent_id of 1 as well. If any of those 'child' rows have children of their own, they'd be deleted too, etc. (that's what the ON DELETE CASCADE means).
Easier actually than thought:
DELETE FROM table WHERE id = # OR parent_id = #; //where # is the same in both places.
Example:
DELETE FROM table WHERE id = 1 OR parent_id = 1;

mysql foreign key concept

CREATE TABLE parent (id INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE child (id INT, parent_id INT,
INDEX par_ind (parent_id),
FOREIGN KEY (parent_id) REFERENCES parent(id)
ON DELETE CASCADE
) ENGINE=INNODB;
I dont understand the meaning of putting ENGINE = INNODB here, and why do we use ON DELETE CASCADE?
engine=innodb will ensure you get foreign key support. The default MyISAM engine doesn't support foreign keys. On delete cascade will remove the child row if the referenced row in the parent table is removed.
MySQL is the DB engine. It can use multiple storage engines. MyISAM is the default storage engine for MySQL and it does not support foreign keys. InnoDB is another storage engine that does support foreign keys. You must specify ENGINE=InnoDB because MySQL will use MyISAM by default.
ON DELETE CASCADE will delete all rows in a table that have a foreign key that references a key that is deleted. I think it is dangerous and defeats a lot of the purpose of foreign key restriction, so I would avoid using it, but this is just my personal opinion.
Say you have:
+-------+-------+
| ordID | proID |
+-------+-------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 2 |
+-------+-------+
And on OrdersItems it has FOREIGN KEY (proID) REFERENCES Products (proID) ON DELETE CASCADE.
Then if someone runs
DELETE FROM Products WHERE proID = 2
Then rows with ordID 4 and 5 will also be deleted (it cascades).