INSERT INTO View, INSTEAD OF Trigger, Identity, multiple tables? - sql-server

Here as my tables (Entier = Integer // Caractère long variable = Varchar) :
http://i.stack.imgur.com/lNjyy.jpg
I created a view V_Enterprise(idContact, phoneNumber, email, name, city, adress)
I tried to create a Trigger on that View to allow users to update the view :
CREATE TRIGGER test
ON V_Entreprise
INSTEAD OF INSERT
AS
DECLARE #T_ContactId INT
BEGIN
INSERT INTO T_Contact
SELECT i.phoneNumber, i.email
FROM Inserted i
SELECT #T_ContactId = ##IDENTITY
INSERT INTO T_Entreprise
SELECT #T_ContactId, i.name, i.city, i.adress
FROM Inserted i
END ;
As I expected, it work on simple inserts, but when I add couples of rows at once, it fails because #T_ContactId only contains the first id. Can someone help me to fix it ? I feel like I should use INNER JOIN inserts but I can't figure out how to deal with it.

OK you should never set scalar variables to a value in inserted or delted in a trigger.
Use the OUTPUT clause instead to get your id values back.

This trigger uses a loop over a cursor and won't require any particular uniqueness in the tables;
CREATE TRIGGER test
ON V_Enterprise
INSTEAD OF INSERT
AS
BEGIN
DECLARE #name VARCHAR(32)
DECLARE #city VARCHAR(32)
DECLARE #address VARCHAR(32)
DECLARE #pn VARCHAR(32)
DECLARE #email VARCHAR(32)
DECLARE cursor1 CURSOR FOR
SELECT name,city,address,phoneNumber,email FROM inserted;
OPEN cursor1;
FETCH NEXT FROM cursor1 INTO #name, #city, #address, #pn, #email;
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO T_Contact (phoneNumber,email) VALUES (#pn, #email);
INSERT INTO T_Enterprise (idcontact,name,city,address) VALUES
(##IDENTITY,#name,#city,#address);
FETCH NEXT FROM cursor1 INTO #name, #city, #address, #pn, #email;
END
CLOSE cursor1;
DEALLOCATE cursor1;
END
GO

I don't know if this is a good way to do it, but you can do this without relying on unique columns or using a cursor using the OUTPUT clause for INSERT. This approach does make use of an in-memory temporary table that could get big with large inserts.
DECLARE #Table table( NewID BIGINT);
INSERT INTO T_Contact (PhoneNumber)
OUTPUT Inserted.ID
INTO #Table
SELECT PhoneNumber FROM inserted WHERE
;
INSERT INTO T_Enterprise (Contact_ID)
SELECT NewID FROM #Table;

If phoneNumber and email are a unique key in T_Contact then you could do this:
CREATE TRIGGER test
ON V_Entreprise
INSTEAD OF INSERT
AS
DECLARE #T_ContactId INT
BEGIN
INSERT INTO T_Contact
SELECT i.phoneNumber, i.email
FROM Inserted i
SELECT #T_ContactId = ##IDENTITY
INSERT INTO T_Entreprise
SELECT
(SELECT idContact FROM T_Contact
WHERE phoneNumber = i.phoneNumber AND email = i.email),
i.name, i.city, i.adress
FROM Inserted i
END ;

Related

Loop through a list of Ids and perform a dynamic insert statement

I want to loop through a list of country IDs and perform an insert statement for each one.
I need a loop where each country ID is accessible as a variable in the loop that I can concatenate into a dynamic SQL query
CREATE TABLE [dbo].countryIds(
CountryId INT IDENTITY(1,1)
)
SET IDENTITY_INSERT [dbo].countryIds ON
INSERT [dbo].countryIds (CountryId) VALUES (8)
INSERT [dbo].countryIds (CountryId) VALUES (13)
SET IDENTITY_INSERT [dbo].countryIds OFF
WHILE EXISTS (SELECT CountryId FROM CountryIds)
BEGIN
-- INSERT INTO anotherTable custom sql where country ID = CountryId
END
I tried a while exists loop, but this loops infinitely.
How can I achieve this?
Try to use cursor instead while exists https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql
CREATE TABLE [dbo].countryIds(
CountryId INT IDENTITY(1,1)
)
SET IDENTITY_INSERT [dbo].countryIds ON
INSERT [dbo].countryIds (CountryId) VALUES (8)
INSERT [dbo].countryIds (CountryId) VALUES (13)
SET IDENTITY_INSERT [dbo].countryIds OFF
declare #CountryId int
declare country_cursor cursor for
select CountryId from CountryIds
open country_cursor
fetch next from country_cursor
into #CountryId
while ##fetch_status = 0
begin
--INSERT INTO anotherTable custom sql where country ID = CountryId
fetch next from country_cursor
into #CountryId
end
close country_cursor
deallocate country_cursor
Using loops in SQL is a bad practice, work with multiple rows:
INSERT INTO anotherTable (...)
SELECT ...
FROM anotherTable a
INNER JOIN countryIds
WHERE countryID = CountryId
First of all I recommend manual transactions to see if everything goes fine before doing actual commit:
BEGIN TRAN;
SQL
ROLLBACK or COMMIT;
Now to your question:
This should be done via cursor (the following is a simple cursor example)
DECLARE #CountryIds int
-- the declaration of the cursor itselfdeclare a cursor
DECLARE cursor_for_insert CURSOR FOR
SELECT CountryId FROM CountryIds
-- open the cursor and fetch first row into variable
OPEN cursor_for_insert
FETCH NEXT FROM insert_cursor INTO CountryIds
-- run till there is a row to get
WHILE ##FETCH_STATUS=0
BEGIN
-- do your insert here
-- INSERT INTO anotherTable custom sql where country ID = CountryId
END
-- don't forget to close it an unallocate it otherwise it will be still allocated!
CLOSE cursor_for_insert
DEALLOCATE cursor_for_insert
GO

MERGE in SQL Server

I am trying to perform the following using a MERGE statement
I have a table that has two columns (TagId is an Identity (PK) and Name as a VARCHAR). I would like to check first if Name exists before I insert it. If it does exist, I would like to get the Identity value. Otherwise, I would insert it and pick up the inserted value.
The beauty of MERGE is it is transactional in nature. so, I won't have to worry about getting an UNIQUE index violation due to timing. Any suggestions? I prefer not to use transactions.
DECLARE
#TagId INT,
#Name VARCHAR(100) = 'TagName'
SELECT TOP(1)
#TagId = T.TagId
FROM dbo.Tag AS T
WHERE T.Name = #Name
IF #TagId IS NULL
BEGIN
INSERT dbo.Tag (Name) VALUES (#Name)
SELECT #TagId = SCOPE_IDENTITY()
END
After trying, this seems to work: it doesn't seem right. The MATCHED clause is required. Otherwise, #Table won't have value.
DECLARE #Table TABLE
(
TagId INT,
Name VARCHAR(100)
)
DECLARE
#TagId INT,
#Name VARCHAR(100) = 'TdagNamed122'
MERGE dbo.Tag AS Target
USING(SELECT #Name) AS Source (Name)
ON Source.Name = Target.Name
WHEN MATCHED THEN
UPDATE SET #TagId = Target.TagId
WHEN NOT MATCHED THEN
INSERT (Name) VALUES (Source.Name)
OUTPUT INSERTED.* INTO #Table
;
SELECT * FROM #Table
Yes as per the documentation , it should execute all statements in atomic fashion
I didn't face any concurrency issues
But There are some concerns as per the link
http://www.mssqltips.com/sqlservertip/3074/use-cauti

tsql looping issue, duplicate records are being inserted

DECLARE #tag VARCHAR(MAX)
DECLARE #TagID as INT;
DECLARE tag_cursor CURSOR
FOR
SELECT tagname FROM #temptag
FOR READ ONLY
OPEN tag_cursor
FETCH NEXT FROM tag_cursor INTO #tag
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT * FROM Tag WHERE TagName=#tag)
BEGIN
SELECT #TagID = TagID FROM Tag WHERE TagName = #tag
Insert into NoteTags(NoteID,TagID) values (#NoteID,#TagID)
END
ELSE
BEGIN
INSERT INTO Tag
SELECT #tag FROM #temptag
SELECT #TagID = SCOPE_IDENTITY();
Insert into NoteTags(NoteID,TagID) values (#NoteID,#TagID)
END
FETCH NEXT FROM tag_cursor INTO #tag
END
CLOSE tag_cursor
DEALLOCATE tag_cursor
I am passing parameters to procedure using XML, I have created a temporary table and stored all values from XML into it. And then I have written Cursor to check if value already exists in the table or not.
If value is not available records will be inserted.
Problem: If I send two values from XML say IND, USA which doesn't exist in my table, duplicate records are being inserted in the table.
Can anyone tell what mistake I made with my code.
After modifying..
BEGIN
INSERT INTO Tag(TagName) values(#tag);
SELECT #TagID = IDENT_CURRENT('Tag');
Insert into NoteTags(NoteID,TagID) values (#NoteID,#TagID)
END
Here's a set based answer to avoid the cursor:-
insert into tag (tagname)
select tt.tagname
from #temptag tt
where not exists(
select *
from tag t
where t.tagname=tt.tagname
)
insert into notetags (noteid,tagid)
select #noteid,t.tagid
from tag t
where exists(
select *
from #temptag tt
where tt.tagname=t.tagname
)
and not exists(
select *
from notetags nt
where nt.noteid=#noteid
and nt.tagid=t.tagid
)
There is no clue in your code where #noteid gets set.
This code is causing your problem:-
INSERT INTO Tag
SELECT #tag FROM #temptag
You already have the value of #tag - but the select is duplicating it for every row in #temptag so you end up with duplicate rows in Tag
remove the select and change the insert to:-
insert into tag (TagName) values (#tag)
I think the better idea would to not use the cursor. How about doing an outer join between the temporary table and the Tag table and only do an insert where the tag entry is null?
I find it pretty rare that you'd ever need to use a cursor.

Stored procedure inserting values into a table using Table-Valued Parameters

Given the following simple structure:
TABLE: Product (ProductId, ProductName)
TABLE: Category (CategoryId, CategoryName)
LINK TABLE: ProductId,CategoryId
I have a table type which I want to pass to a stored procedure to insert the values into another table if they don't exist.
CREATE TYPE StringList_TBLType AS TABLE (s NVARCHAR(255) NOT NULL PRIMARY KEY)
I want to do the following in a stored procedure where I pass in a ProductName, and the StringList_TBLType of Category Names
select all the strings from my StringList_TBLType
Insert the string into the Category TABLE if it does not exist
Get the ID of the inserted or already existing Category
Insert the ProductId, and CategoryId into the LINK TABLE.
I could probably struggle along and get something working, but I have little experience with MS SQL, and stored procedures in general, and am scared that I would end up writing a very inefficient way of doing this.
You can use the MERGE statement to capture the category IDs.
DECLARE #changes TABLE (ChangeID VARCHAR(10), Id INTEGER);
DECLARE #JustSomeRandomVariable INT;
MERGE Category AS TARGET
USING #data AS SOURCE
ON TARGET.Category = SOURCE.s
WHEN NOT MATCHED THEN
INSERT ([Category])
VALUES (SOURCE.s)
WHEN MATCHED THEN
UPDATE SET #JustSomeRandomVariable = 1
OUTPUT $action, inserted.Id INTO #changes;
The random variable in the merge statement makes sure that updates get logged into the #changes table variable.
Now you can use the #changes to update your link table.
INSERT INTO Link SELECT ProductID, ChangeID FROM #changes
Just retrieve the required ProductID with a simple select query.
EDIT:
This could potentially result in double records in the Link table. You might need to tweak it a bit, perhaps use the MERGE statement for inserting into the Link table aswell.
#data is the StringList_TBLType paramter of your procedure.
This is what I would suggest (not tested)
CREATE PROCEDURE AddProductToCategories
#productname nvarchar(255),
#categories StringList_TBLType READONLY
AS
BEGIN
DECLARE #productId bigint --change to datatype of Product.ProductId
SET #productId = (SELECT TOP 1 ProductId FROM Product WHERE ProductName = #productname) --What to do if your product names are not unique?
IF #productId is not NULL
BEGIN
DECLARE cur CURSOR FOR (
SELECT cat.CategoryName, cat.CategoryId, c.s
FROM #categories c
RIGHT OUTER JOIN Category cat on c.s = cat.CategoryName
)
DECLARE #id as bigint --change to datatype of Category.CategoryId
DECLARE #name as nvarchar(255) --change to datatype of Category.CategoryName
DECLARE #categoryNameToAdd as nvarchar(255)
OPEN cur
FETCH NEXT FROM cur INTO #name, #id, #categoryNameToAdd
WHILE ##FETCH_STATUS = 0
BEGIN
IF #id is NULL
BEGIN
--category does not exist yet in table Category
INSERT INTO Category (CategoryName) VALUES (#categoryNameToAdd)
SET #id = SCOPE_IDENTITY()
END
INSERT INTO ProductsCategories --your link table
(ProductId, CategoryId)
VALUES
(#productId, #id)
FETCH NEXT FROM cur INTO #name, #id, #categoryNameToAdd
END --cursor
CLOSE cur
DEALLOCATE cur
END --IF #productId
END --sproc

SQL Server unique row insertion from stored procedure does not work

I have a page on our intranet that submits requests to a perl CGI script. That script in turn calls a stored procedure on a SQL Server DB that check whether an object with certain attributes exists. If it does, the storproc returns the instrument's id, if it doesn't, it creates a new instrument and returns that new instrument's id. The stored procedure creates a transaction, and also uses with (TABLOCKX) in the insert statement. For user-friendliness, when said user submits a bunch of requests simultaneously, the web pages submits the requests to the perl script asynchronously. I thought that when several requests are submitted that all require a new instrument, the first one to hit the storproc would run, lock the table, create the new instrument, release the lock, and then the subsequent calls to the storproc would be aware of the new instrument and use that. What I saw in practice was that there would be a couple of requests that create the new instrument, and the rest would use the most recent one. I tried using a setTimeout on the client side to space out the requests, but that doesn't seem to make a difference. Any ideas as to what I may be doing wrong?
Here is the code of the stored procedure:
CREATE PROCEDURE [dbo].[CreateFutures]
#code varchar(5),
#month int,
#year int,
#currency varchar(3)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
declare #ticker varchar(7)
declare #yearCode char(1)
declare #res as Table (id int)
declare #n as int
set #yearCode = convert(char(1), #year % 10)
set #ticker = (
select #code + futures + #yearCode
from FuturesMonthCodes
where month = #month
)
insert into #res
select top 1 instrument
from InstrumentFutures // This is a view that joins InstrumentText and InstrumentNumber data
where ticker = #ticker
and code = #code
and month = #month
and year = #year
and currency = #currency
order by instrument
set #n = (select COUNT(id) from #res)
if #n = 0
begin
print 'Creating Future'
declare #id int
declare #stamp datetime
set #stamp = CURRENT_TIMESTAMP
insert into Instrument with (TABLOCKX) (insertTime) values (#stamp)
set #id = (select SCOPE_IDENTITY());
insert into InstrumentText (instrumentId, name, value) values (#id, 'type', 'futures')
insert into InstrumentText (instrumentId, name, value) values (#id, 'ticker', #ticker)
insert into InstrumentText (instrumentId, name, value) values (#id, 'code', #code)
insert into InstrumentText (instrumentId, name, value) values (#id, 'currency',#currency)
insert into InstrumentNumber (instrumentId, name, value) values (#id, 'month', #month)
insert into InstrumentNumber (instrumentId, name, value) values (#id, 'year', #year)
insert into #res (id) values (#id)
end
commit transaction
if #n = 0 --instrument created
select top 1 id, 1 from #res order by id
else --returning existing instrument
select top 1 id, 0 from #res order by id
END
It is more sql problem than perl.
Let say 3 scripts try to run this stored proc in the same time.
The first execute and it is locked the table. The others waited for the table to unlock but they not reread the data when the locking is over so they used old data.
If your stored proc make a select you have to rerun it after the locking are gone.
Regards,

Resources