How to handle multiple rows in this Sql Server Trigger? - sql-server

I have the following trigger, but because a trigger needs to handle multiple records, I'm not sure how to correctly handle this, in my trigger code.
Can someone please suggest how I can change the TSql below to correctly handle multiple records, instead of just a single record (as is listed, below).
Table Schema and defaults.
CREATE TABLE [dbo].[tblArticle](
[IdArticle] [int] IDENTITY(1,1) NOT NULL,
[IdArticleStatus] [tinyint] NOT NULL,
[Title] [nvarchar](200) NOT NULL,
[CleanTitle] [nvarchar](300) NOT NULL,
[UniqueTitle] [nvarchar](300) NOT NULL,
[Content] [nvarchar](max) NOT NULL
GO
ALTER TABLE [dbo].[tblArticle] ADD CONSTRAINT [DF_tblArticle_CleanTitle]
DEFAULT (newid()) FOR [CleanTitle]
GO
ALTER TABLE [dbo].[tblArticle] ADD CONSTRAINT [DF_tblArticle_UniqueTitle]
DEFAULT (newid()) FOR [UniqueTitle]
GO
Trigger, which only handles a single record ... not multiple.
ALTER TRIGGER [dbo].[ArticlesAfterInsertOrUpdate]
ON [dbo].[tblArticle]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #IdArticle INTEGER,
#Title NVARCHAR(300),
#CleanTitle NVARCHAR(300),
#UniqueTitle NVARCHAR(300),
#NewCleanTitle NVARCHAR(300),
#CleanTitleCount INTEGER
-- Only Update the CleanTitle and UniqueTitle if *required*
-- This means, create a unique subject of the title, then check if this clean value
-- is different to the current clean value. If so, then update both clean and unique.
-- Otherwise, don't do anything (because it will include this row in the count check, below).
IF UPDATE(Title) BEGIN
-- TODO: How will this handle multiple records???
SELECT #IdArticle = IdArticle, #Title = Title, #CleanTitle = CleanTitle
FROM INSERTED
-- Create the 'Slugs'.
SET #NewCleanTitle = dbo.CreateUniqueSubject(#Title)
SET #UniqueTitle = #NewCleanTitle
IF #NewCleanTitle != #CleanTitle BEGIN
-- We need to update the clean and unique, so lets get started...
-- Grab the count :: eg. how many other _clean_ titles already exist?
-- Note: this is the _only_ reason why we have this
-- column - because it has an index on it.
SELECT #CleanTitleCount = COUNT(IdArticle)
FROM [dbo].[tblArticle]
WHERE CleanTitle = #NewCleanTitle
-- If we have some previous titles, then we need to append a number
-- to the end of the current slug.
IF #CleanTitleCount > 0
SET #UniqueTitle = #NewCleanTitle + CAST((#CleanTitleCount + 1) AS VARCHAR(10))
-- Now update the unique subject field.
UPDATE [dbo].[tblArticle]
SET CleanTitle = #NewCleanTitle,
UniqueTitle = #UniqueTitle
WHERE IdArticle = #IdArticle
END
END
END
GO
Please help!

Don't really need to know what the custom function does, just that it returns the same value for each given input (i.e. the Title). It gets a bit complicated to perform this type of logic in a trigger, but you can certainly make it happen. There are definitely other ways of making it work as well, best approach would depend entirely on your environment, however the following logic will get you what you're looking for as a starting point:
ALTER TRIGGER [dbo].[ArticlesAfterInsertOrUpdate]
ON [dbo].[tblArticle]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON
-- Only Update the CleanTitle and UniqueTitle if *required*
-- This means, create a unique subject of the title, then check if this clean value
-- is different to the current clean value. If so, then update both clean and unique.
-- Otherwise, don't do anything (because it will include this row in the count check, below).
IF UPDATE(Title) BEGIN
-- Materialize with the newCleanTitle value for simplicity sake, could
-- do this inline below, not sure which would work better in your environment
if object_id('tempdb..#tempIData') > 0
drop table #tempIData;
select *,
dbo.CreateUniqueSubject(i.Title) as newCleanTitle
into #tempIData
from inserted i
where i.CleanTitle <> dbo.CreateUniqueSubject(i.Title);
with iData as
( -- Get the data inserted along with a running tally of any duplicate
-- newCleanTitle values
select i.IdArticle as IdArticle,
i.CleanTitle, i.newCleanTitle,
-- Need to get the count here as well to account for cases where
-- we insert multiple records with the same resulting cleanTitle
cast(row_number() over(partition by i.newCleanTitle order by i.IdArticle) as bigint) as cntCleanTitle
from #tempIData i
),
srcData as
( -- Get the existing count of data by CleanTitle value for each
-- newCleanTitle included in the inserted data
select t.CleanTitle as CleanTitle,
cast(coalesce(count(*),0) as bigint) as cntCleanTitle
from dbo.tblArticle t
join
( -- Need a distinct list of newCleanTitle values
select a.newCleanTitle
from iData a
group by a.newCleanTitle
) i
-- Join on CleanTitle as we need to get the existing running
-- count for each distinct CleanTitle values
on t.CleanTitle = i.newCleanTitle
group by t.CleanTitle
)
-- Do the update...
update a
set a.CleanTitle = i.newCleanTitle,
a.UniqueTitle =
case
when i.cntCleanTitle + coalesce(s.cntCleanTitle,0) > 1
then i.newCleanTitle + cast((cast(i.cntCleanTitle as bigint) + cast(coalesce(s.cntCleanTitle,0) as bigint)) as nvarchar(10))
else
i.newCleanTitle
end
from dbo.tblArticle a
join iData i
on a.IdArticle = i.IdArticle
left join srcData s
on i.newCleanTitle = s.CleanTitle;
if object_id('tempdb..#tempIData') > 0
drop table #tempIData;
END
END

Related

Issue with SQL INSERT Trigger

I'm reaching out for some help on this trigger I'm trying to get working.
Basically this is what I'm trying to do.
We have DMS software that writes to a Database and on a particular INSERT value I want the trigger to fire.
This is an example of an INSERT statement that will get processed.
INSERT INTO DOCSADM.ACTIVITYLOG (CR_IN_USE,ACTIVITY_DESC,BILLED_ON,BILLABLE,PAGES,KEYSTROKES,
TYPE_TIME,ELAPSED_TIME,TYPIST,AUTHOR,START_DATE,ACTIVITY_TYPE,REF_DOCUMENT,REF_LIBRARY,APPLICATION,VERSION_LABEL,DOCNUMBER,SYSTEM_ID)
VALUES ('','DOCSFusion','1753-01-01','',0,0,0,0,1920,1920,'2020-08-26T10:17:56',**115**,0,-1,1173,'',75,3252)
but I only want the trigger to fire when we see a value of 115 for the bold section in the INSERT statement (the Activity_type value).
For all other values that re not 115 I don't want to do anything.
This is what I have so far:
CREATE TRIGGER BW_TRIGGER
ON DOCSADM.ACTIVITYLOG
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--Declare some variable and set it as a value of 115.
--Example:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
--Next, you should have a fork or path in your trigger to determine how it proceeds.
--Path 1: The #AlogType value matches the inserted value so you want to process the rest of the trigger. Example path – “ProcessTrigger:”
--Path 2: The #AlogType value does NOT match the inserted value, you want to exit the trigger. Example Path – “ExitTrigger:”
IF #AlogType <> 115
GOTO TriggerExit;
ELSE
Begin
/*Create first temp table to collect insert values*/ --This table will have the SysID Value and the corresponding docnumber for the items you want.
--You can add whatever other values you think you need.
CREATE TABLE #TempSet1
(
AlogsysID INT,
Docnum INT,
AlogDate Varchar(64),
AlogTypist INT,
AlogAuthor INT,
AlogDesc varchar(32),
ALOGVER varchar(10),
ALOG_MATTER INT
)
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
select top 1 System_id, docnumber, LAST_ACCESS_DATE, TYPIST, AUTHOR, MATTER from docsadm.PROFILE where EXISTS (SELECT CLIENT.SYSTEM_ID FROM DOCSADM.CLIENT INNER JOIN DOCSADM.MATTER ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc OR INH_LUP_SEC_FROM IS NULL OR INH_LUP_SEC_FROM = 0) AND MATTER=#AlogDesc
/*Set variable #SysID as the LASTKEY value -1. This will be used to set the SysID column on the #TempSet table*/
--DECLARE #SysID INT = (SELECT LASTKEY FROM DOCSADM.SEQ_SYSTEMKEY) -1;
/*Set the SysID value for every row on the #TempSet1 table as the #SysID variable +1*/
--UPDATE #TempSet1
--SET #SysID = AlogsysID = #SysID + 1
--Your #TempSet should now be set with ALL of the System_IDs and Docnumbers necessary for your insert!!!!—
--Verify this by doing a select against the #TempSet1 Table
SELECT * FROM #TempSet1;
--Next you need to set the SystemID to the correct value for future processing. To do this, we need to get a total count from the #TempSet table.
/*Set a variable to update the NEXTKEY value on the DOCSADM.SEQ_SYSTEMKEY table. The NEXTKEY value is used for the SYSTEM_ID field*/
--DECLARE #SeqUpdateCount INT = (SELECT COUNT(*) FROM #TempSet1);
/*Update the LASTKEY Value on the SEQ_SYSTEMKEY table to the next available value for DM.*/
--UPDATE DOCSADM.SEQ_SYSTEMKEY SET LASTKEY = LASTKEY+#SeqUpdateCount
--If you have all the values you need in your temp table, you can now insert them into the ACTIVITYLOG table.
--INSERT INTO DOCSADM.ACTIVITY
--(SYSTEM_ID, DOCNUMBER, START_DATE, version, EXT,)
--SELECT
--AlogSysID,Docnum,GETUTCDATE(),BLAH, BLAH
--FROM #TableSet1
INSERT INTO DOCSADM.ACTIVITYLOG
(SYSTEM_ID,
DOCNUMBER,
START_DATE,
TYPIST,
AUTHOR,
ACTIVITY_DESC,
VERSION_LABEL,
ACTIVITY_TYPE)
SELECT
AlogsysID, Docnum,AlogDate,AlogTypist, AlogAuthor, ALOG_MATTER, '',115
FROM #TempSet1;
--Now you need to Drop the Temp Table
DROP TABLE #TempSet1
--Go to the other half of your path above to exit the trigger.
END
TriggerExit:
END
Go
but when I try to run any INSERT statement on this table I get this error message. It doesn't matter if the activity_type has a value of 115 or not
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I know the issue is with this section of the trigger:
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
SELECT TOP 1
System_id
, docnumber
, LAST_ACCESS_DATE
, TYPIST
, AUTHOR
, MATTER
FROM docsadm.PROFILE
WHERE EXISTS (SELECT CLIENT.SYSTEM_ID
FROM DOCSADM.CLIENT
INNER JOIN DOCSADM.MATTER
ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc
OR INH_LUP_SEC_FROM IS NULL
OR INH_LUP_SEC_FROM = 0)
AND MATTER=#AlogDesc
It's the SELECT statement that is causing it to fail.
I know that this statement will bring back multiple rows but I only need the value from one of them so I can use this value for my INSERT. I though having the "select top 1" would do this for me but it's not working like I think it should. What am I missing?
If I had to guess I would say your problem is here:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
How are ACTIVITYLOG and INSERTED joined in the above ? without a where it would be a CROSS JOIN. Why do you even drag ACTIVITYLOG into it, you can simply use INSERTED. Also please try to stop using implicit joins ( I can see that later down the script you use the proper, more verbose join syntax)
TRY:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from INSERTED I)
Be careful that this will work with single inserts only. When you do batched inserts the INSERTED is a table containing multiple rows and you will run into issues again.

