Using triggers for referential integrity - sql-server

There are 2 tables in the database that contain the following columns:
department table with column dept_no (char(4), not null)
employee table with column dept_no (char(4), null)
The dept_no column needs to be defined as a primary key in the department table and a foreign key in the employee table using a trigger.
I thought this was the correct solution using the deleted and inserted virtual tables to update/delete the foreign key in the corresponding employee table:
CREATE TRIGGER trig_delete_dept_no
ON department
AFTER DELETE
AS
UPDATE employee
SET employee.dept_no = NULL
FROM deleted
WHERE employee.dept_no = deleted.dept_no
CREATE TRIGGER trig_update_dept_no
ON department
AFTER UPDATE
AS
UPDATE e
SET e.dept_no = i.dept_no
FROM employee e
INNER JOIN inserted i ON e.dept_no = i.dept_no
However, when I update the department dept_no row to a different value I do not see the corresponding dept_no update in the employee table:
UPDATE department
SET dept_no = 'd4'
WHERE dept_no = 'd3'
Deleting functions as expected. What am I doing wrong with the update trigger and how can I combine these two triggers into one trigger?

There is an issue in your design. The first thing is you should not use dept_no as PK (Primary Key). You need to have an IDENTITY or GUID column as Primary Key and refer to that column as FK (Foreign Key).
This way you won't need to worry about changing the dept_no.
The second point is you don't need trigger. you can use CASCADE option on DELETE action.
Find more information on CASCADE

Thank you FLICKER and SMor for helping me think through this. I do not believe the assignment wants us to modify the tables by adding IDENTITY or GUID columns and since we are to strictly use Triggers, this is the best solution I can come up with:
CREATE TRIGGER trig_delete_dept_no ON department AFTER DELETE AS
UPDATE employee
SET employee.dept_no = NULL
FROM deleted
WHERE employee.dept_no = deleted.dept_no
CREATE TRIGGER trig_update_dept_no ON department AFTER UPDATE AS
IF UPDATE(dept_no)
BEGIN
IF (SELECT employee.dept_no
FROM employee, inserted
WHERE employee.dept_no = inserted.dept_no) IS NULL
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Integrity constraint violation, TRIGGER:
trig_update_dept_no, TABLE: department',16,1)
END
ELSE PRINT 'Update successful'
END
This will allow updates to occur in the department as long as there are no orphaned records in the employee table.

Related

Replacing a varchar FK with a new Int FK in SQL Server

I have two tables which we will call Event and EventResponse
Event has PK eventGuid varchar(36) and several other columns
EventResponse has FK eventGuid varchar(36) and other columns
I can easily add an INT identity column to Event with:
ALTER TABLE dbo.[Event]
ADD eventId INT IDENTITY;
And adding a column to EventResponse table is Ok.
How do I update all the eventIds in the response table?
Is this possible with a neat query or do I have to loop through?
Final state should be:
Event has PK eventId, eventGuid varchar(36), other columns
EventResponse has FK eventId, eventGuid varchar(36), other columns
No looping required. Words to live by :)
This should be just about all you need. Use your existing foreign key to update the values for your new key, then drop the old relationship and add the new one.
UPDATE er
SET er.eventID = e.eventID
FROM
dbo.EventResponse AS er
JOIN
dbo.Event as e
ON er.eventGuid = e.eventGuid;
GO
ALTER TABLE dbo.EventResponse
DROP CONSTRAINT <FK_Name>
GO
ALTER TABLE dbo.EventResponse
ADD CONSTRAINT <FK_Name> FOREIGN KEY (eventId)
REFERENCES dbo.Event(eventId)
[ON DELETE CASCADE
ON UPDATE CASCADE]
GO
Then, probably, if you want to save some space in your database:
ALTER TABLE dbo.Event
DROP COLUMN eventGuid;
GO
ALTER TABLE dbo.EventResponse
DROP COLUMN eventGuid;
GO
try this:
update a set a.eventId=b.eventId
from EventResponse a
inner join Event b on a.eventGuid = b.eventGuid

Error on delete trigger

