SQL Server 2012 foreign key constraint trigger - sql-server

I am new to triggers and I have created a trigger to check foreign key constraints across tables in different databases. I know this shouldn't be done however this is the only solution that I could find to resolve my issue with foreign key constraints. The trigger does work however it does not tell me what record caused the violation when the insert script has more than one record to insert. I am looking for a way that the trigger could tell me the record with 'x' primary key and 'y' foreign key was the one to fail. Currently, it runs thru the script comes across a violation, throws the RAISERROR, rolls back everything and nothing gets inserted in the database. Below is my script -
Create Trigger AV.fkConstraintTrigger ON [AQB_MON].[AV].[NAAQValue]
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT *
FROM INSERTED AS I
WHERE NOT EXISTS (
SELECT *
FROM [AVData].[dbo].[SourceParameterTemplate] AS A
WHERE I.[SourceParameterTemplateID] = A.[SourceParameterTemplateID]
)
)
BEGIN
RAISERROR('Violation of foreign key constraint',16,1);
ROLLBACK;
END
END
UPDATE
I made changes to the script based on the comments below because I would rather have them displayed in the message then in a separate table. However when I do I get two 'Incorrect syntax near' errors. First being the '=' and the second is the last ')'. I cannot see what would cause them.
Create Trigger AV.testfkTrigger ON [AQB_MON].[AV].[NAAQValue]
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
Declare #SourceParameterTemplateID varchar(25)
IF EXISTS (
SELECT TOP 1 #SourceParameterTemplateID = [SourceParameterTemplateID]
FROM INSERTED AS I
WHERE NOT EXISTS (
SELECT *
FROM [AVData].[dbo].[SourceParameterTemplate] AS A
WHERE I.[SourceParameterTemplateID] = A.[SourceParameterTemplateID]
)
order by [SourceParameterTemplateID]
)
BEGIN
RAISERROR('Violation of foreign key constraint',16,1, #SourceParameterTemplateID);
ROLLBACK;
END
END

Putting the data into the error message is not very useful. What would make more sense is to create and exception table that stores the data and possibly any other useful fields like the time of the problem and the user who sent in the bad data.
If you temporarily put the info into a table variable, then you can insert to the exception table after the rollback as the table variable is not rolled back.
Then your application can look up the data in the table if an error is returned from the insert/update.

Related

Update a row in one table and also creating a new row in another table to satisfy a foreign key relationship

I have a stored procedure that updates a type of Star. The database table, starValList, has a foreign key for a table called galaxyValList. That key is galaxyID.
So I need to create a new galaxyID value if it is null or empty GUID.
So I try this:
IF(#galaxyID IS NULL OR #galaxyID = '00000000-0000-0000-0000-000000000000')
BEGIN
SELECT #galaxyID=NEWID()
END
UPDATE starValList SET
[starIRR]= #starIRR,
[starDesc] = #starDesc,
[starType] = #starType,
[galaxyID]=#galaxyID
WHERE [starID] = #starID;
And it works for the starValList table!
But I think it fails too because of this error:
The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_starValList_galaxyValList". The conflict occurred in database "Astro105", table "dbo.galaxyValList", column 'galaxyID'.
It fails because there may not yet be an entry for that particular galaxy in galaxyValList table.
But I still need the row in galaxyValList because it can be used later.
How can I fix my stored procedure so that it doesn't generate this error?
Thanks!
Use if exists to check if the value exists on the table. If it does then do an update. If it doesn't then have some other logic to maybe create it or whatever your requirements may be so the value can then be used in an update. Basic example below:
IF(#galaxyID IS NULL OR #galaxyID = '00000000-0000-0000-0000-000000000000')
BEGIN
SELECT #galaxyID=NEWID()
END
if not exists ( select top 1 1 from galaxyTable where galaxyId = #galaxyId)
begin
-- the #galaxyId doesnt exist, create it so you can use the value in an update later
insert into galaxyTable ( galaxyId ) select #galaxyId
end
UPDATE starValList SET
[starIRR]= #starIRR,
[starDesc] = #starDesc,
[starType] = #starType,
[galaxyID]=#galaxyID
WHERE [starID] = #starID;

How to THROW exception in inner transaction without ending SQL Server stored procedure execution?

My objective is to throw an exception back to the caller but continue execution of the SQL Server stored procedure. So, in essence, what I'm trying to accomplish is a try..catch..finally block, even though SQL Server has no concept of a try..catch..finally block, to my knowledge.
I have a sample stored procedure to illustrate. It's just an example I came up with off the top of my head, so please don't pay too much attention to the table schema. Hopefully, you understand the gist of what I'm trying to carry out here. Anyway, the stored proc contains an explicit transaction that throws an exception within the catch block. There's further execution past the try..catch block but it's never executed, if THROW is executed. From what I understand, at least in SQL Server, THROW cannot distinguish between inner and outer transactions or nested transactions.
In this stored procedure, I have two tables: Tbl1 and Tbl2. Tbl1 has a primary key on Tbl1.ID. Tbl2 has a foreign key on EmpFK that maps to Tbl1.ID. EmpID has a unique constraint. No duplicate records can be inserted into Tbl1. Both Tbl1 and Tbl2 have primary key on ID and employ identity increment for auto-insertion. The stored proc has three input parameters, one of which is employeeID.
Within the inner transaction, a record is inserted in Tbl1 -- a new employee ID is added. If it fails, the idea is the transaction should gracefully error out but the stored proc should still continue running until completion. Whether table insert succeeds or fails, EmpID will be employed later to fill in EmpFk.
After the try..catch block, I perform a lookup of Tbl1.ID, via the employeeID parameter that's passed into the stored proc. Then, I insert a record into TBl2; Tbl1.ID is the value for Tbl2.EmpFK.
(And you might be asking "why use such a schema? Why not combine into one table with such a small dataset?" Again, this is just an example. It doesn't have to be employees. You can pick anything. It's just a widget. Imagine Tbl1 may contain a very, very large data set. What's set in stone is there are two tables which have a primary key / foreign key relationship.)
Here's the sample data set:
Tbl1
ID EmpID
1 AAA123
2 AAB123
3 AAC123
Tbl2
ID Role Location EmpFK
1 Junior NW 1
2 Senior NW 2
3 Manager NE 2
4 Sr Manager SE 3
5 Director SW 3
Here's the sample stored procedure:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[usp_TestProc]
#employeeID VARCHAR(10)
,#role VARCHAR(50)
,#location VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #employeeFK INT;
BEGIN TRY
BEGIN TRANSACTION MYTRAN;
INSERT [Tbl1] (
[EmpID]
)
VALUES (
#employeeID
);
COMMIT TRANSACTION MYTRAN;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION MYTRAN;
END;
THROW; -- Raises exception, exiting stored procedure
END CATCH;
SELECT
#employeeFK = [ID]
FROM
[Tbl1]
WHERE
[EmpID] = #employeeID;
INSERT [Tbl2] (
[Role]
,[Location]
,[EmpFK]
)
VALUES (
#role
,#location
,#employeeFK
);
END;
So, again, I still want to return the error to the caller to, i.e. log the error, but I don't wish for it to stop stored procedure execution cold in its tracks. It should continue on very similarly to a try..catch..finally block. Can this be accomplished with THROW or I must use alternative means?
Maybe I'm mistaken but isn't THROW an upgraded version of RAISERROR and, going forward, we should employ the former for handling exceptions?
I've used RAISERROR in the past for these situations and it's suited me well. But THROW is a more simpler, elegant solution, imo, and may be better practice going forward. I'm not quite sure.
Thank you for your help in advance.
What's set in stone is there are two tables which have a primary key /
foreign key relationship.
Using THROW in an inner transaction is not the way to do what you want. Judging from your code, you want to insert a new employee, unless that employee already exists, and then, regardless of whether the employee already existed or not, you want to use that employee's PK/id in a second insert into a child table.
One way to do this is to split the logic. This is psuedocode for what I mean:
IF NOT EXISTS(Select employee with #employeeId)
INSERT the new employee
SELECT #employeeFK like you are doing.
INSERT into Table2 like you are doing.
If you still need to raise an error when an #employeeId that already exists is passed, you can put an ELSE after the IF, and populate a string variable, and at the end of the proc, if the variable was populated, then throw/raise an error.

Exception after dropping table

If I execute a procedure that drops a table and then recreate it using 'SELECT INTO'.
IF that procedure raises an exception after dropping the table, does table dropping take place or not?
Unless you wrap them in a transaction,table will be dropped since each statement will be considered as an implicit transaction..
below are some tests
create table t1
(
id int not null primary key
)
drop table t11
insert into t1
select 1 union all select 1
table t11 will be dropped,even though insert will raise an exception..
one more example..
drop table orderstest
print 'dropped table'
waitfor delay '00:00:05'
select * into orderstest
from Orders
now after 2 seconds,kill session and you can still see orderstest being dropped
I checked with some other statements other than select into ,i don't see a reason why select into will behave differently and this applies even if you wrap statements in a stored proc..
IF you want to rollback all,use a transaction or more better use set xact_Abort on
Yes, the dropped table will be gone. I have had this issue when I script a new primary key. Depending on the table, it saves all the data to a table variable in memory, drops the table, creates a new one with the new pk, then loads the data. If the data violates the new pk, the statement fails and the table variable is dropped leaving me with a new table and no data.
My practice is to create the new table with a slightly different name, load the data, change both table names in a statement, then once all the data is confirmed loaded, drop the original table.

How do I make Check Constraints run before statement

I am running into a problem where my check constraints are correctly stopping commands from executing but my Identity column value increases. I guess this is because the check occurs after the statement runs and the transaction gets rolled back due to the check failing. This leaves the identity value incremented by 1.
Is there a way to run the constraint check before the SQL statement gets executed?
CREATE TABLE TestTable
(
Id INT IDENTITY(1,1) PRIMARY KEY(Id),
Name VARCHAR(100)
)
INSERT INTO TestTable VALUES ('Type-1'),('Type-2'),('Type-55'),('Type-009')
--Add a check constraint so nobody can edit this without doing serious work
ALTER TABLE TestTable WITH NOCHECK ADD CONSTRAINT [CHECK_TestTable_READONLY] CHECK(1=0)
--This fails with the constraint as expected
INSERT INTO TestTable VALUES('This will Fail')
INSERT INTO TestTable VALUES('This will again....')
--Check the Id, it was incremented...
SELECT (IDENT_CURRENT( 'TestTable' ) ) As CurrentIdentity
When I had to do the same thing in the past I created a trigger that just threw an exception on insert and delete. this has several advantages, most importantly is that it prevents updates and deletes and you could give a custom exception message explaining what you did there and why, its an extremely bad habit to just put illogical constraints and hope that 3 months from now people would understand whats going on there and know they should ask you about it. It also prevents the Id counter from being incremented if its that important. If it is important, I would also not use auto increment and just set the ID number manually, since even if you are using these triggers you could always have an accidental syntax error or any other error after you disabled them and tried to add a value.
create trigger PreventChanges
on TestTable
FOR INSERT, UPDATE, DELETE
as
begin
throw 51000, 'DO NOT change anything in that table unless you really have to! in order to do so pleasae talk to GER (or just disable and reenable this trigger)',1
and
It sounds like you're intending to use the identity column for something it's not meant for. But to answer your question, could you not just manually code up some SQL Server IF statements to test your data before the insert happens (perhaps in a stored procedure)? I wouldn't know how to make this dynamic to 'fit all constraints on any table', but the process would do what you want - prevent the INSERT from firing. Though, if your constraints change, then you would have to change the procedure too.
e.g.,
IF 1 = 0 -- or use any of your constraints here...
BEGIN
-- nest more IFs if you have multiple check-constraints...
INSERT INTO TestTable
VALUES ('This will not increase your identity number since 1 does not equal 0')
END

Referencing Columns on the Inserted Table T-SQL

So, I'm trying to create a trigger that will throw an error on inserting data if a foreign key code is not valid. I have two tables, Publisher and Title. Title has a publisher code on it, as does Publisher. I have the trigger on my Title for insert, and I am currently doing an if not exists and selecting the Publisher row where the code is equal to the inserted row's publisher code. I don't know if this is the right way to do it, and probably not, as SQL is giving me a "multi-part identifier Inserted.PublisherCode could not be found" error. Any help you guys could give would be appreciated. Thanks.
go
create trigger TR_Title_PublisherCode_Insert
on title
for Insert
as
if not exists(select * from Publisher where PublisherCode = Inserted.PublisherCode)
begin
raiserror('Publisher does not exist', 16, 1)
rollback tran
end
INSERTED and DELETED are table too
So you have to do this:
if not exists(
select * from Publisher
where PublisherCode in (SELECT PublisherCode FROM inserted)
)
By the way, as hkf said, if you identified PublisherCode AS foreign key, you won't have a need to make trigger

Resources