Insert and update multiple records via same stored procedure

I created this stored procedure to go through all the records in the table comparing the id (primary key) if exists and the records changed, make the necessary changes & update the record.
If the id is not in the table then insert the record. This stored procedure
compiles fine, but doesn't seem to work properly. Does this need a while loop?
ALTER PROCEDURE [dbo].[SMLineUpdate]
(
#id [int],
#Payroll_Id [int],
#ProductCode nvarchar(255),
#Description nvarchar (255),
#Qty nvarchar(255)
)
AS
IF EXISTS (SELECT Id from Smline where #id = Id) BEGIN
update dbo.SmLine
Set [Payroll_Id] = #Payroll_Id
, ProductCode = #ProductCode
, Description = #Description
, Qty = #Qty
END ELSE BEGIN
INSERT INTO SmLine ([Payroll_Id], [ProductCode], [Description], [Qty])
VALUES (#Payroll_Id, #ProductCode, #Description, #Qty)
END
Your update query is missing a where condition
update dbo.SmLine
Set [Payroll_Id] = #Payroll_Id
,ProductCode = #ProductCode
,Description = #Description
,Qty = #Qty
WHERE Id = #Id -- the query missed this where condition
IF EXISTS(SELECT Id from Smline where Id =#id)
BEGIN
update dbo.SmLine
Set [Payroll_Id]= #Payroll_Id
,ProductCode= #ProductCode
,Description = #Description
,Qty = #Qty
WHERE Id = #Id
END
ELSE
BEGIN
INSERT INTO SmLine ([Payroll_Id],[ProductCode],[Description],[Qty])
VALUES (#Payroll_Id,#ProductCode ,#Description,#Qty)
END
Your SP does not meet the requirement of insert multiple records. It works only for a single record update or inserts, you have to pass multiple id's and values respectively for update multiple so use a different approach like XML as an input parameter so u can simply do this operation for multiple by extracting the XML data.
Your update statement lacks a where statement. That is a major 'no-no', as it will (god forbid...) update all lines in the table.
Your insert statement lacks an identity insert, so consider the case where you are trying to update/insert id=5, but by now this line is deleted (not found in the where), and ids are much bigger. you would search for it -- > not find, and insert a new line (say id=101), then look for id=5 again, not find it again, and insert it again (say id=102), and so on... I don't think that's what you intended. Consider a Merge statement (when matched/when not matched) and get the best of both worlds. Also consider not deleting from the table, and instead add an 'IsDeleted' column (which allows 'reviving' a deleted row).

How to prevent insertion of cyclic reference in SQL

I have the following table:
create table dbo.Link
(
FromNodeId int not null,
ToNodeId int not null
)
Rows in this table represent links between nodes.
I want to prevent inserts or updates to this table from creating a cyclic relationship between nodes.
So if the table contains:
(1,2)
(2,3)
it should not be allowed to contain any of the following:
(1,1)
(2,1)
(3,1)
I'm happy to treat (1,1) separately (e.g. using a CHECK CONSTRAINT) if it makes the solution more straightforward.
I was thinking of creating an AFTER INSERT trigger with a recursive CTE (though there may be an easier way to do it).
Assuming this is the way to go, what would the trigger definition be? If there is a more elegant way, what is it?
Note first that it is preferable to detect cycles in another environment as recursive CTEs aren't known for their good performance and neither is a trigger that would run for each insert statement. For large graphs, a solution based on the solution below will likely be inefficient.
Suppose you create the table as follows:
CREATE TABLE dbo.lnk (
node_from INT NOT NULL,
node_to INT NOT NULL,
CONSTRAINT CHK_self_link CHECK (node_from<>node_to),
CONSTRAINT PK_lnk_node_from_node_to PRIMARY KEY(node_from,node_to)
);
That would block inserts with node_from equal to node_to, and for rows that already exist.
The following trigger should detect cyclic references by throwing an exception if a cyclic reference is detected:
CREATE TRIGGER TRG_no_circulars_on_lnk ON dbo.lnk AFTER INSERT
AS
BEGIN
DECLARE #cd INT;
WITH det_path AS (
SELECT
anchor=i.node_from,
node_to=l.node_to,
is_cycle=CASE WHEN i.node_from/*anchor*/=l.node_to THEN 1 ELSE 0 END
FROM
inserted AS i
INNER JOIN dbo.lnk AS l ON
l.node_from=i.node_to
UNION ALL
SELECT
dp.anchor,
node_to=l.node_to,
is_cycle=CASE WHEN dp.anchor=l.node_to THEN 1 ELSE 0 END
FROM
det_path AS dp
INNER JOIN dbo.lnk AS l ON
l.node_from=dp.node_to
WHERE
dp.is_cycle=0
)
SELECT TOP 1
#cd=is_cycle
FROM
det_path
WHERE
is_cycle=1
OPTION
(MAXRECURSION 0);
IF #cd IS NOT NULL
THROW 67890, 'Insert would cause cyclic reference', 1;
END
I tested this for a limited number of inserts.
INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,2); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,4); -- OK
And
INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- PK violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,1); -- Check constraint violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,2); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,1); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,1); -- Exception: Insert would cause cyclic reference
It also detects cyclic references already present in the inserted rows if inserting more than one row at once, or if a path longer than one edge would be introduced in the graph. Going off on the same initial inserts:
INSERT INTO dbo.lnk(node_from,node_to)VALUES(8,9),(9,8); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,5),(5,6),(6,1); -- Exception: Insert would cause cyclic reference
EDIT: handle multi-record inserts, moved logic in separate function
I have considered a procedural approach, it is very fast and almost independent from number of records in link table and graph "density"
I have tested it on a table with 10'000 links with nodes values from 1 to 1000.
It is really really fast an do not suffer of link table dimension or "density"
In addition, the function could be used to test values before insert or (for example) if you don't want to use a trigger at all an move test logic to the client.
Consideration on Recursive CTE: BE CAREFUL!
I have tested the accepted answer on my test table (10k rows) but after 25 minutes, I have cancelled the insert operation of one single row because the query was hung with no result...
Downsizing the table to 5k rows the insert of a single record can last up to 2-3 minutes.
It is very dependant from the "population" of the graph. If you insert a new path, or you are adding a node to a path with low "ramification" it is quite fast but you have no control over that.
When the graph will be more "dense" this solution will blow up in your face.
Consider your needs very carefully.
So, let's see how to..
First of all I have set the PK of table to both columns and added an index on second column for full coverage. (the CHECK on FromNodeId<>ToNodeId is not needed cause the algorithm already cover this case).
CREATE TABLE [dbo].[Link](
[FromNodeId] [int] NOT NULL,
[ToNodeId] [int] NOT NULL,
CONSTRAINT [PK_Link] PRIMARY KEY CLUSTERED ([FromNodeId],[ToNodeId])
)
GO
CREATE NONCLUSTERED INDEX [ToNodeId] ON [dbo].[Link] ([ToNodeId])
GO
Then I have built a function to test the validity of a single link:
drop function fn_test_link
go
create function fn_test_link(#f int, #t int)
returns int
as
begin
--SET NOCOUNT ON
declare #p table (id int identity primary key, l int, t int, unique (l,t,id))
declare #r int = 0
declare #i int = 0
-- link is not self-referencing
if #f<>#t begin
-- there are links that starts from where new link wants to end (possible cycle)
if exists(select 1 from link where fromnodeid=#t) begin
-- PAY ATTENTION.. HERE LINK TABLE ALREADY HAVE ALL RECORDS ADDED (ALSO NEW ONES IF PROCEDURE IS CALLED FROM A TRIGGER AFTER INSERT)
-- LOAD ALL THE PATHS TOUCHED BY DESTINATION OF TEST NODE
set #i = 0
insert into #p
select distinct #i, ToNodeId
from link
where fromnodeid=#t
set #i = 1
-- THERE IS AT LEAST A STEP TO FOLLOW DOWN THE PATHS
while exists(select 1 from #p where l=#i-1) begin
-- LOAD THE NEXT STEP FOR ALL THE PATHS TOUCHED
insert into #p
select distinct #i, l.ToNodeId
from link l
join #p p on p.l = #i-1 and p.t = l.fromnodeid
-- CHECK IF THIS STEP HAVE REACHED THE TEST NODE START
if exists(select 1 from #p where l=#i and t=#f) begin
-- WE ARE EATING OUR OWN TAIL! CIRCULAR REFERENCE FOUND
set #r = -1
break
end
-- THE NODE IS STILL GOOD
-- DELETE FROM LIST DUPLICATED ALREADY TESTED PATHS
-- (THIS IS A BIG OPTIMIZATION, WHEN PATHS CROSSES EACH OTHER YOU RISK TO TEST MANY TIMES SAME PATHS)
delete p
from #p p
where l = #i
and (exists(select 1 from #p px where px.l < p.l and px.t = p.t))
set #i = #i + 1
end
if #r<0
-- a circular reference was found
set #r = 0
else
-- no circular reference was found
set #r = 1
end else begin
-- THERE ARE NO LINKS THAT STARTS FROM TESTED NODE DESTINATIO (CIRCULAR REFERENCE NOT POSSIBLE)
set #r = 1
end
end; -- link is not self-referencing
--select * from #p
return #r
end
GO
Now let's call it from a trigger.
If more than a row will be inserted the trigger will test each link against the whole insert (old table + new recs), if all are valid and the final table will be consistent the insert will complete, if one of them is not valid the insert will abort.
DROP TRIGGER tr_test_circular_reference
GO
CREATE TRIGGER tr_test_circular_reference ON link AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
declare #p table (id int identity primary key, l int, f int, t int)
declare #f int = 0
declare #t int = 0
declare #n int = 0
declare #i int = 1
declare #ins table (id int identity primary key, f int, t int)
insert into #ins select * from inserted
set #n = ##ROWCOUNT;
-- there are links to insert
while #i<=#n begin
-- load link
select #f=f, #t=t from #ins where id = #i
if dbo.fn_test_link(#f, #t)=0 begin
declare #m nvarchar(255)
set #m = formatmessage('Insertion of link (%d,%d) would cause circular reference (n.%d)', #f, #t, #i);
THROW 50000, #m, 1
end
set #i = #i + 1
end
END
GO
I hope this will help

TSQL variable object reference not set to instance of an object

I'm trying to create a trigger that initialises a record over multiple tables when a master record is inserted to a master table. The code below gives the error "Object reference not set to an instance of an object."
Appraisal is the table the trigger exists for and every table concerned has an AppraisalID foreign key.
BEGIN TRANSACTION
DECLARE #appraisalid int
SET #appraisalid = (SELECT AppraisalID FROM inserted)
INSERT INTO dbo.AppraisalCPD (AppraisalID)
VALUES (#appraisalid)
COMMIT;
The code below works, but I would rather use a variable to assign the value as I need to add rows to a fair few tables.
BEGIN
INSERT INTO dbo.AppraisalCPD
(AppraisalID)
SELECT AppraisalID
FROM inserted;
END;
If anyone can suggest a way to set an appraisalid variable using the inserted row in the Appraisal table to insert rows into every other table I need to, that'd be very helpful.
I am assuming you are talking about home appraisal.
The [appraisal] table is the parent and the [owners]/[location] are the children.
Here is a play schema in tempdb.
-- just playing
use tempdb;
go
-- drop table
if object_id('appraisal') > 0
drop table appraisal
go
-- create table - home appraisal
create table appraisal
(
app_id int identity (1,1),
app_request datetime,
app_employee_id int
);
-- drop table
if object_id('owners') > 0
drop table owners
go
-- create table - the owners
create table owners
(
own_id int identity (1,1) primary key,
own_first_nm varchar(64),
own_last_nm varchar(64),
app_id int
);
-- drop table
if object_id('location') > 0
drop table location
go
-- the location
create table location
(
loc_id int identity (1,1) primary key,
loc_street varchar(64),
loc_city varchar(64),
loc_state varchar(2),
loc_zip varchar(9),
app_id int
);
go
When creating empty child records via a trigger, you either have to define default values or supply them in the trigger.
Also, I leave setting up foreign keys for you to handle.
The code for the trigger could look like the following.
-- The trigger
CREATE TRIGGER DBO.make_empty_children on dbo.appraisal
for insert, update, delete
as
BEGIN
-- nothing to do?
IF (##rowcount = 0) RETURN;
-- do not count rows
SET NOCOUNT ON;
-- delete
IF NOT EXISTS (SELECT * FROM inserted)
BEGIN
RETURN;
END
-- insert
ELSE IF NOT EXISTS (SELECT * FROM deleted)
BEGIN
-- dummy record for owners
insert into owners
(
own_first_nm,
own_last_nm,
app_id
)
select
'enter first name',
'enter last name',
app_id
from
inserted;
-- dummy record for location
insert into location
(
loc_street,
loc_city,
loc_state,
loc_zip,
app_id
)
select
'enter street',
'enter city',
'ri',
'00000',
app_id
from
inserted;
RETURN;
END
-- update
ELSE
BEGIN
RETURN;
END
END
GO
I left place holders for DELETE and UPDATE operations.
1 - Do you want to reject updates to the id (key) in the appraisal table. Probably not. This can also be taken care (prevented) of by a Foreign Key (FK).
2 - Do you want to cascade deletes? This also can be handled by a FK or in code in the trigger.
Lets see no records in the tables.
select * from appraisal;
select * from owners;
select * from location;
Like Marc / Aaron said, the inserted / deleted tables are record sets. Not a single row.
Order is not guaranteed. Use an order by if you want the records inserted by app id order.
-- Insert 2 records
insert into appraisal
(
app_request,
app_employee_id
)
values
(getdate(), 7),
(dateadd(d, 1, getdate()), 7);
-- Lets see the data
select * from appraisal;
select * from owners;
select * from location;

Why is the natural ID generation in this SQL Stored Proc creating duplicates?

I am incrementing the alphanumeric value by 1 for the productid using stored procedure. My procedure incrementing the values up to 10 records, once its reaching to 10th say for PRD0010...no more its incrementing... however, the problem is it is repeating
the same values PRD0010.. for each SP call.
What could be the cause of this?
create table tblProduct
(
id varchar(15)
)
insert into tblProduct(id)values('PRD00')
create procedure spInsertInProduct
AS
Begin
DECLARE #PId VARCHAR(15)
DECLARE #NId INT
DECLARE #COUNTER INT
SET #PId = 'PRD00'
SET #COUNTER = 0
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
--here increse the vlaue to numeric id by 1
SET #NId = #NId + 1
--GENERATE ACTUAL APHANUMERIC ID HERE
SET #PId = #PId + cast(#NId AS VARCHAR)
INSERT INTO tblProduct(id)values (#PId)
END
Change
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
To
SELECT TOP 1
#NId = cast(substring(id, 4, len(id)) as int)
FROM tblProduct order by LEN(id) DESC, ID DESC
You have to remember that
PRD009
is always greater than
PRD0010
or
PRD001
All in all, I think your approach is incorrect.
Your values will be
PRD00
PRD001
...
PRD009
PRD0010
PRD0011
...
PRD0099
PRD00100
This will make sorting a complete nightmare.
In addition to astander's analysis, you also have a concurrency issue.
The simple fix would be to add this at the beginning of your proc:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
And add a COMMIT at the end. Otherwise, two callers of this stored proc will get the same MAX/TOP 1 value from your table, and insert the same value.
Also, you can and should prevent these duplicates from existing by adding a key to your table, for this column. If you already have a PRIMARY KEY on this table, you can add an additional key using a UNIQUE constraint. This will prevent duplicates occurring in the future, no matter what programming errors occur. E.g.
ALTER TABLE tblProduct ADD CONSTRAINT UQ_Product_ID UNIQUE (ID)

Resources