I'm building an app that can assign guests to an existing user. Those guests can see some information about the users they are associated with.
It's N:M:
Users can have multiple guests associated
Guests can be associated to multiple users
Under the hood they are stored in the same table, the only difference between them is the role they have.
(My real table have more fields like password etc... but those are not relevant to this question)
I need to create a table like this:
The thing is that userId and guestId are referenced to the same column (id) of Users table.
Is this viable?
A user can only be either a guest or a host. This is why you have a role column in your user table.
Your guests table contains two users in a row, and as I mentioned in the request comments, I'd call them host_user_id and guest_user_id to make their roles obvious and indicate they are both user IDs.
The table design is okay for that. The only downside is that you could mistakenly make a host a guest and a guest a host, because the guests table is not aware of which is which, both are mere user IDs. If you want the database to guarantee consistency in this aspect, this gets a tad more complicated. Here is a design that deals with this problem:
create table users
(
id not null int,
full_name not null varchar(100),
role not null varchar(10),
created_at not null timestamp,
updated_at timestamp,
constraint pk_users primary key (id),
constraint chk_users_role check (role in 'host', 'guest'),
constraint unq_users_role unique (id, role)
);
create table guests
(
host_user_id not null int,
host_role not null varchar(10),
guest_user_id not null int,
guest_role not null varchar(10),
constraint pk_guests primary key (host_user_id, guest_user_id),
constraint fk_guests_host foreign key (host_user_id, host_role) references users (id, role),
constraint fk_guests_guest foreign key (guest_user_id, guest_role) references users (id, role),
constraint chk_guests_host_role check (host_role = 'host'),
constraint chk_guests_guest_role check (guest_role = 'guest')
);
This doesn't look as nice as before, as host_role being constantly 'host' and guest_role being constantly 'guest' looks redundant to us human readers, but it guarantess that a host_user_id really refers to a host user and a guest_user_id to a guest user.
Is this what you want?
create table users (
userId int generated always as identity primary key,
fullname varchar(255),
. . .
);
create table guests (
guestId int generated always as identity primary key,
fullname varchar(255),
. .
);
create table userGuests (
userGuestId int generated always as identity primary key,
userId int references users(userId),
guestId int references guests(guestId
);
You can then load these as:
insert into guests (fullname, . . .)
select distinct fullname
from old_users
where role = 'guest';
insert into users (fullname, . . .)
select distinct fullname
from old_users
where role = 'user';
insert into userGuests (userId, guestId)
select u.userId, g.userId
from old_users ou join
old_users og
on ou.id = og.id join
users u
on ou.fullname = u.fullname join
guests g
on og.fullname = g.fullname
where ou.role = 'user' and og.role = 'guest';
Note: This all uses generic SQL syntax, but it should give the idea.
I wouldn't know why it wouldn't work. I would change the name of the guests table though just to make it more clear that it doesn't hold the guests, but the guests relations.
Related
I have 3 table Student,Teacher,User.
Student:
CREATE TABLE Student( id INT NOT NULL PRIMARY KEY,name VARCHAR(50) NOT NULL);
INSERT INTO [dbo].[Student]([id],[name]) VALUES(4,'Ram'),(5,'Raman');
Teacher:
CREATE TABLE Teacher( id INT NOT NULL PRIMARY KEY,name VARCHAR(50) NOT NULL);
INSERT INTO [dbo].[Student]([id],[name]) VALUES(1,'Raj'),(2,'Rahul');
User:
CREATE TABLE [dbo].[User](
id INT NOT NULL PRIMARY KEY,
user_id INT NOT NULL,
user_type CHAR(1) NOT NULL,
user_name VARCHAR(10) NOT NULL,
user_password VARCHAR(255) NOT NULL,
CONSTRAINT FOREIGN KEY (user_id) REFERENCES Student (id),
CONSTRAINT FOREIGN KEY (user_id) REFERENCES Teacher (id) );
Now I try to INSERT in User table with below query
INSERT INTO [dbo].[User] ([id] ,[user_id] ,[user_type],[user_name] ,[user_password]) VALUES (1 ,1,'S','Raj_001','********')
It gives me error for violation of foreign key due to
value of user_id is available in Teacher and not in Student
So my question is: How can I achieve that a value of user_id is present in one of those table and data should be inserted in User table.
Your table structure is flawed. A foreign key tells the database that there is definitely one and only one row on one side of the relationship. You can't have a partial match, it's all or nothing. This is before considering how you would ensure that you don't end up with the same id in both the teacher and student table.
It would be better to have two columns in your user table, one for teacher id and one for student id. In fact going further given the only extra data in both student and teacher tables is their name why not just eliminate both and store the name in the user table?
Another option to consider is that your foreign key is pointed in the wrong direction. Perhaps a better approach is reversing it to ensure each student and teacher is a user rather than that a user is either a student or a teacher.
First of all get rid of those key words from table name like [User],user_id etc.
It really is problematic and irritating.
Secondly why 2 key in [User] table,id, user_id ? It is not require.
I will keep only id or user_id.
Thirdly, knowing the real table structure or even purpose of each table help in better data modeling.
From [User] table what it appear is that id and user_type are composite primary key.
It should be. If this is true then you can't define FK constraint, as user_type is not available in either Teacher table and Student Table.
And what is appear that ,for example first data is inserted in Student or Teacher then data is inserted in User table in same Transaction.
So in all above scenario, Instead of Trigger is ideal scenario in this condition.
My script is just demo,
Create Proc spStudentInsert
as
set nocount on
set xact_abort on
begin try
begin tran
--bulk insert or single insert ,no problem
insert into Student
insert into [User]
if (##Trancount>0)
commit
end try
begin catch
if (##Trancount>0)
rollback
end catch
CREATE TRIGGER INSTEADOF_TR_I_User ON [user]
INSTEAD OF INSERT
AS
BEGIN
DECLARE #Flag BIT = 1
IF NOT EXISTS (
SELECT 1
FROM Student S
INNER JOIN inserted i ON i.id = S.id
)
SET #Flag = 0
ELSE IF NOT EXISTS (
SELECT 1
FROM Teacher T
INNER JOIN inserted i ON i.id = T.id
)
AND #Flag = 1
SET #Flag = 0
IF (#Flag = 0)
BEGIN
RAISERROR (
N'Invalid user'
,16
,1
)
RETURN
END
END
In case I am wrong about id, user_type composite PK then you can do other way,
PK of User id is FK in Student table as well as Teacher table.
Also , id are PK in their respective table.
So first you insert in User table then you insert in Student or Teacher table.
So design in this case will be,
CREATE TABLE [dbo].[User](
id INT NOT NULL ,
user_type CHAR(1) NOT NULL,
user_name VARCHAR(10) NOT NULL,
user_password VARCHAR(255) NOT NULL,
CONSTRAINT [PK_user] PRIMARY KEY (id)
)
INSERT INTO [dbo].[User] ([id] ,[user_type],[user_name] ,[user_password])
VALUES (1 ,1,'S','Ram_001','********')
--drop table [User]
--alter table [user]
-- drop constraint PK_user
CREATE TABLE Student( id INT NOT NULL PRIMARY KEY,name VARCHAR(50) NOT NULL);
ALTER TABLE Student
add CONSTRAINT FK_StudentUser FOREIGN KEY (id) REFERENCES [User] (id);
INSERT INTO [dbo].[Student]([id],[name]) VALUES(1,'Ram'),(5,'Raman');
--select * from [Student]
CREATE TABLE Teacher( id INT NOT NULL PRIMARY KEY,name VARCHAR(50) NOT NULL);
ALTER TABLE Teacher
add CONSTRAINT FK_TeacherUser FOREIGN KEY (id) REFERENCES [User] (id);
INSERT INTO [dbo].Teacher([id],[name]) VALUES(1,'Raj'),(2,'Rahul');
So what it appear from your question, I will create Instead of Trigger and go with that model.
There are two ways to do this without re-doing your table schema
Create a 4th table that contains the union of ID from Student and Teacher. Presumably, you would insert to that table whenever you insert into Student and Teacher, and then have the constraint act against that table.
Create a custom function based constraint rather than a foreign key which looks up against a union of both the student and teacher tables.
Neither of these are great/clean solutions, and as others have noted, you probably are dealing with the fact that the schema isn't ideal.
Still, if you're just modifying an existing system (and I assume this is a simplified version of what you're actually dealing with), then one of the two solutions I mentioned id easier than redoing the schema.
Your foreign key definition has some logical problems. It forces the user_id to exists in both tables. The solution here is depended on the business needs and real data.
You can create a Person table with 1-1 relation to the student and the Teacher tables and then use the Person.Id column in the foreign key definition. This solution assumes that the students' and teachers' data may change differently.
As another way (which is explained in other answers), If your student and teachers' data is similar, you can combine both tables, and difference data by one added "Type" column.
SO you want to tell the system that your User must be in one of your tables .
it's not possible in databases logic but you can write a script that have a condition (IF exist) then insert you user data
notice : you have to remove your foreign keys .
its a wrong logic !
you are telling your system that your user is a student and a teacher to !
that is absolutely wrong .
I feel like there were some excellent responses in this thread, but I'm going to take a stab at giving you a different direction. I'll try to be clear on why, and try to acknowledge your situation as I do so.
Student/Teacher Data is Often Messy
As someone with experience normalizing data sets in higher education, the issue you've run into resonated with me. Educational users could be in all three categories (Student, Teacher, and User) or just one of them, depending on the how and why the category was linked. Worse, they can enter from multiple directions and end up with multiple unlinked accounts. More mature institutions and tools have protections against this, but I still see user-created databases and ten year old 'it was temporary' solutions that cause me existential pain.
The Main Stumbling Block
Any database with tables that independently define who is a user based on different criteria have a potential point of failure.
Foreign keys was the right direction to be thinking in for this problem. You want these tables to connect and you want them to stay consistent with one another, regardless of which side of the data gets altered. We just need to add a little extra.
One Table To Rule Them All
Before I go further, I want to say that it is possible to get all of the fields you're tracking into a single table, but having multiple tables with distinct purposes is an easy way to protect against changes later.
The foreign key table must inherit the key from another table, but people often say foreign keys can't be primary keys as well. Why?
Foreign keys are not automatically unique keys in the tables they're in. If there can be multiple fields tied to that same key, the table ends up worthless.
We fix that with the Unique constraint. Applied to a foreign key field, Unique essentially makes it act as a primary key would.
Sample Method
Below is an alternative design for what you seemed to be after, creating a master list of IDs that can link across all tables. I tossed in a few minor tracking fields that can be useful for debugging.
/*Create Tables*/
CREATE TABLE ID(
USER_ID int NOT NULL PRIMARY KEY AUTO_INCREMENT,
USER_CREATED timestamp
);
CREATE TABLE USER(
USER_ID int NOT NULL UNIQUE FOREIGN KEY REFERENCES ID(USER_ID),
USER_LOGIN VARCHAR(10) NOT NULL UNIQUE,
USER_PASSWORD VARCHAR(255) NOT NULL,
USER_NAME VARCHAR(50) NOT NULL
);
CREATE TABLE PERMISSIONS(
USER_ID int NOT NULL UNIQUE FOREIGN KEY REFERENCES ID(USER_ID),
STUDENT CHAR(1),
TEACHER CHAR(1)
);
This creates a flag for student and teacher that could both be true or both be false. If you want the code to force them into only one or the other, you can still have the permissions table do a USER_TYPE field instead. I suggest a null or neither value being possible in either case if you plan to use this for any length of time. Best of luck.
I'm a newbie to SQL and I'm jumping in head first trying to learn as much as possible as I'm coding, which is difficult as I'm designing the database I'll have to live with for a while so I want to make sure I do it right. I learned the basics of the Many-to-many bridge tables, but what if the two fields are the same type? Let's say a social network with thousands of users, and how would you create a table to keep track of who is friends with who? What if there is additional data about each relationship, say... "date friended" for example. Knowing that there will be queries like "show all friends of userX friended between dateY and dateZ". My database would have several situations like this and I can't figure out an efficient way to do it. Since it's coming up a lot for me, I figure others have figured out the best way to design the tables, right?
Create a User table then a Relationships table where you store the id of the two friend and any kind of information about their relationship.
SQL diagram
MySQL code
CREATE TABLE `Users` (
`id` TINYINT NOT NULL AUTO_INCREMENT DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `Relationships` (
`id` TINYINT NOT NULL AUTO_INCREMENT DEFAULT NULL,
`userid` TINYINT NULL DEFAULT NULL,
`friendid` TINYINT NULL DEFAULT NULL,
`friended` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
ALTER TABLE `Relationships` ADD FOREIGN KEY (userid) REFERENCES `Users` (`id`);
ALTER TABLE `Relationships` ADD FOREIGN KEY (friendid) REFERENCES `Users` (`id`);
SQL selection
After you fill up the tables with data, you can create your SQL SELECT query to get all of your friends. Your friends are those whose id is in one side side while your id is in the other side. You check both sides for your id so you don't need to store relationships twice. Also you have to exclude your id, because you can't be your own friend (in a normal, healthy world).
SELECT *
FROM Users u
INNER JOIN Relationships r ON u.id = r.userid
INNER JOIN Relationships r ON u.id = r.friendid
WHERE
(r.userid = $myid OR r.friendid = $myid)
AND r.friended >= $startdate
AND r.friended <= $enddate
AND u.id != $myid;
Where $myid, $startdate and $enddate can be PHP variables so in double quotes you can pass this code directly to your database.
The Model
A model stretching over three tables would be an option. You'd have the obvious table user with all the user's specifics (Name, Date of Birth, ...).
CREATE TABLE `user` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45),
`dob` DATE,
PRIMARY KEY (`id`)
);
Second, a table connection of the user could contain privileges granted to a connection (can this connection see my photo album), and importantly, would refer to a table friendship. We need the table connection in between because one user can be connected into many friendships.
CREATE TABLE `connection` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`friendship_id` INT NOT NULL,
`privilege_mask` TINYINT,
PRIMARY KEY (`id`)
);
friendship in turn could include shared details like when this friendship was established. Users that are connected to the same friendship are friends.
CREATE TABLE `friendship` (
`id` INT NOT NULL AUTO_INCREMENT,
`met_first_time` DATE,
PRIMARY KEY (`id`)
);
This way would be a more realistic model than the other solutions posted so far in that it avoids directionality (unreciprocated friendship - which shouldn't exist!), but it would be a little more work to implement.
Query Friends
An example that should query the names of your friends, may be (although not tested):
SELECT B.name FROM user A
INNER JOIN connection conn_A ON conn_A.user_id = A.id
INNER JOIN connection conn_B ON conn_A.friendship_id = conn_B.friendship_id
INNER JOIN user B ON conn_B.user_id = B.id
INNER JOIN friendship ON friendship.id = conn_A.friendship_id
WHERE A.name = 'Dan' AND A.id <> B.id AND
friendship.met_first_time BETWEEN '2013-4-1' AND '2013-6-30';
You may notice that if you don't care about the date when you made friends, you don't need to JOIN to the friendship table because connections already share friendship_id keys. The essence of any such query would be the JOIN between conn_A and conn_B on conn_A.friendship_id = conn_B.friendship_id.
A single linking table suffices, like so:
People( PersonId bigint, Name nvarchar, etc )
Friends( FromPersonId bigint, ToPersonId bigint, DateAdded datetime )
Example queries:
Who is friends with me? (i.e. people who have added me as a friend, but not necessarily reciprocated)
SELECT
People.Name
FROM
Friends
INNER JOIN People ON Friends.FromPersonId = People.PersonId
WHERE
Friends.ToPersonId = #myPersonId
Who added me between two dates?
SELECT
People.Name
FROM
Friends
INNER JOIN People ON Friends.FromPersonId = People.PersonId
WHERE
Friends.ToPersonId = #myPersonId
AND
Friends.DateAdded >= #startDate
AND
Friends.DateAdded <= #endDate
You should use the ID's of the two users as PRIMARY KEY in a relationship table (the relation would be unique even if not bi-directional). Something like that
CREATE TABLE Users ( id int(9) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id) );
CREATE TABLE Relationships ( id1 int(9) NOT NULL, id2 int(9)
NOT NULL, friended TIMESTAMP NOT NULL, PRIMARY KEY (id1,
id2) );
Please note:
id1 and id2 reference Users table (the code above was very simplified)
even if the relationship is not "bi-directional" you can think
that if you have id1 - id2 it seems that id1 user add id2 user as
a friend but not necessarily the opposite - THEN id2 user can add
id1 user as friend - in the table of relationships you can have
these possible combinations:
id1-id2, (only 1 add 2 as a friend)
id2-id1, (only2 add 1 as a friend)
id1-id2 AND id2-id1 (both - it means 2 lines in relationships table, and both are MUTUAL
friends)
I'm working on an application which has different types of users i.e. students, tutors, and administrators. However, I initially set up my database to have only two tables: Users (that holds all the login information, common to all types) and Profiles (that holds all other information; though each column applies to a certain type of user).
I was thinking maybe I should have Users, and 3 separate tables for each type i.e. Students, Tutors, and Administrators. However, how do I link those three tables with Users table? I'm sure there would be a bridge table but I'm not too sure how to do that.
This should be a solid starting point for you. Define the tables which it sounds like you are ok with, then apply the relationships using the ALTER TABLE command. See below.
CREATE TABLE Users
(
ID INT IDENTITY (1,1),
PRIMARY KEY (ID),
UserName VARCHAR(20),
Password VARCHAR(20)
)
CREATE TABLE Students
(
ID INT IDENTITY (1,1),
PRIMARY KEY (ID),
UserID INT --foreign key to Users.ID column
--Other columns
)
CREATE TABLE Tutors
(
ID INT IDENTITY (1,1),
PRIMARY KEY (ID),
UserID INT --foreign key to Users.ID column
--Other columns
)
CREATE TABLE Administrators
(
ID INT IDENTITY (1,1),
PRIMARY KEY (ID),
UserID INT --foreign key to Users.ID column
--Other columns
)
--Apply foreign key relationships
ALTER TABLE Students
ADD FOREIGN KEY (UserID)
REFERENCES Users(ID)
ALTER TABLE Tutors
ADD FOREIGN KEY (UserID)
REFERENCES Users(ID)
ALTER TABLE Administrators
ADD FOREIGN KEY (UserID)
REFERENCES Users(ID)
You would add the primary key of the Users table as a foreign key to each of the 3 seperate tables.
Hope this helps.
USER_TYPE table, containing userType and userTypeID columns.
USER table, containing userID column plus login information, plus a userTypeID column, linking to USER_TYPE table.
USER_STUDENT table, containing student related columns, plus a userID column, linking to the USER table.
USER_TUTOR table, containing tutor related columns, plus a userID column, linking to the USER table.
USER_ADMINISTRATOR table, containing administrator related columns, plus a userID column, linking to the USER table.
You can JOIN 1, 2 and {3 or 4 or 5} tables as shown below, and in similar other ways:
SELECT U.*, S.*
FROM USER as U
INNER JOIN USER_TYPE AS UT ON UT.userTypeID = U.userTypeID
INNER JOIN USER_STUDENT AS US ON US.userID = U.userID
WHERE UT.userType = 'STUDENT'
Another approach is to introduce a "role" table that defines the type of roles/profiles in your system (student, admin, tutor, ...), then add a mapping table "user_role" which maps users to roles (ie John is both a student and a teacher implies two records in the "user_role" table. Role-specific information for a user can be in the tables shown as "detail" tables below.
USER
----
user_id
username
password
ROLE
----
role_id
role_name
USER_ROLE
----------
user_role_id
user_id
role_id
from_date
to_date
STUDENT_DETAIL
--------------
user_role_id
student_number
TUTOR_DETAIL
------------
user_role_id
sin
i am actually reading Doctrine Reference: One to Many, Unidirectional with Join table. but this will probably be more of a SQL quesiton. basically, this is supposed to model a one to many, unidirectional relationship. i guess from the PHP code (in that link), its such that 1 user have many phonenumbers.
the question is from the SQL, it seems like 1 user can have many phonenumbers. and 1 phonenumber can only belong to 1 user. am i right?
CREATE TABLE User (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY(id)
) ENGINE = InnoDB;
CREATE TABLE users_phonenumbers (
user_id INT NOT NULL,
phonenumber_id INT NOT NULL,
UNIQUE INDEX users_phonenumbers_phonenumber_id_uniq (phonenumber_id),
PRIMARY KEY(user_id,
phonenumber_id)
) ENGINE = InnoDB;
CREATE TABLE Phonenumber (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY(id)
) ENGINE = InnoDB;
ALTER TABLE users_phonenumbers ADD FOREIGN KEY (user_id) REFERENCES User(id);
ALTER TABLE users_phonenumbers ADD FOREIGN KEY (phonenumber_id) REFERENCES Phonenumber(id);
can't i just simplify the database to ... below ... no need for join tables and what not?
Users (id, name)
Phonenumbers (id, user [FK], number)
Correct, these are two valid approaches to the same problem. And yes, the unique index on users_phonenumbers means that each phone number can belong to only one user.
The design is actually suboptimal.
The idea must have been that there are telephone numbers, users, and that they can be linked many-to-many. Because of the unique index on phonenumberid hoever, each number can only be assigned to one user.
Then the whole users_phonenumbers has become redundant, because they could just have added a userid column on the phonenumbers table and save themselves a join.
BAd table design if you ask me.
I have three tables: Users, Companies and Websites.
Users and companies have websites, and thus each user record has a foreign key into the Websites table. Also, each company record has a foreign key into the Websites table.
Now I want to include foreign keys in the Websites table back into their respective "parent" records. How do I do that? Should I have two foreign keys in each website record, with one of them always NULL? Or is there another way to go?
If we look into the model here, we will see the following:
A user is related to exactly one website
A company is related to exactly one website
A website is related to exactly one user or company
The third relation implies existence of a "user or company" entity whose PRIMARY KEY should be stored somewhere.
To store it you need to create a table that would store a PRIMARY KEY of a website owner entity. This table can also store attributes common for a user and a website.
Since it's a one-to-one relation, website attributes can be stored in this table too.
The attributes not shared by users and companies should be stored in the separate table.
To force the correct relationships, you need to make the PRIMARY KEY of the website composite with owner type as a part of it, and force the correct type in the child tables with a CHECK constraint:
CREATE TABLE website_owner (
type INT NOT NULL,
id INT NOT NULL,
website_attributes,
common_attributes,
CHECK (type IN (1, 2)) -- 1 for user, 2 for company
PRIMARY KEY (type, id)
)
CREATE TABLE user (
type INT NOT NULL,
id INT NOT NULL PRIMARY KEY,
user_attributes,
CHECK (type = 1),
FOREIGN KEY (type, id) REFERENCES website_owner
)
CREATE TABLE company (
type INT NOT NULL,
id INT NOT NULL PRIMARY KEY,
company_attributes,
CHECK (type = 2),
FOREIGN KEY (type, id) REFERENCES website_owner
)
you don’t need a parent column, you can lookup the parents with a simple select (or join the tables) on the users and companies table. if you want to know if this is a user or a company website i suggest using a boolean column in your websites table.
Why do you need a foreign key from website to user/company at all? The principle of not duplicating data would suggest it might be better to scan the user/company tables for a matching website id. If you really need to you could always store a flag in the website table that denotes whether a given website record is for a user or a company, and then scan the appropriate table.
The problem I have with the accepted answer (by Quassnoi) is that the object relationships are the wrong way around: company is not a sub-type of a website owner; we had companies before we had websites and we can have companies who are website owners. Also, it seems to me that website ownership is a relationship between a website and either a person or a company i.e. we should have a relationship table (or two) in the schema. It may be an acceptable approach to keep personal website ownership separate from corporate website ownership and only bring them together when required e.g. via VIEWs:
CREATE TABLE People
(
person_id CHAR(9) NOT NULL UNIQUE, -- external identifier
person_name VARCHAR(100) NOT NULL
);
CREATE TABLE Companies
(
company_id CHAR(6) NOT NULL UNIQUE, -- external identifier
company_name VARCHAR(255) NOT NULL
);
CREATE TABLE Websites
(
url CHAR(255) NOT NULL UNIQUE
);
CREATE TABLE PersonalWebsiteOwnership
(
person_id CHAR(9) NOT NULL UNIQUE
REFERENCES People ( person_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE TABLE CorporateWebsiteOwnership
(
company_id CHAR(6) NOT NULL UNIQUE
REFERENCES Companies( company_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE VIEW WebsiteOwnership AS
SELECT url, company_name AS website_owner_name
FROM CorporateWebsiteOwnership
NATURAL JOIN Companies
UNION
SELECT url, person_name AS website_owner_name
FROM PersonalWebsiteOwnership
NATURAL JOIN People;
The problem with the above is there is no way of using database constraints to enforce the rule that a website is either owned by a person or a company but not both.
If we can assuming the DBMS enforces check constraints (as the accepted answer does) then we can exploit the fact that a (human) person and a company are both legal persons and employ a super-type table (LegalPersons) but still retain relationship table approach (WebsiteOwnership), this time using the VIEWs to separate personal website ownership from separate from corporate website ownership but this time with strongly typed attributes:
CREATE TABLE LegalPersons
(
legal_person_id INT NOT NULL UNIQUE, -- internal artificial identifier
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type IN ( 'Company', 'Person' ) ),
UNIQUE ( legal_person_type, legal_person_id )
);
CREATE TABLE People
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type = 'Person' ),
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
person_id CHAR(9) NOT NULL UNIQUE, -- external identifier
person_name VARCHAR(100) NOT NULL
);
CREATE TABLE Companies
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type = 'Company' ),
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
company_id CHAR(6) NOT NULL UNIQUE, -- external identifier
company_name VARCHAR(255) NOT NULL
);
CREATE TABLE WebsiteOwnership
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE VIEW CorporateWebsiteOwnership AS
SELECT url, company_name
FROM WebsiteOwnership
NATURAL JOIN Companies;
CREATE VIEW PersonalWebsiteOwnership AS
SELECT url, person_name
FROM WebsiteOwnership
NATURAL JOIN Persons;
What we need are new DBMS features for 'distributed foreign keys' ("For each row in this table there must be exactly one row in one of these tables") and 'multiple assignment' to allow the data to be added into tables thus constrained in a single SQL statement. Sadly we are a far way from getting such features!
First of all, do you really need this bi-directional link? It is a good practice to avoid it unless absolutely needed.
I understand it that you wish to know whether the site belongs to a user or to a company. You can achieve that by having a simple boolean field in the Website table - [BelongsToUser]. If true, then you look up a user, if false - you look up a company.
A bit late, but all the existing answers seemed to fall somewhat short of the mark:
Owner to website is a 1:Many relation
Website to owner is a 1:1 relation
Users and Companies tables should not have a foreign key into the Websites table
None of the website data, common to users and companies or not, should be in the Users or Companies tables
None of the owner's information, common or not, should be in the Websites table
MySQL ignores, silently, CHECK constraints on tables (no enforcement of referential integrity)
The DBMS ought to handle the 'relation' logic, not the application using the database
Some of this is recognized in the answer from onedaywhen, yet that answer still missed the opportunity to make MySQL do the heavy lifting and enforce the referential integrity.
A website can only have one owner, legally, anyway. A person, or company, can have any number of websites, including none. A link in the database from owner to website can only be 1:1 at any level of normalization. In reality the relation is 1:Many, and would require having multiple table entries for each owner that happens to own more than one website. A link from website to owner is 1:1 in both database terms and in reality. Having the link from website to owner represents the model better. With an index in the website table, doing the 1:Many lookup for a given owner becomes reasonably efficient.
The CHECK attribute in SQL would be an excellent solution, if MySQL didn't happen to silently ignore it.
MySQL Docs 13.1.20 CREATE TABLE Syntax
The CHECK clause is parsed but ignored by all storage engines.
MySQL's functionality does offer two solutions as work-arounds to implement the behavior of CHECK and keep the referential integrity of the data. Triggers with stored procedures is one, and works well with all manner of constraints. Easier to implement, though less versatile, is using a VIEW with a WITH CHECK OPTION clause, which MySQL will implement.
MySQL Docs 24.5.4 The View WITH CHECK OPTION Clause
The WITH CHECK OPTION clause can be given for an updatable view to prevent inserts to rows for which the WHERE clause in the select_statement is not true. It also prevents updates to rows for which the WHERE clause is true but the update would cause it to be not true (in other words, it prevents visible rows from being updated to nonvisible rows).
The MySQLTUTORIAL site gives a good example of both options in their Introduction to the SQL CHECK constraint tutorial. (You have to think around the typos, but good otherwise.)
Having found this question while trying to resolve a similar mutually exclusive foreign key split and developing a solution, with hints generated by the answers, it seems only proper to share my solution in return.
Recommended Solution
For the minimum impact to the existing schema, and the application accessing the data, retain the Users and Companies tables as they are. Rename the Websites table and replace it with a VIEW named Websites which the application can continue to access. Except when dealing with the ownership information, all the old queries to Websites should still work. So:
The setup
-- Keep the `Users` table about "users"
CREATE TABLE `Users` (
`id` INT SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- user_attributes
);
-- Keep the `Companies` table about "companies"
CREATE TABLE `Companies` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- company_attributes
);
-- Attach ownership information about the website to the website's record in the `Websites` table, renamed to `WebsitesData`
CREATE TABLE `WebsitesData` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`is_personal` BOOL,
`owner_user` BIGINT UNSIGNED DEFAULT NULL,
`owner_company` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `WebsiteOwner_User` (`owner_user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebsiteOwner_Company` (`owner_company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
-- Create a new `VIEW` with the original name of `Websites` as the gateway to the website records which can enforce the constraints you need
CREATE VIEW `Websites` AS
SELECT * FROM `WebsitesData` WHERE
(`is_personal`=TRUE AND `owner_user` IS NOT NULL AND `owner_company` IS NULL) OR
(`is_personal`=FALSE AND `owner_user` IS NULL AND `owner_company` IS NOT NULL)
WITH CHECK OPTION;
Usage
-- Use the Websites VIEW for the INSERT, UPDATE, and SELECT operations as you normally would and leave the WebsitesData table in the background.
INSERT INTO `Websites` SET
`is_personal`=TRUE,
`owner_user`=$userID;
INSERT INTO `Websites` SET
`is_personal`=FALSE,
`owner_company`=$companyID;
-- Or, using different field lists based on the type of owner
INSERT INTO `Websites` (`is_personal`,`owner_user`, ...)
VALUES (TRUE, $userID, ...);
INSERT INTO `Websites` (`is_personal`,`owner_company`, ...)
VALUES (FALSE, $companyID, ...);
-- Or, using a common field list, and placing NULL in the proper place
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (TRUE, $userID, NULL, ...);
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (FALSE, NULL, $companyID, ...);
-- Change the company that owns a website
-- Will ERROR if the site was owned by a User.
UPDATE `Websites` SET `owner_company`=$new_companyID;
-- Force change the ownership from a User to a Company
UPDATE `Websites` SET
`owner_company`=$new_companyID,
`owner_user`=NULL,
`is_personal`=FALSE;
-- Force change the ownership from a Company to a User
UPDATE `Websites` SET
`owner_user`=$new_userID,
`owner_company`=NULL,
`is_personal`=TRUE;
-- Selecting the owner of a site without needing to know if it is personal or not
(SELECT `Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE AND `Websites`.`id`=$siteID)
UNION
(SELECT `Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE AND `Websites`.`id`=$siteID);
-- Selecting the sites owned by a User
SELECT `name` FROM `Websites`
WHERE `is_personal`=TRUE AND `id`=$userID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.$userID
WHERE `is_personal`=TRUE AND `Users`.`name`="$user_name";
-- Selecting the sites owned by a Company
SELECT `name` FROM `Websites` WHERE `is_personal`=FALSE AND `id`=$companyID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Comnpanies` ON `Websites`.`owner_company`=`Companies`.$userID
WHERE `is_personal`=FALSE AND `Companies`.`name`="$company_name";
-- Listing all websites and their owners
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION ALL
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
ORDER BY Website, Owner;
-- Listing all users or companies which own at least one website
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION DISTINCT
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
GROUP BY `Owner` ORDER BY `Owner`;
Normalization Level Up
As a technical note for normalization, the ownership information could be factored out of the Websites table and a new table created to hold the ownership data, including the is_normal column.
CREATE TABLE `Websites` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`owner` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `Website_Owner` (`owner`)
REFERENCES `WebOwners` (id`)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE `WebOwnersData` (
`id` SERIAL PRIMARY KEY,
`is_personal` BOOL,
`user` BIGINT UNSIGNED DEFAULT NULL,
`company` BIGINT UNSIGNED DEFAULT NULL,
FOREIGN KEY `WebOwners_User` (`user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebOwners_Company` (`company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
CREATE VIEW `WebOwners` AS
SELECT * FROM WebsitesData WHERE
(`is_personal`=TRUE AND `user` IS NOT NULL AND `company` IS NULL) OR
(`is_personal`=FALSE AND `user` IS NULL AND `company` IS NOT NULL)
WITH CHECK OPTION;
I believe, however, that the created VIEW, with its constraints, prevents any of the anomalies that normalization aims to remove, and adds complexity that is not needed in the situation. The normalization process is always a trade off anyway.