Foreign Key to multiple tables - sql-server

I've got 3 relevant tables in my database.
CREATE TABLE dbo.Group
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.User
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.Ticket
(
ID int NOT NULL,
Owner int NOT NULL,
Subject varchar(50) NULL
)
Users belong to multiple groups. This is done via a many to many relationship, but irrelevant in this case. A ticket can be owned by either a group or a user, via the dbo.Ticket.Owner field.
What would be the MOST CORRECT way describe this relationship between a ticket and optionally a user or a group?
I'm thinking that I should add a flag in the ticket table that says what type owns it.

You have a few options, all varying in "correctness" and ease of use. As always, the right design depends on your needs.
You could simply create two columns in Ticket, OwnedByUserId and OwnedByGroupId, and have nullable Foreign Keys to each table.
You could create M:M reference tables enabling both ticket:user and ticket:group relationships. Perhaps in future you will want to allow a single ticket to be owned by multiple users or groups? This design does not enforce that a ticket must be owned by a single entity only.
You could create a default group for every user and have tickets simply owned by either a true Group or a User's default Group.
Or (my choice) model an entity that acts as a base for both Users and Groups, and have tickets owned by that entity.
Heres a rough example using your posted schema:
create table dbo.PartyType
(
PartyTypeId tinyint primary key,
PartyTypeName varchar(10)
)
insert into dbo.PartyType
values(1, 'User'), (2, 'Group');
create table dbo.Party
(
PartyId int identity(1,1) primary key,
PartyTypeId tinyint references dbo.PartyType(PartyTypeId),
unique (PartyId, PartyTypeId)
)
CREATE TABLE dbo.[Group]
(
ID int primary key,
Name varchar(50) NOT NULL,
PartyTypeId as cast(2 as tinyint) persisted,
foreign key (ID, PartyTypeId) references Party(PartyId, PartyTypeID)
)
CREATE TABLE dbo.[User]
(
ID int primary key,
Name varchar(50) NOT NULL,
PartyTypeId as cast(1 as tinyint) persisted,
foreign key (ID, PartyTypeId) references Party(PartyID, PartyTypeID)
)
CREATE TABLE dbo.Ticket
(
ID int primary key,
[Owner] int NOT NULL references dbo.Party(PartyId),
[Subject] varchar(50) NULL
)