This trigger should delete a row from the parent table that is not deleted from the child table. The error is in the image below.
My code attempt:
CREATE TRIGGER ProductDeleted ON Product
for DELETE AS
BEGIN
DELETE FROM OrderItem
WHERE ProductID = (SELECT ProductID FROM DELETED)
END
help me please
You could simplify it by adding a CASCADE DELETE hint on a foreign key constraint such as
CREATE TABLE OrderItem
(
ID INT ,
ProductID NOT NULL UNIQUE
CONSTRAINT fk_Products
REFERENCES Products (ID) ON DELETE CASCADE
);
Since you already have a table, all you need to do is drop the constraint and recreate a new one.
ALTER TABLE OrderItem DROP
CONSTRAINT fk_ProductID;
ALTER TABLE OrderItem ADD
CONSTRAINT fk_ProductID
FOREIGN KEY (ID)
REFERENCES Product (ID)
ON DELETE CASCADE;
What this means, is that , any time you delete a record from the parent table (Product), child records from (OrderItem)will be deleted as well, so you dont have to use triggers, unless if you want to do some recording.
If you are really insisting on using triggers then you can tweak it a little bit like this :
ALTER TRIGGER ProductDeleted on Product
INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
/* First we are deleting referenced columns in OrderItem table */
DELETE FROM OrderItem
where ProductID IN (select deleted.ID /* Columns from product Table */ from deleted)
/* Now we are doing actual delete statement */
DELETE FROM Products where ID IN (select deleted.ID from deleted)
END
But once again you should consider using ON CASCADE DELETE, its much simpler to setup, easier to maintain and you can have only one INSTEAD OF trigger per table, so if you ever need to do something more meaningful you would have to change this one, and add extra overhead.
Add SET NOCOUNT ON as the first thing in the trigger's body (after BEGIN).

Create Trigger on Parent table that creates row in Child table and assigns new Parent PK to Child as FK

I have two tables:
Customer (customerID, firstName, lastName,....)
Account (accountID, currentBalance....customerID) customerID references CUSTOMER.
The tables have a 1:M (Customer:Account), Mandatory-Mandatory relationship.
I am trying to setup a trigger that automatically creates a child row when a parent is inserted, after researching Stack and elsewhere I have managed to create a trigger on the parent that creates a row in the child:
CREATE OR REPLACE TRIGGER CMustHaveAccount
AFTER INSERT ON CUSTOMER
FOR EACH ROW
BEGIN
INSERT INTO ACCOUNT (accountID)
SELECT SEQACCOUNTID.NEXTVAL
FROM dual;
END;
/
All my attempts to set the FK in Account as the new PK in Customer are failing, I tried a number of triggers, the most promising being:
CREATE OR REPLACE TRIGGER AMustHaveCustomer
AFTER INSERT ON CUSTOMER
FOR EACH ROW
BEGIN
INSERT INTO ACCOUNT (customerID)
SELECT :new.customerID
FROM CUSTOMER;
END;
/
This trigger throws back the error
ORA-04091: table .CUSTOMER is mutating, trigger/function may not see
it
.
If I change the trigger to BEFORE, it gives error ORA-01400: cannot insert NULL into ("ACCOUNT"."ACCOUNTID"). I am assuming because technically the insert has not been completed so the PK I am inserting into Customer does not yet exist.
I want to have a trigger(s) that inserts a value into Account, with a primary key from my sequence, when a row is created in Customer, and for the PK customerID to automatically be assigned to customerID in ACCOUNT as the foreign key.
I am just learning SQL and Databases, please excuse me if the answer is obvious.
Help greatly appreciated!
The foreign key of ACCOUNT is the primary key of CUSTOMER, so this should work for you. Note the :new keyword, which is how to reference values in the current record and so avoid the "mutating table" error.
CREATE OR REPLACE TRIGGER CMustHaveAccount
AFTER INSERT ON CUSTOMER
FOR EACH ROW
BEGIN
INSERT INTO ACCOUNT (accountID, currentBalance, customerID)
values ( SEQACCOUNTID.NEXTVAL, 0, :NEW.customerID);
END;
/

mssql table multi foreign key cascade

