I have a question about constraints in SQL, to be exact in transact-sql. I have a database for trips. I created a procedure for adding new trip participant. I am using ms-sql server so foreign key and primary key constraints were added when I created tables. Now in my procedure for adding new participant for a trip I have
insert VoyageThemes(VoyageId,ThemeId) values (#voyageId,#themeId)
now, in VoyageThemes table both VoyageId and ThemeId are primary keys and foreign keys, so when I try to add new values that doesen't correspond to already existing values in database constraint raises it's head.
My question is, can I somehow check if constraint 'said' that I can't add values to table, so I can stop the procedure or I need to manually check in my database if VoyageId and ThemeId exists.
I need to know if those values exists because of this line of code:
update Voyages
set Voyages.Price=Voyages.Price+#costOfTheme*#numOfParticipants
I am updating the price of a trip, so this line of code can only excecute only if there is a corresponding VoyageId and ThemeId
I guess you can use a try/catch?:
...
BEGIN TRY
insert VoyageThemes(VoyageId,ThemeId) values (#voyageId,#themeId)
-- If we are here, then the insert succeeded, proceed with the update
update Voyages
set Voyages.Price=Voyages.Price+#costOfTheme*#numOfParticipants
...
END TRY
BEGIN CATCH
-- insert failed, check error
SELECT #error_number = ERROR_NUMBER(),
#error_severity = ERROR_SEVERITY(),
#error_state = ERROR_STATE()
IF #error_number = 547
-- constraint violation
BEGIN
PRINT '...'
END
ELSE
-- propagate error
BEGIN
RAISERROR(#error_number, #error_severity, #error_state) WITH LOG
END
END CATCH
Rather than INSERT, use MERGE to create a row only if it does not already exists e.g.
MERGE INTO VoyageThemes
USING (
VALUES (#voyageId, #themeId)
) AS S (VoyageId, ThemeId)
ON VoyageThemes.VoyageId = S.VoyageId
AND VoyageThemes.ThemeId = S.ThemeId
WHEN NOT MATCHED THEN
INSERT (VoyageId, ThemeId)
VALUES (VoyageId, ThemeId);
Related
I want to prevent further duplicates from being added to my table while allowing existing duplicates to remain. I thought I could accomplish this using a filtered index as follows.
But when I execute the following query:
CREATE UNIQUE INDEX IX_Account
ON Holdings(Account)
WHERE Account NOT IN (select Account from Holdings)
I get the following error:
Msg 1046, Level 15, State 1, Line 57
Subqueries are not allowed in this context. Only scalar expressions are allowed.
How can I prevent further duplicates from being added?
You can't have your cake and eat it.
Either
decide that your data should have integrity and purge the duplicated before adding the unique index (filtering it for the reason you mention does not make sense)
or
enforce your logic with an insert trigger:
create trigger no_more_duplicates on Holdings
after insert as
if exists
(
select 1
from inserted
where inserted.Account IN (select Account from Holdings)
)
raiserror('Cannot add duplicates',16,0)
end -- trigger
This trigger's a bit dumb, it will not prevent duplicates on a multiple-row insert, nor will it let the nonduplicate ones be saved. Yet, it's enough that you get the picture.
raiserror in a trigger will not automatically rollback the transaction, but throw will. Alternatively you can raiserror and rollback.
Also with an AFTER trigger the data in the INSERTED virtual table is already present in the table. So a trigger would need to look like:
use tempdb
drop table if exists Holdings
create table Holdings(id int primary key, Account int)
go
create or alter trigger no_more_duplicates on Holdings
after insert as
begin
if exists
(
select 1
from inserted
where inserted.Account IN (select Account from Holdings where id <> inserted.id)
)
begin
throw 60000, 'Cannot add duplicates', 1 ;
--raiserror('Cannot add duplicates',16,1)
end;
end -- trigger
go
insert into Holdings(id,Account) values (1,1)
go
insert into Holdings(id,Account) values (2,1)
go
select * from holdings
I can detect duplicate records, but when I'm inserting new data it will detect it as a duplicate record even if doesn't already exist.
Here is my code:
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
DECLARE #Year float,
#Month float,
#SDPGroup nvarchar(255);
SELECT
#Year = i.InvYear, #Month = i.InvMonth, #SDPGroup = i.SDPGroup
FROM inserted i;
IF (SELECT COUNT(*) FROM SDPRawInventory A
WHERE A.InvYear = #Year
AND A.InvMonth = #Month
AND A.SDPGroup = #SDPGroup) >= 1
BEGIN
RAISERROR ('Duplicate data', 16, 1)
ROLLBACK;
END
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This is the table
And to clarify there is no primary key nor unique identifier.
If you are unable to put a constraint in place, then you need to handle the fact that Inserted may have multiple records. And because its an after insert trigger, you don't need to do anything if no duplicates are found because the records are already inserted.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
-- But ensure the duplicate is a *different* record - assumes a unique ID
AND S.ID <> I.ID
)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
END;
Note the simplified and modern error handling.
EDIT: And if you have no unique key, and no permission to add one, then you need an instead of trigger to only insert non-duplicates e.g.
ALTER TRIGGER [dbo].[SDPRawInventory_Dup_Trigger]
ON [dbo].[SDPRawInventory]
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Reject the entire insert if a single duplicate exists
-- Note if multiple records are inserted, some of which are duplicates and some of which aren't, they all get rejected
IF EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
INNER JOIN Inserted I ON
-- Test for a duplicate
A.InvYear = I.InvYear
AND A.InvMonth = I.InvMonth
AND A.SDPGroup = I.#SDPGroup
)
-- Test that Inserted itself doesn't contain duplicates
OR EXISTS (SELECT 1 FROM Inserted GROUP BY InvYear, InvMonth, SDPGroup HAVING COUNT(*) > 1)
BEGIN
THROW 51000, 'Duplicate data.', 1;
END;
INSERT INTO dbo.SDPRawInventory (SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty)
SELECT SDP_SKU_DESC, WholeQty, InvYear, InvMonth, SDPGroup, invUOM, LooseQty
FROM Inserted I
WHERE NOT EXISTS (
SELECT 1
FROM dbo.SDPRawInventory S
-- Test for a duplicate
WHERE S.InvYear = I.InvYear
AND S.InvMonth = I.InvMonth
AND S.SDPGroup = I.SDPGroup
);
END;
Note: This doesn't do anything to handle existing duplicates.
This trigger is executed after the new records were inserted, so it will at least find the original records in the SELECT COUNT statement. Changing >= 1 into >= 2 can only partially fix this when inserting is guaranteed to occur one record as a time. Moreover, it will still fail when there were already multiple duplicated of the newly inserted record in the database before the insert.
You need to exclude the latest inserted records from the COUNT. But a better idea would probably be to add a UNIQUE constraint for preventing duplicates, so no trigger would be necessary.
If adding a constraint is not possible yet, you should initiate a clean-up process to eliminate the existing duplicates beforehand. Everything else is looks pretty flawed to me, since it is unlikely the current approach will ever bring the table into a state with no duplicates.
You are creating the infinite loop. You just have to remove the insertion part from your trigger.
ELSE
BEGIN
INSERT INTO SDPRawInventory
SELECT * FROM inserted;
END
This should not be in the trigger as trigger is called as part of insertion. you should not write actual insertion in to table in trigger.
Im trying to make this trigger work when trying to delete a record. The way it is suposed to work is, when someone tries to delete a record it rollbacks and inserts an audit record to TbAudit table which by the way, all columns have a NOT NULL constraint. However, turns out it wont do it, because for some reason I dont understand when I try to delete a record it will display the message and rollback BUT all my variables within the select statement are getting NULL values even though Im pulling them directly from the "deleted" table. Please help.
USE BdPlan
GO
CREATE TRIGGER TrAudit
ON Plan.TPlan
AFTER DELETE
AS
BEGIN
DECLARE #IdPlan = int,
#IdEmployee int,
#Month int,
#Year int
ROLLBACK
PRINT 'CANT DELETE RECORDS'
-- All variables are getting NULL
SELECT #IdPlan = D.IdPlan,
#IdEmployee = D.IdEmployee ,
#Month = D.Month,
#Year = D.Year
FROM deleted AS D
INSERT INTO BdAudit.dbo.TbAudit
VALUES
(
#IdPlan,
#IdEmployee,
#Month,
#Year,
SUSER_NAME(),
GETDATE()
)
END
I believe there may be problems with this approach:
you are trying to access the DELETED pseudotable after the transaction has been rolled back - it will have zero rows after the rollback (see below)
your trigger only attempts to deal with a single row deletion - it should be able to handle multi row deletes
It is also noted that inserting directly into the Audit table from the Deleted pseudotable before ROLLBACK will of course roll the audit data back as well.
From here it is apparent you can cache the data to be audited in a #Temporary table variable, then do the ROLLBACK (which doesn't undo the #Temp table), and then do the Audit insertion:
ALTER trigger d_foo ON FOO AFTER DELETE
AS BEGIN
DECLARE #Temp AS TABLE
(
ID INT,
-- Obviously add all your fields go here
);
INSERT INTO #Temp(ID)
SELECT ID FROM DELETED;
ROLLBACK TRAN;
insert into fooaudit(id)
select id from #Temp;
END;
Simplified SqlFiddle here with multiple row deletion.
To confirm, the DELETED pseudotable contains zero rows after a ROLLBACK in a trigger, as this modified Fiddle demonstrates.
Here is a trigger
CREATE TRIGGER [dbo].[CheckApplyId]
ON [dbo].[AppliedStudent_event] INSTEAD OF INSERT
AS
DECLARE #studentId INT
DECLARE #compReq_Id INT
BEGIN
SELECT #studentId = studentId
FROM INSERTED
SELECT #compReq_Id = compReq_Id
FROM INSERTED
IF EXISTS(SELECT StudentId,
compreq_id
FROM AppliedStudent_event
WHERE StudentId = #studentId
AND compreq_id = #compReq_Id)
BEGIN
ROLLBACK
PRINT 'User Already Applied'
END
END
When in insert a data into a table using command:
INSERT INTO AppliedStudent_event (StudentId, compreq_id)
VALUES (3026, 1)
Message is:
(1 row(s) affected)
But when I execute a sql command no data is inserted in the table.
Can you please tell why are you using trigger because you use only assign the variable #studentId and #compReq_Id from inserted table.
That's a broken trigger because inserted can contain multiple (or no) rows - so a statement like SELECT #ScalarVariable = column from inserted is always wrong.
And it's unnecessary since you can just place a UNIQUE constraint on the StudentId and compreq_id columns:
ALTER TABLE AppliedStudent_event
ADD CONSTRAINT UQ_Student_Events
UNIQUE (StudentId,compreq_id)
And it's further broken because you've specified it as an instead of trigger - that says that your code is going to be responsible for the actual insert - but your code doesn't actually do that. That's why no data ends up in the table.
If you insist on doing it as a trigger, it's actually tricky to get everything correct (that's why I'd really recommend the UNIQUE constraint). It'll end up being something like this:
CREATE TRIGGER [dbo].[CheckApplyId]
ON [dbo].[AppliedStudent_event] INSTEAD OF INSERT
AS
IF EXISTS(select
StudentId,compreq_id,COUNT(*)
from inserted
group by StudentId,compreq_id
HAVING COUNT(*) > 1)
OR EXISTS (select *
from inserted i
inner join
AppliedStudent_event e
on
i.StudentId = e.StudentId and
i.compreq_id = e.compreq_id)
BEGIN
ROLLBACK
PRINT 'User Already Applied'
END
ELSE
BEGIN
INSERT INTO AppliedStudent_event(StudentId,compreq_id /* Other columns? */)
SELECT StudentId,compreq_id /* And again, other columns */
FROM inserted
END
Suppose I have 3 tables in my database like this:
Person (personid, ...) ---this is entity, personid is identity column
Phone (phoneid, ...) ---this is entity, phoneid is identity column
PersonPhone (personid, phoneid) ---this relationship
When I insert data into tables, I need to insert rows into the entity tables first and get the generated id, and then I need to insert a row into the relationship table.
It's working fine. Question is: I have a stored procedure with transaction with Try Catch statement like:
BEGIN TRY
declare aCursor cursor local fast_forward for (Select Query...)
open aCursor;
fetch next from aCursor into #variables....
while ##fetch_status = 0
begin
INSERT INTO Person(...);
set #personid =##IDENTITY;
INSERT INTO Phone(...);
set #phoneid =##IDENTITY;
INSERT INTO PersonPhone(#personid, #phoneid);
end;
END TRY
BEGIN CATCH
close aCursor;
deallocate selectdistributor;
SELECT ERROR_NUMBER() AS ErrorNumber,ERROR_MESSAGE() AS ErrorMessage;
ROLLBACK TRAN;
RETURN;
END CATCH
COMMIT;
close aCursor;
deallocate aCursor;
With the cursor, there is more the one records will be inserted for person, phone. When run the SP, it will stop for first time data insert on relationship. Also I can get right #personid, #phoneid, but before the transaction is done, I get error on INSERT INTO PersonPhone(#personid, #phoneid); as:
The INSERT statement conflicted with the FOREIGN KEY constraint "Person_PersonPhone_FK1". The conflict occurred in database "MYDB", table "dbo.Person", column 'PersonID'.
Looks like Id generated by system not recognized before transaction done.
How to resolve this problem?
Likely your problem is that you used ##identity which should underr no circumstances ever be used for this purpose. If one of the tables has a trigger, it is possoble to return the wrong value (the identity from the table the trigger is inserting into) and that value coincidentally does not exist in the table that the foreign key links to.
So suppose you havea trigger onteh first table that iunserts to an audit table with an identity. The record is inserted with an ID of 1234. BUt the tirigger inserts to a differnt table and so value retiurned by ##Identity is 5678 which does not exist inteh orginal table. When you go to insert to the table with the FK back on the first table, you get theerror message because value 5678 does not exist in the first table.
Use the OUTPUT clause instead. You could also use scope_identity(), but output is more flexible (And lets you do multiple record inserts and receive all the identities back as well as the other fields which might uniquely identify a record.
And I'm with Aaron, I would not use a cursor for this unless someone threatened my life.