Using Instead-Of SQL triggers to insert or update a view - sql-server

I want to write a trigger to the view, VW_BANKBRANCH:
If the inserted row contains a bankcode that exists in the table, then update the
bName column of bank table with the inserted data
If not, insert rows to bank table to reflect the new information.
But my trigger is not working..
My tables
CREATE TABLE bank(
code VARCHAR(30) PRIMARY KEY,
bName VARCHAR(50)
);
CREATE TABLE branch(
brNum INT PRIMARY KEY,
brName VARCHAR(50),
braddress VARCHAR(50),
bcode VARCHAR(30) REFERENCES bank(code)
);
CREATE VIEW VW_BANKBRANCH
AS
SELECT code,bname,brnum,brName
FROM bank ,branch
WHERE code=bcode
My trigger
CREATE TRIGGER tr_VW_BANKBRANCH_INSERT ON VW_BANKBRANCH
INSTEAD OF INSERT
AS
BEGIN
DECLARE #insertedBankCode INT
#insertedbname varchar
#insertedbrnum int
#insertedbrName varchar
SELECT #insertedBankCode = code
FROM INSERTED
IF(#insertedBankCode=code)
SET code=#insertedBankCode
bname=#insertedbname
brnum=#insertedbrnum
brName=#insertedbrName
ELSE
insert(code,bname,brnum,brName)
END

I've adapted the instead of trigger on the view below - I'm assuming you want to upsert both bank and branch accordingly (although note that the branch address is not currently in the view).
That said, I would be careful of (ab)using an instead of trigger on an INSERT to do upserts - this might not be entirely intuitive to the reader.
Also, remember that the INSERTED pseudo table could contain a SET of rows, so needs to be adjusted to set based approach accordingly.
CREATE TRIGGER tr_VW_BANKBRANCH_INSERT ON VW_BANKBRANCH
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE b
SET bname = i.bname
FROM bank b
INNER JOIN inserted i
ON i.code = b.code;
UPDATE br
SET
br.brName = i.brName,
br.braddress = NULL -- TODO add this to the view
FROM branch br
INNER JOIN inserted i
ON br.bcode = i.code
AND br.brNum = i.brNum;
INSERT INTO bank(code, bname)
SELECT code, bname
FROM inserted i
WHERE NOT EXISTS
(SELECT 1 FROM bank b WHERE b.code = i.Code);
INSERT INTO Branch(brNum, brName, braddress, bcode)
SELECT brNum, brName, NULL, code
FROM inserted i
WHERE NOT EXISTS
(SELECT 1
FROM branch br
WHERE br.bcode = i.Code AND br.brNum = i.brNum);
END;
GO
SqlFiddle here - I've also adjusted the view to use a JOIN, rather than the old style of WHERE joins.
If you have SqlServer 2008 or later, you could also use MERGE instead of separate inserts and updates.

Related

How can I use a trigger to allow an incremented, user-assigned ID?

I am moving a small database from MS Access into SQL Server. Each year, the users would create a new Access database and have clean data, but this change will put data across the years into one pot. The users have relied on the autonumber value in Access as a reference for records. That is very inaccurate if, say, 238 records are removed.
So I am trying to accommodate them with an id column they can control (somewhat). They will not see the real primary key in the SQL table, but I want to give them an ID they can edit, but still be unique.
I've been working with this trigger, but it has taken much longer than I expected.
Everything SEEMS TO work fine, except I don't understand why I have the same data in my INSERTED table as the table the trigger is on. (See note in code.)
ALTER TRIGGER [dbo].[trg_tblAppData]
ON [dbo].[tblAppData]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #NewUserEnteredId int = 0;
DECLARE #RowIdForUpdate int = 0;
DECLARE #CurrentUserEnteredId int = 0;
DECLARE #LoopCount int = 0;
--*** Loop through all records to be updated because the values will be incremented.
WHILE (1 = 1)
BEGIN
SET #LoopCount = #LoopCount + 1;
IF (#LoopCount > (SELECT Count(*) FROM INSERTED))
BREAK;
SELECT TOP 1 #RowIdForUpdate = ID, #CurrentUserEnteredId = UserEnteredId FROM INSERTED WHERE ID > #RowIdForUpdate ORDER BY ID DESC;
IF (#RowIdForUpdate IS NULL)
BREAK;
-- WHY IS THERE A MATCH HERE? HAS THE RECORD ALREADY BEEN INSERTED?
IF EXISTS (SELECT UserEnteredId FROM tblAppData WHERE UserEnteredId = #CurrentUserEnteredId)
BEGIN
SET #NewUserEnteredId = (SELECT Max(t1.UserEnteredId) + 1 FROM tblAppData t1);
END
ELSE
SET #NewUserEnteredId = #CurrentUserEnteredId;
UPDATE tblAppData
SET UserEnteredId = #NewUserEnteredId
FROM tblAppData a
WHERE a.ID = #RowIdForUpdate
END
END
Here is what I want to accomplish:
When new record(s) are added, it should increment values from the Max existing
When a user overrides a value, it should check to see the existence of that value. If found restore the existing value, otherwise allow the change.
This trigger allows for multiple rows being added at a time.
It is great for this to be efficient for future use, but in reality, they will only add 1,000 records a year.
I wouldn't use a trigger to accomplish this.
Here is a script you can use to create a sequence (op didn't tag version), create the primary key, use the sequence as your special id, and put a constraint on the column.
create table dbo.test (
testid int identity(1,1) not null primary key clustered
, myid int null constraint UQ_ unique
, somevalue nvarchar(255) null
);
create sequence dbo.myid
as int
start with 1
increment by 1;
alter table dbo.test
add default next value for dbo.myid for myid;
insert into dbo.test (somevalue)
select 'this' union all
select 'that' union all
select 'and' union all
select 'this';
insert into dbo.test (myid, somevalue)
select 33, 'oops';
select *
from dbo.test
insert into dbo.test (somevalue)
select 'oh the fun';
select *
from dbo.test
--| This should error
insert into dbo.test (myid, somevalue)
select 3, 'This is NO fun';
Here is the result set:
testid myid somevalue
1 1 this
2 2 that
3 3 and
4 4 this
5 33 oops
6 5 oh the fun
And at the very end a test, which will error.

EF4 RowCount issue on instead of insert trigger while updating an other table

I have some trouble with entityFramework 4. Here is the thing :
We have a SQL server database. Every table have 3 instead of triggers for insert, update and delete.
We know EntityFramework has some issues to deal with theses triggers, that's why we added the following code at the end of triggers to force the rowCount :
for insert :
DECLARE #Identifier BIGINT;
SET #Identifier = scope_identity()
SELECT #Identifier AS Identifier
for update/delete :
CREATE TABLE #TempTable (temp INT PRIMARY KEY);
INSERT INTO #TempTable VALUES (1);
DROP TABLE #TempTable
It worked fine until now :
From an instead of insert trigger (let's say table A), I try to update a field of an other table (table B)
I know my update code perfectly work since a manual insert does the work. The issue shows up only when I'm using Entity framework.
I have the solution now, let's make a school case of this with a full example. :)
In this example, our application is an addressBook. We want to update the business Activity (IsActive column in Business)
everytime we add, update or delete a contact on this business. The business is considered as active if at least one of the contact
of the business is active. We record every state changements on the business in a table to have the full history.
So, we have 3 tables :
table Business (Identifier (PK Identity), Name, IsActive),
table Contact (Identifier (PK Identity), Name, IsActive, IdentifierBusiness)
table BusinessHistory (Identifier (PK Identity), IsActive, Date, IdentifierBusiness)
Here's are the triggers one we are interested in :
table Contact (trigger IoInsert):
-- inserting the new rows
INSERT INTO Contact
(
Name
,IsActive
,IdentifierBusiness
)
SELECT
t0.Name
,t0.IsActive
,t0.IdentifierBusiness
FROM
inserted AS t0
-- Updating the business
UPDATE
Business
SET
IsActive = CASE WHEN
(
(t0.IsActive = 1 AND Business.IsActive = 1)
OR
(t0.IsActive = 1 AND Business.IsActive = 0)
) THEN 1 ELSE 0
FROM
inserted AS t0
WHERE
Business.Identifier = t0.IdentifierBusiness
AND
t0.IsActive = 1
AND
Business.IsActive = 0
-- Forcing rowCount for EntityFramework
DECLARE #Identifier BIGINT;
SET #Identifier = scope_identity()
SELECT #Identifier AS Identifier
Table Business (trigger IoUpdate)
UPDATE
Business
SET
IsActive = 1
FROM
Contact AS t0
WHERE
Business.Identifier = t0.IdentifierBusiness
AND
t0.IsActive = 1
AND
Business.IsActive = 0
---- Updating BusinessHistory
INSERT INTO BusinessHistory
(
Date
,IsActive
,IdentifierBusiness
)
SELECT
DATE()
,t0.IsActive
,t0.Identifier
FROM
inserted AS t0
INNER JOIN
deleted AS t1 ON t0.Identifier = t1.Identifier
WHERE
(t0.Identifier <> t1.Identifier)
-- Forcing rowCount for EntityFramework
CREATE TABLE #TempTable (temp INT PRIMARY KEY);
INSERT INTO #TempTable VALUES (1);
DROP TABLE #TempTable
Table BusinessHistory :
-- Updating the business
UPDATE
Business
SET
IsActive = CASE WHEN
(
(t0.IsActive = 1 AND Business.IsActive = 1)
OR
(t0.IsActive = 1 AND Business.IsActive = 0)
) THEN 1 ELSE 0
FROM
inserted AS t0
WHERE
Business.Identifier = t0.IdentifierBusiness
AND
t0.IsActive = 1
AND
Business.IsActive = 0
-- inserting the new rows
INSERT INTO BusinessHistory
(
Date
,IsActive
,IdentifierBusiness
)
SELECT
DATE()
,t0.IsActive
,t0.Identifier
FROM
inserted AS t0
-- Forcing rowCount for EntityFramework
DECLARE #Identifier BIGINT;
SET #Identifier = scope_identity()
SELECT #Identifier AS Identifier
So, in a nutshell, what happened ?
We have 2 tables, Business and Contact. Contact is updating table Business on insert and update.
When Business is updated, it does an insert into BusinessHistory, which is storing the history of updates of table Business
,when the field IsActive is updated.
the thing is, even if I don't insert a new row in BusinessHistory, I launch an insert instruction and so, I go inside the instead of insert trigger of the table BusinessHistory. Of course, in the end of this one, there is a scope_identity(). You can use scope_identity only once, and it gives back the last identity inserted.
So, since I did not inserted any BusinessHistory, it was consuming the scope_identity of my newly inserted contact : the scope_identity of the instead of
insert of the contact table was empty !
How to isolate the issue ?
Using the profiler, you figure out that there are insert instruction in BusinessHistory when it should not be any of them.
Using the debugging, you will eventually end in the an insert trigger your are not supposed to be in.
How to fix it ?
Several alternatives here. What I did was to surround in table Business the insert of BusinessHistory by an If condition :
I want the insert to be inserted only if the statut "IsActive" has changed :
IF EXISTS
(
SELECT
1
FROM
inserted AS t0
INNER JOIN
deleted AS t1 ON t0.Identifier = t1.Identifier
WHERE
(t0.IsActive <> t1.IsActive)
)
BEGIN
INSERT INTO BusinessHistory
(
Date
,IsActive
,IdentifierBusiness
)
SELECT
DATE()
,t0.IsActive
,t0.Identifier
FROM
inserted AS t0
INNER JOIN
deleted AS t1 ON t0.Identifier = t1.Identifier
WHERE
(t0.IsActive <> t1.IsActive)
END
An other possibility is, in the trigger instead of insert of the table BusinessHistory, to surround the whole trigger by an IF EXISTS condition
IF EXISTS (SELECT 1 FROM inserted)
BEGIN
----Trigger's code here !
END
How to avoid it ?
Well, use one of these fixes !
Avoiding scope_identity(), ##IDENTITY is more than enough in most of the cases ! In my company, we only use scope_identity because of EF 4 !
I know my english is not perfect, I can edit if it's not good enough, or if someone want to add something on this subject !

trigger for insert or update after checking relationship

I have this 3 tables:
And i need to build a trigger that: A date ("encontro") can only works when theres a friendship ("amizade") between 2 profiles ("perfis").
I've created this trigger but i feel lost.. HELP ME
CREATE TRIGGER relaƧoes_after_insert
ON encontros
INSTEAD OF insert -
as
begin
declare #idperfilA int;
declare #idperfilB int;
declare #data datetime;
declare #count int;
declare cursor_1 cursor for select * from inserted;
open cursor_1;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
WHILE ##FETCH_STATUS = 0
BEGIN
if exists( select * from inserted i, amizade a
where i.IDPERFILA = a.IDPERFILA and i.IDPERFILB = a.IDPERFILB and GETDATE() > DATA)
RAISERROR('there isnt friendship', 16, 10);
else
insert into ENCONTROS select * from inserted;
end;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
END
close cursor_1;
deallocate cursor_1;
I think the better answer would be to not create use a trigger for this at all. Instead I would create and enforce a foreign key constraint between encontros and amizade.
As far as I can tell, this will result in doing what you want without having to write your own code to try and recreate behavior provided by the database. It also makes it much easier to understand from a database design point of view.
alter table dbo.encontros
add constraint fk_amizade__encontros
foreign key (idperflia, idperflib) references dbo.amizade (idperflia, idperflib)
/* optional
on delete { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
on update { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
--*/*
;
More about table constraints.
NO ACTION
The SQL Server Database Engine raises an error and the delete action on the row in the parent table is rolled back.
CASCADE
Corresponding rows are deleted from the referencing table if that row is deleted from the parent table.
SET NULL
All the values that make up the foreign key are set to NULL when the corresponding row in the parent table is deleted. For this constraint to execute, the foreign key columns must be nullable.
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
Based on your reply to #3N1GM4:
#3N1GM4 if exists some friendship with a date after today (for example) it is an error, so the friendship doesnt exist. But i dont know if it matters at this point. IDPERFILA and IDPERFILB will match A and B at amizade table, but i need to make sure that they were not the same
You could create a check constraint on amizade that will prevent rows with invalid dates from being inserted into the table.
alter table dbo.amizade
add constraint chk_data_lt_getdate ([data] < get_date());
More about check constraints; more examples from Gregory Larson.
original answer:
I'm still waiting on some clarification on the question, but one of the versions in this should be on the right path:
create trigger relaƧoes_after_insert
on encontros
instead of insert
as
begin
/* To abort when any row doesn't have a matching friendship */
if not exists (
select 1
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
begin;
raiserror('there isnt friendship', 16, 10);
else
insert into encontros
select * from inserted;
end;
end;
/* To insert all rows that have a matching friendship, you could use this instead */
insert into encontros
select i.*
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
end;
The only potential issue I see with using an inner join instead of exists for the second option (inserting rows that have a friendship and ignoring ones that don't) is if there could ever be an issue where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb) or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila) would return duplicates of the inserted rows from each condition returning a match.

Looping table valued parameter values passed as parameter to stored procedure for checking a value

I have a stored procedure that receives a TVP as input. Now, I need to check the received data for a particular ID in a primary key column. If it exists, then I just need to update the table using those new values for other column (sent via TVP). Else, do an insert.
How to do it?
CREATE PROCEDURE ABC
#tvp MyTable READONLY
AS
IF EXISTS (SELECT 1 FROM MYTAB WHERE ID= #tvp.ID)
DO update
ELSE
Create
Just wondering the if exists loop I did is correct. I reckon its wrong as it will only check for first value and then update. What about other values? How should I loop through this?
Looping/CURSOR is the weapon of last resort, always search for solution that is SET based, not ROW based.
You should use MERGE which is designed for this type of operation:
MERGE table_name AS TGT
USING (SELECT * FROM #tvp) AS SRC
ON TGT.id = SRC.ID
WHEN MATCHED THEN
UPDATE SET col = SRC.col
WHEN NOT MATCHED THEN
INSERT (col_name, col_name2, ...)
VALUES (SRC.col_name1, SRC.col_name2, ...)
If you don't like MERGE use INSERT/UPDATE:
UPDATE table_name
SET col = tv.col,
...
FROM table_name AS tab
JOIN #tvp AS tv
ON tab.id = tv.id
INSERT INTO table_name(col1, col2, ...)
SELECT tv.col1, tv.col2, ...
FROM table_name AS tab
RIGHT JOIN #tvp AS tv
ON tab.id = tv.id
WHERE tab.id IS NULL

Unable to set value based on INSERTED table in trigger

GOAL:I'm creating after insert trigger that should insert new record to OrderSuspendRule table based on rule in this table that was related with Promotion of which new version was created.
PROBLEM
I cannot set value to #SUS_ID. Select returns value but it isn't set to variable.
Sample insert:
INSERT INTO PromotionHeader (Guid,CreatedAt,UpdatedAt,IsActive,CompanyId,UpdatedById,CreatedById,Name,[Description],ValidFrom,ValidTo,BusinessUnitId,OfferId,[Version],StatusId,PreviousId)
select newid(),CreatedAt,UpdatedAt,1,CompanyId,UpdatedById,CreatedById,Name,[Description],ValidFrom,ValidTo,BusinessUnitId,OfferId,[Version]+1,StatusId,916 FROM PromotionHeader WHERE Id=916
Where PreviousId points to older version of promotion.
CREATE TRIGGER TRIG1 ON DBO.PromotionHeader
AFTER INSERT
AS
DECLARE #SUS_ID INT
SET #SUS_ID = (
SELECT Max(id)
FROM OrderSuspendRule
WHERE PromotionHeaderId = (
SELECT PreviousId
FROM inserted
WHERE ID = SCOPE_IDENTITY()
)
AND ISACTIVE=1
)
IF (#SUS_ID IS NOT NULL) --**VARIABLE IS ALWAYS NULL NO MATTER WHAT**
BEGIN
INSERT INTO OrderSuspendRule (
Guid
,CreatedAt
,UpdatedAt
,IsActive
,CompanyId
,UpdatedById
,CreatedById
,SuspendFrom
,SuspendTo
,PromotionHeaderId
,SuspendTypeId
,OfferItemId
)
SELECT NEWID()
,GETDATE()
,GETDATE()
,1
,CompanyId
,UpdatedById
,CreatedById
,SuspendFrom
,SuspendTo
,SCOPE_IDENTITY()
,SuspendTypeId
,OfferItemId
FROM OrderSuspendRule
WHERE id = #SUS_ID
END
Inside a for insert trigger, you can assume that all rows in the inserted table were inserted. There is no need to double check this with scope_identity().
To explain why scope_identity() is null, remember that scope_identity() returns the last inserted identity in the current scope. Since your trigger runs in its own scope, this will always be null, unless the trigger itself performs an insert.
Also, be aware that your trigger can be run for an insert of multiple rows. That means you can't expect only a single #sus_id, there might be many.

Resources