Updating foreign key values - sql

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

Related

Bypass database constraints during record deletion

I have around 20 mapping tables which refer to a single table.
The single table being referenced is,
field (
id integer,
value char
)
The mapping tables are as,
employee_field_map (
employee_id integer references employee(id),
field_id references field(id)
)
dept_field_map (
dept_id integer references dept(id),
field_id references field(id)
)
and similar additional 18 mapping tables.
Now if I want to delete number of records from the field table where field.id = employee_field_map.field_id it takes very long amount of time because there are 20 mapping tables which refer to the field table; And for each of that mapping table a constraint violation check is performed before deleting a record from the field table.
A field table rcord will always be referenced by only one of the mapping table at a time.
In above scenario before deleting a record from field table of course the corresponding record in employee_field_map table is deleted first. So I know for sure that none of the mapping table contains a reference to the field table record being deleted. So is there a way to tell the database engine not to perform those constraint checks when the delete on field table is being performed?
Disabling the constraints is not an option unfortunately. Please advise.
Assuming each of the mapping tables has an index on field_id, then the lookups should not be expensive.
I am wondering why you are not declaring them using cascading delete foreign key references:
employee_field_map (
employee_id integer references employee(id),
field_id references field(id) on delete cascade
);
Nothing in your data model is saying that the field is in only one mapping table. In fact, I don't see why things are broken out the way they are. Presumably there is a reason for breaking the fields apart like this instead of just having a "type" column in the fields table.

Cascade Update on non-primary key in sql server 2008

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)

SQL Server Constraint (Limit bit field based on a foreign key)

I need help with constraints in SQL Server. The situation is for each OrderID=1 (foreign key not primary key so there are multiple rows with the same ID) on the table, the bit field can only be 1 for one of those rows, and for each row with OrderID=2, the bit field can only be 1 for one row, etc etc. It should be 0 for all other rows with the same OrderID. Any new records coming in with 1 in the bit field should reject if there is already a row with that OrderID which has the bit field set to 1. Any ideas?
CREATE UNIQUE INDEX ON UnnamedTable (OrderID) WHERE UnnamedBitField=1
It's called a Filtered Index. If you're on a pre-2008 version of SQL Server, you can implement a poor-mans equivalent of a filtered index using an indexed view:
CREATE VIEW UnnamedView
WITH SCHEMABINDING
AS
SELECT OrderID From UnnamedSchema.UnnamedTable WHERE UnnamedBitField=1
GO
CREATE UNIQUE CLUSTERED INDEX ON UnnamedView (OrderID)
You can't really do it as a constraint, since SQL Server only supports column constraints and row constraints. There's no (non-fudging) way to write a constraint that deals with all values in the table.
You could more fully normalize the schema which will help you not have to hunt for the already set bit but use a join. You need to remove the bit field and crate a new table say X containing OrderID and the primary key of your table, with the primary key of X being all those fields.
This means that when you insert you need to insert into your original table and into X f and only if you would have set the bit to 1 on your table. The insert will fail if there is already a row in X which is as if there was already an original row with bit set to 1.
The downside is that this takes up more space than your schema but is easier to maintain as you can't get to the equivalent of having two rows with the bit set to 1.
The only way to do that is to subclass the parent table. You didn't mention it but a common reason for this pattern is to represent one unique active row from the set of all rows with the same common key value. Let's Assume your bit field represents the active Orders....
Then I would create a separate table called ActiveOrders, which will only contain the one row with the bit field set to 1
Create Table ActiveOrders(int Orderid Primary Key Null)
and the other table with all the rows in it, with it's own unique Primary Key OrderId
Create Table AllOrders
(OrderId Integer Primary Key Not Null, ActiveOrderId Integer Not Null,
[All other data fields]
Constraint FK_AllOrders2ActiveOrder
Foreign Key(ActiveOrderId) references ActiveOrders(OrderId))
You now no longer even need the bit field, as the presence of the row in the ActiveOrders table identifies it as the Active Order... To get only the active Orders (the ones that in your scheme would have bit field set to 1), just join the two tables.
I aggree with the other answers and if you can change the schema then do that but if not then I think something like this will do.
CREATE FUNCTION fnMyCheck
(#id INT)
RETURNS INT
AS
BEGIN
DECLARE #i INT
SELECT #i = COUNT(*)
FROM MyTable
WHERE FkCol = #id
AND BitCol = 1
RETURN #i
END
ALTER TABLE YourTable
ADD CONSTRAINT ckMyCheck CHECK (fnMyCheck(FkCol)<=1)
but there are problems that can come from doing using a udf in a check constraint, such as this
Edit to add comment regarding problems with this 'solution':
There are more straightforward issues than what you've linked to.
INSERT INTO YourTable(FkCol,BitCol) VALUES (1,1),(1,0)
followed by
UPDATE YourTable SET BitCol=1
succeeds and leaves two rows with FkCol=1 and BitCol=1

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.

How to cascade update 2 tables whose foreign key is not set to cascade?

I got 2 tables. They have a foreign key relation. But the foreign key is not set to update cascade. Now I want to update the table's primary key. SQL Server always prevent me from doing that because of the FK. How could I do it in SQL command? I don't have the right to modify the FK.
Thanks.
Why would you want to do this? It strikes me as potentially hazardous.
Your SQL would need to update the related table first NULLing the links back to the PK table and storing the IDs of the records effected. Then you can update the PK in the PK table. Then go back and update the FKs in the related tables.
Create new rows based on the existing row's attribute values but using the new key value. Do the same for all referencing tables. Then, using the new old key value, delete rows from the referencing tables then the referenced table. Something like:
INSERT INTO Table1 (key_col, attrib_col1)
SELECT 'new_key_value', attrib_col1
FROM Table1
WHERE key_col = 'old_key_value';
INSERT INTO Table2 (key_col, attrib_col2)
SELECT 'new_key_value', attrib_col2
FROM Table2
WHERE key_col = 'old_key_value';
DELETE
FROM Table2
WHERE key_col = 'old_key_value';
DELETE
FROM Table1
WHERE key_col = 'old_key_value';
You should only need to INSERT new rows for the parent table and UPDATE the child rows after the fact...
INSERT INTO ParentTable (PKColumn, attribute1, attribute2)
SELECT 'NewPKValue', attribute1, attribute2
FROM ParentTable
WHERE PKColumn = 'OldPKValue'
;
UPDATE ChildTable
SET FKColumn = 'NewPKValue'
WHERE FKColumn = 'OldPKValue'
;
DELETE
FROM ParentTable
WHERE PKColumn = 'OldPKValue'
;
Now for the gotchas:
1.) The above code wont work if there are any unique indexes defined on non-PK columns in the parent table and you need to use the current records' non-PK data values without modification.
2.) Since you're asking this question, I'm assuming your Parent table is not using an IDENTITY column as the PK.
3.) The code is certainly less than efficient. If your db has to do this infrequently on a few rows, you should be fine. If you need to do this 80 times per second, then I would strongly suggest you talk to the programmer/DBA or vendor, if they're at all available, about updating the FK definition to include ON UPDATE CASCADE.
4.) Make sure that there aren't any triggers on either table that would lead to unintended side effects when you create the new parent records or update the child records.