Cascade Update on non-primary key in sql server 2008 - sql

I am trying to implement cascade update and I have searched online for the Fk constraint. I found the solutions like
http://sqlandme.com/2011/08/08/sql-server-how-to-cascade-updates-and-deletes-to-related-tables/
and that blog explain the process pretty good. I am not sure that dose this only works on PK or we can set up the cascade update/delete on non-pk fields also.
I have two tables.
tblregistration:
UserID (PK)
UserName
CompName
etc
tblposting_detail:
Bidid (pk)
UserID (Fk with tblregistration)
CompName
Now when a user edit his/her profile and update the company name, that is updating the compname in tblregistration, what I want here is that compname update also in my tblposting_detail on the same user who have updated his/her profile.
I have read some article saying that cascade update and delete gives unexpected results sometimes so its not preferred all the time and better to have two different update SQL statement instead on cascade update.
Could any one help me understand the process and best practice for this particular problem.
Thanks.

Cascade update and cascade delete give perfectly predictable results. They give unexpected results only when developers are ignorant of their predictable results.
The target of a foreign key constraint must be a set of columns that are unique--the set of columns must have either a primary key constraint or a unique constraint.
In your case, tblregistration.CompName isn't unique. (Probably can't be unique.) You could mimic cascades using triggers, or by revoking permissions on the tables and requiring application code to use a stored procedure, but you're better off dropping the column CompName from tblPosting_detail. Use a SELECT query with a join when you need the data that's in that column.