The first option in #Nathan Skerl's list is what was implemented in a project I once worked with, where a similar relationship was established between three tables. (One of them referenced two others, one at a time.)
So, the referencing table had two foreign key columns, and also it had a constraint to guarantee that exactly one table (not both, not neither) was referenced by a single row.
Here's how it could look when applied to your tables:
CREATE TABLE dbo.[Group]
(
ID int NOT NULL CONSTRAINT PK_Group PRIMARY KEY,
Name varchar(50) NOT NULL
);
CREATE TABLE dbo.[User]
(
ID int NOT NULL CONSTRAINT PK_User PRIMARY KEY,
Name varchar(50) NOT NULL
);
CREATE TABLE dbo.Ticket
(
ID int NOT NULL CONSTRAINT PK_Ticket PRIMARY KEY,
OwnerGroup int NULL
CONSTRAINT FK_Ticket_Group FOREIGN KEY REFERENCES dbo.[Group] (ID),
OwnerUser int NULL
CONSTRAINT FK_Ticket_User FOREIGN KEY REFERENCES dbo.[User] (ID),
Subject varchar(50) NULL,
CONSTRAINT CK_Ticket_GroupUser CHECK (
CASE WHEN OwnerGroup IS NULL THEN 0 ELSE 1 END +
CASE WHEN OwnerUser IS NULL THEN 0 ELSE 1 END = 1
)
);
As you can see, the Ticket table has two columns, OwnerGroup and OwnerUser, both of which are nullable foreign keys. (The respective columns in the other two tables are made primary keys accordingly.) The CK_Ticket_GroupUser check constraint ensures that only one of the two foreign key columns contains a reference (the other being NULL, that's why both have to be nullable).
(The primary key on Ticket.ID is not necessary for this particular implementation, but it definitely wouldn't harm to have one in a table like this.)

Another approach is to create an association table that contains columns for each potential resource type. In your example, each of the two existing owner types has their own table (which means you have something to reference). If this will always be the case you can have something like this:
CREATE TABLE dbo.Group
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.User
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.Ticket
(
ID int NOT NULL,
Owner_ID int NOT NULL,
Subject varchar(50) NULL
)
CREATE TABLE dbo.Owner
(
ID int NOT NULL,
User_ID int NULL,
Group_ID int NULL,
{{AdditionalEntity_ID}} int NOT NULL
)
With this solution, you would continue to add new columns as you add new entities to the database and you would delete and recreate the foreign key constraint pattern shown by #Nathan Skerl. This solution is very similar to #Nathan Skerl but looks different (up to preference).
If you are not going to have a new Table for each new Owner type then maybe it would be good to include an owner_type instead of a foreign key column for each potential Owner:
CREATE TABLE dbo.Group
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.User
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.Ticket
(
ID int NOT NULL,
Owner_ID int NOT NULL,
Owner_Type string NOT NULL, -- In our example, this would be "User" or "Group"
Subject varchar(50) NULL
)
With the above method, you could add as many Owner Types as you want. Owner_ID would not have a foreign key constraint but would be used as a reference to the other tables. The downside is that you would have to look at the table to see what the owner types there are since it isn't immediately obvious based upon the schema. I would only suggest this if you don't know the owner types beforehand and they won't be linking to other tables. If you do know the owner types beforehand, I would go with a solution like #Nathan Skerl.
Sorry if I got some SQL wrong, I just threw this together.

Yet another option is to have, in Ticket, one column specifying the owning entity type (User or Group), second column with referenced User or Group id and NOT to use Foreign Keys but instead rely on a Trigger to enforce referential integrity.
Two advantages I see here over Nathan's excellent model (above):
More immediate clarity and simplicity.
Simpler queries to write.

you can also use an enum to identify whether Owner is user or group like this:
CREATE TABLE dbo.Group
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TABLE dbo.User
(
ID int NOT NULL,
Name varchar(50) NOT NULL
)
CREATE TYPE Enum_OwnerType AS ENUM ('Group', 'User');
CREATE TABLE dbo.Ticket
(
ID int NOT NULL,
Owner int NOT NULL,
OwnerType Enum_OwnerType NOT NULL,
Subject varchar(50) NULL
)
Maybe it's no better than any of proposed solutions, it might not offer any advantage. In fact, I think that this might require altering Enum_OwnerType and even ticket in order to change OwnerType, I guess... I hope it's useful anyway.

I have many cases like this and I just use polymorphic ability like below:
example
I have turnovers table that have this columns id, amount, user_id and I need to know the refrence of every records, So I just add two Fields table_id and table_type and my final turnovers table is like id, amount, user_id,table_id, table_type.
if new record is about order record inserted like this
[1,25000,2,22,order]
and if new record is about increment credit like this
[1,25000,2,23,credit]
note
if using M:M tables its take so much time two retrieve the records
and my way
Cons is turnovers table records number is grows up
Pons is more flexible in new records and readable and search ability

nathan_jr's 4th option (model an entity that acts as a base for both Users and Groups, and have tickets owned by that entity) doesn't enforce referential integrity on PartyId. You'd have to do that on the application layer which invites all sorts of trouble. Can't really call it an antipattern when django's genericforeignkey implements the same solution, but no doubt you can design something more robust and performant using your framework's orm (using something like django's Multi-table inheritance)

CREATE TABLE dbo.OwnerType
(
ID int NOT NULL,
Name varchar(50) NULL
)
insert into OwnerType (Name) values ('User');
insert into OwnerType (Name) values ('Group');
I think that would be the most general way to represent what you want instead of using a flag.

Related

Did I write this query correctly?

I'm given the question
My solution is
USE Finances
CREATE TABLE Account
(AccountID varchar(25) NOT NULL PRIMARY KEY,
AccountName varchar(50) NOT NULL,
AccountAddress varchar(30) NULL,
AccountCity varchar(25) NULL,
AccountState char(2) NULL,
AccountZip varchar(10) NULL,
AccountPhone varchar(14) NULL)
CREATE TABLE Transactions
(TransactionID INT NOT NULL PRIMARY KEY IDENTITY,
AccountID varchar(25) REFERENCES Account (AccountID),
TransactionDate smalldatetime NOT NULL,
TransactionAmount money NOT NULL)
CREATE TABLE Register
(RegisterID INT NOT NULL PRIMARY KEY IDENTITY,
TransactionID INT NOT NULL REFERENCES Transactions (TransactionID));
Part of the reason I'm confused is because I don't completely understand the primary/foreign key relationship. Like I look at the question and say, ok the registerID will be the single/unique ID to seperate every account from each other. But I see that there's a TranasctionID in the same table. And that TransactionID is a foreign key to the TransactionID in the transaction table. So does that mean whenever a new tranasctionID is added there is a corresponding RegisterID. Like whats the point of the Register ID?? And did I write this query correctly?
The only issue in you script is your column "AccountID" in table "Transactions" is creating with allowing NULL and "TransactionAmount" is NOT NULL where as it should allow NULL. Other wise I found the tables are creating with appropriate type and relation as instructed.
Regarding your confusion, I am confused too thinking about the purpose of table "Register". As per instruction, yes it will create a new row every time a new transaction take place. As registerId is a PK, with each transaction a new PK will generate. But the question is why a new RegisterId is required for each TransactionID? If RegisterId is referring to other table for a transaction details, it can be easily use the TransactionID directly as both TransactionID and RegisterId are unique (PK).

inserting into multiple related table at once and return id if exists else insert into multiple tables

I am a newbie to SQL/PostgreSQL. I wanted to insert into multiple related tables at once if certain criteria is satisfied else return the existing id of the table. Let me give an example here, following are my exemplary tables.
CREATE TABLE profile (
id serial NOT NULL,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
PRIMARY KEY (id)
)
CREATE TABLE movies (
movied_id serial NOT NULL,
movie_name VARCHAR NOT NULL,
profile_id INTEGER NOT NULL,
PRIMARY KEY (movie_id),
FOREIGN KEY (profile_id) REFERENCES profile.id
)
CREATE TABLE sports (
sport_id serial NOT NULL,
sport_name VARCHAR NOT NULL,
profile_id INTEGER NOT NULL,
PRIMARY KEY (sport_id),
FOREIGN KEY (profile_id) REFERENCES profile.id
)
CREATE TABLE visited_countries (
country_name VARCHAR NOT NULL,
profile_id INTEGER NOT NULL,
PRIMARY KEY (country_name),
FOREIGN KEY (profile_id) REFERENCES profile.id
)
Here, profile table contains id, first_name, last_name and it is related to movies, sports, visited_countries tables. There exist one to many relation ship between the profile table and all the other tables (Foreign Keys are shown in tables as well). Given this situation:
I want to insert into all these tables at once in the order by getting the unique profile ID first and then use that as FK for all the other tables.
I wanted to check the following before inserting into all of them.
-- first_name, last_nane, and all the movies, sports, visited_countries seen by profile id should be different then only I will insert into all these table at once in the order using the generated profiled ID else just return the profile id.
I have multiple clients writing into the tables at the same time so I wanted to lock and do this transaction in one go.
I read couple of things using CTEs but couldn't get it working correctly. Any suggestion and systematic approach to do this would be helpful.

How can I create a table in SQL only if a column in another table is true?

For example, assume I want to create the following table:
CREATE TABLE Add_Friends(
PRIMARY KEY(email_1, email_2),
email_1 VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users,
email_2 VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users,
accepted_or_not BIT
)
CREATE TABLE Messages1(
message_ID INT PRIMARY KEY IDENTITY,
date_sent date,
message_content VARCHAR(100),
sender VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users,
reciever VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users
)
Users should only be able to send messages to each other if they are friends (the last column in Add_Friends). How can I define this constraint in the Messages1 table?
If I understand correctly your question:
You don't need to create a table for each conversation, you can put all the messages together in the same table and id the conversation registering the primarykey from the add frinds.
I assume you have another table with all the data of your "persons" and then Add_Friends is the table where you add realtionsship between those persons, so to do the restriction you want you need to first check if the relationship between sender and reciever exist and then do an insert if the query returns a value.
SELECT ID_FRIEND
FROM ADD_FRIENDS
WHERE email_1 = "+ID_SENDER+" AND email_2 = "+ID_RECIEVER+"
If this query returns a value it means the realationship exist and you can do the insert.
Is better to do this in two steps so you can also restrict things like if the person is or not registed, online status or even be able to write a message in the first place.
And then to search for a chat you can do:
SELECT *
FROM messages AS m INNER JOIN add_friends as AF ON M.SENDER= AF.EMAIL1
WHERE email_2 = '" + EMAIL_CONVERSION_WITH + "'"
You can achieve this in any language.
You can create a user defined function passing it the sender & receiver - that function can check if a record of those emails exists in Add_Friends and if accepted_or_not = 1 and return BIT (0 / 1). Then you can use the function in a constraint:
ALTER TABLE [Messages1]
WITH CHECK ADD CONSTRAINT [Your_Constraint_Name]
CHECK ([dbo].[Your_Function_Name]([sender], [reciever]) = 1)
ALTER TABLE [Messages1] CHECK CONSTRAINT [Your_Constraint_Name]
Instead of having a column accepted_or_notin Add_Friends, you can define another table 'Friends_Accepted':
CREATE TABLE Friends_Accepted(
Id INT PRIMARY KEY IDENTITY,
email_canSend VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users,
email_canReceive VARCHAR(50) FOREIGN KEY REFERENCES Normal_Users,
)
In this table, you would add TWO lines when a friend request is accepted -- one for each direction of the conversation: (A.Email,B.Email), (B.Email, A.Email)
Now you can have the table Messages1 reference Friends_Accepted:
CREATE TABLE Messages1(
message_ID INT PRIMARY KEY IDENTITY,
date_sent date,
message_content VARCHAR(100),
fromTo_Id FOREIGN KEY REFERENCES Friends_Accepted,
)
So a message from (A to B) and a message from (B to A) will reference two DIFFERENT lines in Friends_Accepted

BuyerAccounts and SellerAccounts Tables that both reference Accounts Table. Create Account INSERT

I am designing tables to store accounts of website users. There will be two types of accounts for the website, BuyerAccounts and SellerAccounts. My approach is to have a master Accounts table that will store information that is common to both types of accounts and then to have a table each for BuyerAccounts and SellerAccounts.
This is the basic setup..
create table Accounts
(
UserID_PK int identity not null primary key,
Email varchar(50) not null unique,
UserPassword varchar(50) not null,
Salt varchar(50) not null,
AccountType tinyint not null,
);
create table SellerAccounts
(
SellerID_PK int not null primary key foreign key references Accounts(UserID_PK),
SellerColumn int
);
create table BuyerAccounts
(
BuyerID_PK int not null primary key foreign key references Accounts(UserID_PK),
DeliveryAddress string,
BuyerColumn string
);
Each of SellerAccounts and BuyerAccounts has a Primary Key that is also a Foreign Key referencing the Primary Key of the Accounts Table.
This all seems okay so far to me but I'm a beginner so please say if I have done something wrong or bad so far.
When an account is created I want to create a record in Accounts and also a record in either BuyerAccounts or SellerAccounts.
How do I do this? I'm going to make it a stored procedure but my primary concern here is how to make an INSERT into two tables that are linked. Say a BuyerAccount is being created. Does account creation have to be broken into two INSERTS? First into Accounts, so that the UserID_PK for the account that will also be the BuyerID_PK of BuyerAccounts is known and then the rest of the account info can be inserted into BuyerAccounts? Is this how is should work?
How do I write an INSERT that returns UserID_PK so that I have it for the second insert? Or even better can SQL Server do something clever and just work out the Values of UserID_PK and BuyerID_PK for me and do everything with just one insert?
Thank you for your help!
use scope_identity()
example
declare #id int
insert Accounts values('a#b.com','pass','bla',1)
select #id = SCOPE_IDENTITY()
insert SellerAccounts values(#id,1)
select * from SellerAccounts

INSTEAD OF UPDATE Trigger and Updating the Primary Key

I am making changes to an existing database while developing new software. There is also quite a lot of legacy software that uses the database that needs to continue working, i.e. I would like to maintain the existing database tables, procs, etc.
Currently I have the table
CREATE TABLE dbo.t_station (
tx_station_id VARCHAR(4) NOT NULL,
tx_description NVARCHAR(max) NOT NULL,
tx_station_type CHAR(1) NOT NULL,
tx_current_order_num VARCHAR(20) NOT NULL,
PRIMARY KEY (tx_station_id)
)
I need to include a new field in this table that refers to a Plant (production facility) and move the tx_current_order_num to another table because it is not required for all rows. So I've created new tables:-
CREATE TABLE Private.Plant (
PlantCode INT NOT NULL,
Description NVARCHAR(max) NOT NULL,
PRIMARY KEY (PlantCode)
)
CREATE TABLE Private.Station (
StationId VARCHAR(4) NOT NULL,
Description NVARCHAR(max) NOT NULL,
StationType CHAR(1) NOT NULL,
PlantCode INT NOT NULL,
PRIMARY KEY (StationId),
FOREIGN KEY (PlantCode) REFERENCES Private.Plant (PlantCode)
)
CREATE TABLE Private.StationOrder (
StationId VARCHAR(4) NOT NULL,
OrderNumber VARCHAR(20) NOT NULL,
PRIMARY KEY (StationId)
)
Now, I don't want to have the same data in two places so I decided to change the dbo.t_station table into a view and provide instead of triggers to do the DELETE, INSERT and UPDATE. No problem I have [most of] them working.
My question regards the INSTEAD OF UPDATE trigger, updating the Primary Key column (tx_station_id) and updates to multiple rows.
Inside the trigger block, is there any way to join the inserted and deleted [psuedo] tables so that I know the 'before update primary key' and the 'after update primary key'? Something like this...
UPDATE sta
SET sta.StationId = ins.tx_station_id
FROM Private.Station AS sta
INNER JOIN deleted AS del
INNER JOIN inserted AS ins
ON ROW_IDENTITY_OF(del) = ROW_IDENTITY_OF(ins)
ON del.tx_station_id = sta.StationId
At this stage I've put a check in the trigger block that rollbacks the update if the primary key column is updated and there is more than one row in the inserted, or deleted, table.
The short answer is no.
You could put a surrogate key on Private.Station, and expose that through the view, and use that to identify before and after values. You wouldn't need to change the primary key or foreign key relationship, but you would have to expose some non-updateable cruft through the view, so that it showed up in the pseudo-tables. eg:
alter table Private.Station add StationSk int identity(1,1) not null
Note, this may break the legacy application if it uses SELECT *. INSERT statements without explicit insert column lists should be ok, though.
Short of that, there may be some undocumented & consistent ordering between INSERTED and DELETED, such that ROW_NUMBER() OVER (ORDER BY NULLIF(StationId,StationId)) would let you join the two, but I'd be very hesitant to take the route. Very, very hesitant.
Have you intentionally not enabled cascade updates? They're useful when primary key values can be updated. eg:
CREATE TABLE Private.Station (
StationId VARCHAR(4) NOT NULL,
Description NVARCHAR(max) NOT NULL,
StationType CHAR(1) NOT NULL,
PlantCode INT NOT NULL,
PRIMARY KEY (StationId),
FOREIGN KEY (PlantCode) REFERENCES Private.Plant (PlantCode)
ON UPDATE CASCADE
-- maybe this too:
-- ON DELETE CASCADE
)
Someone might have a better trick. Wait and watch!

Resources