I'm confident that this is possible but for the life of me I can't figure it out.
What I have created is a user history MSSQL table to hold the changes made to a user and by whom. This table contains two foreign keys which reference my other table (User) - one fkey for the affected user and the other fkey for the user making the changes.
What I need is for any changes to the (User) table to cascade and update the corresponding entries in this new table.
The fields in the new table (User_History) are as follows (Each user is identified by two fields):
Affected_User_House_Id - int
Affected_User_Id - int
Modified_By_User_House_Id - int
Modified_By_User_Id – int
Modification_Date - datetime
ModificationMade - ntext
Each field is a primary key except for ‘ModificationMade’. The field ‘Modification_Date’ is accurate down to 1 second.
The problem I am having is creating said cascade.
I have tried running the following T-SQL code:
ALTER TABLE [User_History] WITH CHECK
ADD CONSTRAINT [FK_User_History_User] FOREIGN KEY([Affected_User_House_Id], [Affected_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO
ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User]
GO
ALTER TABLE [User_History] WITH CHECK
ADD CONSTRAINT [FK_User_History_User_ModifiedBy] FOREIGN KEY([Modified_By_User_House_Id], [Modified_By_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO
ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User_ModifiedBy]
GO
This T-SQL gave me the following error:
*'User' table saved successfully
'User_History' table
- Unable to create relationship 'FK_User_History_User_ModifiedBy'.
Introducing FOREIGN KEY constraint 'FK_User_History_User_ModifiedBy' on table 'User_History' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.*
The code works if I remove the second “ON UPDATE CASCADE” the however that will mean the values in the fields “Modified_By_User_House_Id” and “Modified_By_User_Id” will not be updated to match their referenced values in the User table.
I am at a lost as to how to acomplish this goal.
You can only specify a single cascade. Here's an attempt to simulate multiple cascades with two triggers:
create table TabA (
ID1 int not null,
ID2 int not null,
_RowID int IDENTITY(1,1) not null,
constraint PK_TabA PRIMARY KEY (ID1,ID2),
constraint UQ_TabA__RowID UNIQUE (_RowID)
)
go
create table TabB (
ID1a int not null,
ID2a int not null,
ID1b int not null,
ID2b int not null,
constraint PK_TabB PRIMARY KEY (ID1a,ID2a,ID1b,ID2b)
)
They're simpler than your tables, but hopefully close enough. We need an immutable identifier in TabA, and obviously the IDs aren't it, since the whole point is to cascade changes to them. So I've added _RowID.
It would be nice to implement at least a real foreign key and just simulate the cascade behaviour on top of that, but some simple reflection will demonstrate that there's always a point where the FK would be broken. So we simulate it:
create trigger FK_TabB_TabA on TabB
after insert,update
as
set nocount on
if exists (
select
*
from
inserted i
left join
TabA a
on
i.ID1a = a.ID1 and
i.ID2a = a.ID2
left join
TabA b
on
i.ID1b = b.ID1 and
i.ID2b = b.ID2
where
a._RowID is null or
b._RowID is null)
begin
declare #Error varchar(max)
set #Error = 'The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "'+DB_NAME()+'", table "dbo.TabB".'
RAISERROR(#Error,16,0)
rollback
end
And then the cascading update:
create trigger FK_TabB_TabA_Cascade on TabA
after update
as
set nocount on
;with Updates as (
select
d.ID1 as OldID1,
d.ID2 as OldID2,
i.ID1 as NewID1,
i.ID2 as NewID2
from
inserted i
inner join
deleted d
on
i._RowID = d._RowID
)
update b
set
ID1a = COALESCE(u1.NewID1,ID1a),
ID2a = COALESCE(u1.NewID2,ID2a),
ID1b = COALESCE(u2.NewID1,ID1b),
ID2b = COALESCE(u2.NewID2,ID2b)
from
TabB b
left join
Updates u1
on
b.ID1a = u1.OldID1 and
b.ID2a = u1.OldID2
left join
Updates u2
on
b.ID1b = u2.OldID1 and
b.ID2b = u2.OldID2
where
u1.OldID1 is not null or
u2.OldID1 is not null
go
Some simple inserts:
insert into TabA (ID1,ID2)
values (1,1),(1,2),(2,1),(2,2)
go
insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,2)
Then the following gets an error. Not quite like a built in FK violation, but close enough:
insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,3)
--Msg 50000, Level 16, State 0, Procedure FK_TabB_TabA, Line 28
--The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "Flange", table "dbo.TabB".
--Msg 3609, Level 16, State 1, Line 1
--The transaction ended in the trigger. The batch has been aborted.
This is the update that we wanted to be able to perform:
update TabA set ID2 = ID2 + 1
And we query the FK table:
select * from TabB
Result:
ID1a ID2a ID1b ID2b
----------- ----------- ----------- -----------
1 2 2 3
So the update cascaded.
Why you can't use real FKs:
You want to have cascading updates. That means that the ID values in TabA are going to change to a new value that doesn't currently exist (caveat - we're excluding situations where 2n rows swap their identity values) - otherwise, the primary key constraint will be broken by this update.
As such, we know that the new key value will not yet exist. If we were to attempt cascading updates using an INSTEAD OF trigger (to update the child table before the parent) then the new values we attempt to update to in TabB do not yet exist. Alternately, if we attempt to do cascading updates using an AFTER trigger - well, we're too late. The FK constraint has already prevented the update.
I suppose you could implement an INSTEAD OF trigger that inserts the new rows as "duplicates", updates the children, then deletes the old rows. In such a circumstance, I think you could have real FKs. But I don't want to try writing that trigger to be right in all circumstances (e.g where you have three rows being updated. Two swap their ID values and the other creates a new ID)
According to this knowledge base article, this error message occurs when "a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement."
Since you have two paths coming from the same table, a possible workaround may involve creating a new key on the parent table and creating a single foreign key on the child ([Affected_User_House_Id], [Affected_User_Id], [Modified_By_User_House_Id], [Modified_By_User_Id]). This however, will likely create a lot of overhead. As a last resort, you can use triggers to enforce the relational integrity.

