MySQL - Multiple foreign keys, that are only valid on individual check constraints
It sounds like you're trying to implement polymorphic-associations, where a given column can be an id referencing any of several parent table.
A foreign key constraint references one parent table. You can't make these conditional. This is why polymorphic associations are fundamentally not compatible with relational constraints.
If you need to reference one of several parent tables, one way to do it is to create multiple columns, each of which are nullable. Then you can use a check constraint or a trigger to make sure that exactly one of them is not null.
CREATE TABLE items (
Itemid INT NOT NULL PRIMARY KEY,
Itemtype ENUM ('Action', 'Risk') NOT NULL,
Actionid INT NULL,
Riskid INT NULL,
FOREIGN KEY (Actionid) REFERENCES actions (Actionid),
FOREIGN KEY (Riskid) REFERENCES risks (riskid),
CHECK (CASE Itemtype
WHEN 'Action' THEN Actionid IS NOT NULL AND Riskid IS NULL
WHEN 'Risk' THEN Riskid IS NOT NULL AND Actionid IS NULL
END)
);
See also:
- Possible to do a MySQL foreign key to one of two possible tables?
- Why can you not have a foreign key in a polymorphic association?
- MySQL - Conditional Foreign Key Constraints
Conditional Foreign Key to multiple tables
If you really want to do this, you can have two nullable columns one for CompanyId and one for EmployeeId that act as foreign keys.
But I would rather you to try and review the database schema design.
Constrain with conditional value check in MySQL
According to the documentation,
A UNIQUE index permits multiple NULL values for columns that can contain NULL.
So removing the not null
-constraint from Status
and adding a unique index on (ContactId,PhoneId,Status)
will work as you want it to, if you use null
instead of 0
for inactive records.
If you don't want to or cannot use null
for your Status
column, want to make sure both Status=0
and Status=null
behave identically, or e.g. want to treat Status=2
as active (and enforcing uniqueness) too, you can add a dummy column that will be calculated from Status
.
If you are using MySQL 5.7+, you can do this with a generated column:
CREATE TABLE IF NOT EXISTS `ContactPhone` (
`ContactPhoneId` int(10) unsigned NOT NULL auto_increment primary key,
`ContactId` int(11) NOT NULL,
`PhoneId` smallint(5) unsigned NOT NULL,
`Status` tinyint(1) NOT NULL DEFAULT '1',
`StatusUnq` tinyint(1) as (if(Status <> 0, 1, null)) stored null,
constraint unique (ContactId, PhoneId, StatusUnq)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
insert into ContactPhone (ContactPhoneId, ContactId, PhoneId, Status)
values (1, 1, 1, 1);
insert into ContactPhone (ContactPhoneId, ContactId, PhoneId, Status)
values (2, 1, 1, 1);
-- Duplicate key error
insert into ContactPhone (ContactPhoneId, ContactId, PhoneId, Status)
values (3, 1, 1, 0);
insert into ContactPhone (ContactPhoneId, ContactId, PhoneId, Status)
values (4, 1, 1, 0);
update ContactPhone set Status = 1 where ContactPhoneId = 4;
-- Duplicate key error
Otherwise, you can use a normal column and use triggers to calculate the value of the column, e.g.:
create trigger trbi_contactPhoneUnique before insert on ContactPhone
for each row
set new.StatusUnq = if(new.Status <> 0, 1, null);
create trigger trbu_contactPhoneUnique before update on ContactPhone
for each row
set new.StatusUnq = if(new.Status <> 0, 1, null);
You can of course switch the formula to e.g. if(new.Status <> 0, new.Status, null);
if you want to allow different values of Status
too.
How to insert data into table conditionally based on foreign key value?
Your second query seems the correct way to proceed, however looks like you can benefit from adding the institution_id
in the teacher_has_student
table, and define the foreign key with this column as well:
CREATE TABLE teacher (
teacher_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
teacher_name VARCHAR(50) NOT NULL,
institution_id INT(10) UNSIGNED,
PRIMARY KEY (teacher_id),
UNIQUE KEY teacher_institution (teacher_id, institution_id)
);
CREATE TABLE student (
student_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
student_name VARCHAR(50) NOT NULL,
institution_id INT(10) UNSIGNED,
PRIMARY KEY (student_id),
UNIQUE KEY student_institution (student_id, institution_id)
);
CREATE TABLE teacher_has_student (
teacher_id INT(10) UNSIGNED NOT NULL,
student_id INT(10) UNSIGNED NOT NULL,
institution_id INT(10) UNSIGNED NOT NULL,
UNIQUE KEY (teacher_id, student_id, institution_id),
CONSTRAINT teacher_istitution FOREIGN KEY (teacher_id, institution_id) REFERENCES teacher (teacher_id, institution_id),
CONSTRAINT student_istitution FOREIGN KEY (student_id, institution_id) REFERENCES student (student_id, institution_id)
);
INSERT INTO teacher (teacher_name, institution_id)
VALUES ("Stark", 101), ("Haydn", 102);
INSERT INTO student (student_name, institution_id)
VALUES ("Parker", 101), ("Beethoven", 102);
/* THIS ONE WORKS for both student 2 and teacher 2 have institution_id 102 */
INSERT INTO teacher_has_student (teacher_id, student_id, institution_id)
VALUES (2, 2, 102);
/* foreign key constraint fails: for none of theacher and student have institution_id 101 */
INSERT INTO teacher_has_student (teacher_id, student_id, institution_id)
VALUES (2, 2, 101);
/* foreign key constraint fails: for none of theacher have no institution_id 101 */
INSERT INTO teacher_has_student (teacher_id, student_id, institution_id)
VALUES (2, 1, 101);
/* foreign key constraint fails: for none of student have no institution_id 101 */
INSERT INTO teacher_has_student (teacher_id, student_id, institution_id)
VALUES (1, 2, 101);
How to implement a conditional foreign key restriction?
If I get what you want you need to have a primary key on A in table Y and a unique constraint on A,B in table Y.
Try this:
create table Y
(
A int not null,
B int not null,
primary key (A)
);
create unique index IX_Y_AB on Y(A, B);
create table X
(
A int not null,
B int not null,
C int null,
primary key (A, B),
foreign key (A) references Y(A),
foreign key (A, C) references Y(A, B)
);
Test:
insert into Y values (1, 2)
insert into X values (1, 1, null) -- OK
insert into X values (1, 2, 2) -- OK
insert into X values (1, 3, 3) -- Fail
Related Topics
SQL Query to Concatenate Column Values from Multiple Rows in Oracle
Left Outer Join Doesn't Return All Rows from My Left Table
Delete All Duplicate Rows Except For One in MySQL
Split Function Equivalent in T-Sql
SQL Server - Best Way to Get Identity of Inserted Row
How to Return Only the Date from a SQL Server Datetime Datatype
Finding Duplicate Values in a SQL Table
How to (Or Can I) Select Distinct on Multiple Columns
How to Use Parameters in Vba in the Different Contexts in Microsoft Access
How to Return Rows That Have the Same Column Values in MySQL
Adding an Identity to an Existing Column
Equivalent of Explode() to Work With Strings in MySQL
Case Insensitive Searching in Oracle
Split Comma Separated Column Data into Additional Columns
Column Calculated from Another Column
Return Default Result For in Value Regardless
How to Query Using Fields Inside the New Postgresql Json Datatype