You can do it by introducing a "super-key" that covers both your existing PK and the additional columns that you want to have cascade:
create table tblregistration (
UserID int not null primary key,
UserName int not null,
CompName int not null,
constraint UQ_reg_target UNIQUE (UserID,CompName)
)
go
create table tblposting_detail (
Bidid int not null primary key,
UserID int not null references tblregistration (UserID),
CompName int not null,
constraint FK_post_reg FOREIGN KEY (UserID,CompName)
references tblregistration (UserID,CompName) on update cascade
)
go
insert into tblregistration values (1,1,1)
go
insert into tblposting_detail values (2,1,1)
go
update tblregistration set CompName = 4 where UserID = 1
go
select * from tblposting_detail
Result:
Bidid UserID CompName
----------- ----------- -----------
2 1 4
But I agree with the others that you just probably shouldn't have this column in your second table. I'd also advise against using a tbl prefix on all of your tables.
(Note that, at this point, it's up to you whether you keep the FK on just UserID as well as the FK on UserID and CompName, or just retain the latter one)

Related

Is a single field contain multiple foreign key relationship from different table [SQL]

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.

Delete entry in table that is referenced by another table

I have two tables, Games and Sport_Games. Sport_Games has a foreign key that references Games.
Now when I want to delete an entry from Games I get an error saying:
"FK__Sport_Gam__game___1D7C2B7C". The conflict occurred in database "DatabasesProject", table "dbo.Sport_Games", column 'game_id'.
The statement has been terminated.
Im assuming this is because I can't delete something from a table with a constraint without dropping it. I tried adding ON DELETE CASCADE in table Games in hopes that when I delete a game it is also deleted from Sport_Games but that didn't work either.
Here are the tables for reference:
CREATE TABLE Games(
game_id INT IDENTITY PRIMARY KEY,
name VARCHAR(50),
release_date date,
rating INT,
min_age INT,
development_team_email VARCHAR(50) FOREIGN KEY REFERENCES Development_Teams,
release_conference INT FOREIGN KEY REFERENCES Conferences
--ON DELETE CASCADE ---this is what I added
)
CREATE TABLE Sport_Games(
game_id INT PRIMARY KEY FOREIGN KEY REFERENCES Games,
sport_type VARCHAR(50),
)
I have other tables referencing Games as well, action_games, strategy_games, But they're similar to sport_games
If you know the GameID of the records you are deleting, simply delete records in the Sport_Games table that have the same GameID first, then delete from Games table.
For the cascade to work, you need to ad that to the FK definition on the Sport_Games table. That way when the Games record is deleted, the sport_Games record will be deleted as well.
Your ON DELETE CASCADE will work for you if you put it on the correct Foreign Key. But to answer your question...
Since Games.Game_ID has a constraining reference against Sport_Games.Game_ID, you need to either turn the constraint off so it will not prevent you from deleting, but this will leave orphans, or delete the children before you delete the parent. Since you must already know the PK value to delete the parent, you simply:
DELETE FROM Sport_Games WHERE Game_ID = [The id you are using to delete from Games]
Then you delete the parent:
DELETE FROM Games WHERE Game_ID = [Same id used above]
What I'm about to say might not directly answer your question, but it could be beneficial for you moving forward or in future designs.
I've found placing "delete_date" and "delete_id" in tables to be a useful way to work around these FK constraint issues. If your application is only calling existing stored procedures and table functions you could update the queries to include "AND delete_date IS NULL" in their conditions. If your application is writing adhoc queries, it might also be as simple as creating or modifying a base class to inject the condition that omits soft-deleted rows.

Update trigger old values natural key

I have an accounts table with the account owner as the primary key. In the update trigger, I want to update some accounts to new owners. Since this table doesn't have an id field, how do I use the inserted/updated tables in the trigger? DB is sql server 2008.
CREATE TRIGGER accounts_change_owner on accounts AFTER INSERT
AS BEGIN
MERGE INTO accounts t
USING
(
SELECT *
FROM inserted e
INNER JOIN deleted f ON
e.account_owner = f.account_owner ---this won't work since the new account owner value is diff
) d
ON (t.account_owner = d.account_owner)
WHEN MATCHED THEN
UPDATE SET t.account_owner = d.account_owner
END
I think I understood your question, but I am not sure. You want to be able update account owner name in one table and to have this update propagated to the referencing tables?
If so you don't really need a trigger, you can use on update cascade foreign key.
Like this:
create table AccountOwner
(
Name varchar(100) not null
constraint PK_AccountOwner primary key
)
create table Account
(
AccountName varchar(100) not null,
AccountOwnerName varchar(100) not null
constraint FK_Account_AccountOwnerName references AccountOwner(Name) on update cascade
)
insert AccountOwner values('Owner1')
insert Account values('Account1', 'Owner1')
Now if I update table AccountOwner like this
update AccountOwner
set Name = 'Owner2'
where Name = 'Owner1'
it will automatically update table 'Account'
select *
from Account
AccountName AccountOwnerName
----------- -----------------
Account1 Owner2
I think you need to modify the design of your table. Recall that the three attributes of a primary key are that the primary key must be
Non-null
Unique
Unchanging
(If the primary key consists of multiple columns, all columns must follow the rules above). Most databases enforce #1 and #2, but the enforcement of #3 is usually left up to the developers.
Changing a primary key value is a classic Bad Idea in a relational database. You can probably come up with a way to do it; that doesn't change the fact that it's a Bad Idea. Your best choice is to add an artificial primary key to your table, put NOT NULL and a UNIQUE constraints on the ACCOUNT_OWNER field (assuming that this is the case), and change any referencing tables to use the artificial key.
The next question is, "What's so bad about changing a primary key value?". Changing the primary key value alters the unique identifier for that particular data; if something else is counting on having the original value point back to a particular row, such as a foreign key relationship, after such a change the original value will no longer point where it's supposed to point.
Good luck.

Will multiply insert requests to the same table with direct query and store-procedure cause collision?

Multiply users can call store procedure(SP), that will make some changes to mytable in SQL Server. This SP should insert some rows to mytable that has reference to itself through parentid column.
TABLE mytable(
id int identity(1,1) primary key,
name varchar(20) not null,
parentId int not null foreign key references mytable(id)
)
in order to insert row to such table, accordingly to other posts, I have 2 ways:
Allow null to parentid column by ALTER TABLE mytable alter column parentid int null;, insert the row, update parentid and than disable null to parentid
Allow IDENTITY by set identity_insert maytable on, insert dummy row with id=-1 and parentid=-1, insert the correct row with reference to -1, update the parentid to SCOPE_IDENTITY() and in the end set IDENTITY to off
The case:
Assume I take the 2nd way. SP managed to set identity_insert mytable on BUT didn't yet finished the execution of the rest SP. At this time, there are other INSERT requests(NOT through SP) to the mytable table like INSERT INTO mytable(name,parentid) VALUES('theateist', -1). No id is specified because they assumed that IDENTITY is off and therefore id is auto-incremental.
The Question:
Will this cause errors while inserting because IDENTITY, in this period of time, is ON and not auto-incremental any more and therefore it will require id specification? If yes, it will be better to use the 1st way, isn't it?
Thank you
identity_insert is a per-connection setting - you won't affect other connections/statements running against this table.
I definitely wouldn't suggest going the first way, if it could be avoided, since it could impact other users of the table - e.g. some other connection could do a broken insert (parentid=null) while the column definition allows it, and then your stored proc will break. Also, setting a column not null forces a full table scan to occur, so this won't work well as the table grows.
If you did stick with method 2, you've still got an issue with what happens if two connections run this stored proc simultaneously - they'll both want to insert the -1 row, at different times, and delete it also. You'll have conflicts.
I'm guessing the problem you're having is inserting the "roots" of the tree(s), since they have no parent, and so you're attempting to have them self referencing. I'd instead probably make the roots have a null parentid permanently. If there's some other key column(s), these could be used in a filtered index or indexed view to ensure that only one root exists for each key.
Imagine that we're building some form of family trees, and ignoring most of the realities of such beasts (such as most families requiring children to have two parents):
CREATE TABLE People (
PersonID int IDENTITY(1,1) not null,
Surname varchar(30) not null,
Forename varchar(30) not null,
ParentID int null,
constraint PK_People PRIMARY KEY (PersonID),
constraint FK_People_Parents FOREIGN KEY (ParentID) references People (PersonID)
)
CREATE UNIQUE INDEX IX_SoleFamilyRoot ON People (Surname) WHERE (ParentID is null)
This ensures that, within each family (as identified by the surname), exactly one person has a null ParentID. Hopefully, you can modify this example to fit your model.
On SQL Server 2005 and earlier, you have to use an indexed view instead.

Updating foreign key values

I have a database application in which a group is modeled like this:
TABLE Group
(
group_id integer primary key,
group_owner_id integer
)
TABLE GroupItem
(
item_id integer primary key,
group_id integer,
group_owner_id integer,
Foreign Key (group_id, group_owner_id) references Group(group_id, group_owner_id)
)
We have a multi field foreign key set up including the group_owner_id because we want to ensure that a GroupItem cannot have a different owner than the Group it is in. For other reasons (I don't think I need to go into detail on this) the group_owner_id cannot be removed from the GroupItem table, so just removing it is not an option.
My big problem is if i want to update the group_owner_id for the entire group, I'm writing code like this (in pseudo code):
...
BeginTransaction();
BreakForeignKeys(group_items);
SetOwnerId(group, new_owner_id);
SaveGroup(group);
SetOwnerId(group_items, new_owner_id);
SetForeignKeys(group_items, group);
SaveGroupItems(group_items);
CommitTransaction()
...
Is there a way around doing this? It seems a bit clunky. Hopefully, I've posted enough detail.
Thanks.
Does SQL Server not support UPDATE CASCADE? :-
Foreign Key (group_id, group_owner_id)
references Group(group_id, group_owner_id)
ON UPDATE CASCADE
Then you simply update the Group table's group_owner_id.
Tony Andrew's suggestion works. For example, say you'd like to change the owner of group 1 from 2 to 5. When ON UPDATE CASCADE is enabled, this query:
update [Group] set group_owner_id = 5 where group_id = 1
will automatically update all rows in GroupItem.
If you don't control the indexes and keys in the database, you can work around this by first inserting the new group, then modifying all rows in the dependant table, and lastly deleting the original group:
insert into [Group] values (1,5)
update [GroupItem] set group_owner_id = 5 where group_id = 1
delete from [Group] where group_id = 1 and group_owner_id = 2
By the way, GROUP is a SQL keyword and cannot be a table name. But I assume your real tables have real names so this is not an issue.
You might try to add an update rule to cascade changes.
Microsoft Links
Foreign Key Relationships Dialog Box (see Update Rule)
Grouping Changes to Related Rows with Logical Records