Cascade Triggers in SQLite

I've the following DB structure in SQLite:
I want to create a trigger that whenever I delete a country all the related districts, municipalities and parishes are also deleted (like MySQL InnoDB), I've tried using SQLite triggers and came up with this:
Districts:
CREATE TRIGGER [delete_country]
BEFORE DELETE
ON [countries]
FOR EACH ROW
BEGIN
DELETE FROM districts WHERE districts.id_countries = id;
END
Municipalities:
CREATE TRIGGER [delete_district]
BEFORE DELETE
ON [districts]
FOR EACH ROW
BEGIN
DELETE FROM municipalities WHERE municipalities.id_districts = id;
END
Parishes:
CREATE TRIGGER [delete_municipality]
BEFORE DELETE
ON [municipalities]
FOR EACH ROW
BEGIN
DELETE FROM parishes WHERE parishes.id_municipalities = id;
END
I haven't yet tested the delete_district and delete_municipality triggers because I get a strange behavior on the delete_country trigger: when I delete a country only the first related district gets deleted, all the others related districts remain in the table. What am I doing wrong?
The trigger looks like it is deleting districts whose id equals id_countries, that is, the where clause is actually
WHERE districts.id_countries = districts.id
You need to reference the id from the countries table. In a delete trigger, use "old" to do this.
CREATE TRIGGER [delete_country]
BEFORE DELETE
ON [countries]
FOR EACH ROW
BEGIN
DELETE FROM districts WHERE districts.id_countries = old.id;
END
Also, I would suggest changing your schema naming convention. Usually, the table name is singular, and corresponds to the entity in a row. I would have a country table with columns id and name, a district table with id, country_id and name, etc.
country
-------
id
name
district
-------
id
country_id
name
municipality
------------
id
district_id
name
parish
-----
id
municipality_id
name
Then the trigger would be
CREATE TRIGGER [delete_country]
BEFORE DELETE
ON [country]
FOR EACH ROW
BEGIN
DELETE FROM district WHERE district.country_id = old.id;
END
PRAGMA foreign_keys = 1;
This will enable the enforcement of foreign keys just like in MySQL. If you define your table with 'ON DELETE CASCADE' clause like so:
CREATE TABLE "notification" (
"rowid" INTEGER NOT NULL,
"user" INTEGER NOT NULL,
"task" TEXT NOT NULL,
"payload" TEXT NOT NULL,
PRIMARY KEY("rowid"),
FOREIGN KEY("user") REFERENCES "user" ON UPDATE CASCADE ON DELETE CASCADE
);
The whole row will be deleted when (in this case) it's parent user row is deleted.
P.S: I know this is a dead thread, but I figured I put this here for the people viewing in 2019. ;D

Resources