I have to execute blocks of T-SQL with insert and update instructions.
Let's say I have these two blocks of code, and I have no direct control on them.
Block #1:
insert into mytable (id, col1)
values ((select max(id) + 1 from mytable), 'foo');
insert into non_existing_table (id, col1)
values ((select max(id) from mytable), 'bar');
Block #2:
insert into mytable (id, col1)
values ((select max(id) + 1 from mytable), 'foo');
insert into anothertable (id, col1)
values ((select max(id) from mytable), 'bar');
In the first block there are two instructions, the first is ok, the second one fails because the second table does not exists.
In the second block there are also two instructions, both are ok.
What I want to do is run both two blocks
The first block should do nothing in the end, since the second instruction fails, the first should be rolled back
The second block is fine, since there is no error, both inserts should be committed
What's the correct way to do this? Maybe a stored procedure that accepts a full text parameter and run all the code as a transaction? How can I do that in SQL Server?
You can create a stored procedure with transactions.
Try like this.
CREATE PROCEDURE [dbo].[Procedure_name]
#iErrorCode int OUTPUT,
--other parameters
AS
BEGIN
BEGIN TRY
begin tran
insert into mytable (id,col1) values ((select max(id)+1 from mytable),'foo');
insert into non_existing_table (id,col1) values ((select max(id) from mytable),'bar');
--other queries
commit tran
SELECT #iErrorCode =##ERROR;
END TRY
BEGIN CATCH
rollback tran
Select ERROR_NUMBER();
Select ERROR_MESSAGE();
SELECT #iErrorCode =##ERROR
END CATCH
END
Related
How can I make sure following statements don't have a race condition?
IF NOT EXISTS (select col1 from Table1 where SomeId=#SomeId)
INSERT INTO Table1 values (#SomeId,...)
IF NOT EXISTS (select col1 from Table2 where SomeId=#SomeId)
INSERT INTO Table2 values (#SomeId,...)
Is this enough
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
IF NOT EXISTS (SELECT col1 FROM Table1 WITH (UPDLOCK) WHERE SomeId=#SomeId)
INSERT INTO Table1 VALUES (#SomeId,...)
COMMIT TRAN
BEGIN TRAN
IF NOT EXISTS (SELECT col1 FROM Table2 WITH (UPDLOCK) WHERE SomeId=#SomeId)
INSERT INTO Table2 VALUES (#SomeId,...)
COMMIT TRAN
Yes. That is enough. Setting the transaction isolation level to serializable will create key locks that cover SomeId=#SomeId when you run your select-- which will prevent other processes from inserting values with the same key (SomeId=#SomeId) while your transaction is running.
The WITH(UPDLOCK) hint will cause the SELECT to obtain an update lock on the selected row(s), if they exist. This will prevent other transactions from modifying these rows (if they existed at the time of the select) while your transaction is running.
It doesn't look like you really need the WITH(UPDLOCK) hint, since you are committing the transaction right away if the record already exists. If you wanted to do something else before committing if the record does exist, you might need this hint-- but as it is, it appears you do not.
A statement is a transaction
declare #v int = 11;
insert into iden (val)
select #v
where not exists (select 1 from iden with (UPDLOCK) where val = #v)
I'm trying to write a somewhat simple stored procedure. I first want to verify if there is anything in the table; if there is to TRUNCATE it, if there isn't to populate it with some data. What happens is, it gets execute without any error, but nothing happens to the table. It is as it was before executing it.........empty.
ALTER PROCEDURE [dbo].[LoadReportDataCI]
AS
If (Select Count(*) from tbl_TempTableReport)>0
BEGIN
Truncate table tbl_TempTableReport
END
Begin
INSERT INTO tbl_TempTableReport (userID, VendorID, VendorName, UnitCost, UnitCount, CostValue)
SELECT
'1234',
VendorID,
VendorName,
InvValue,
BegInvOnHand,
BegCurrentValue
From vVendorsAndInvONHand
END
If I highlight starting at INSERT until the END, the data gets populated in the table, but otherwise it doesn't work. Shed some light anyone?
There is no point to checking if there are rows before truncating. It is far faster to just truncate than checking for the existence of rows. If there are no rows it will still run just fine.
ALTER PROCEDURE [dbo].[LoadReportDataCI]
AS
Truncate table tbl_TempTableReport;
INSERT INTO tbl_TempTableReport
(
userID
, VendorID
, VendorName
, UnitCost
, UnitCount
, CostValue
)
SELECT
'1234'
, VendorID
, VendorName
, InvValue
, BegInvOnHand
, BegCurrentValue
From vVendorsAndInvONHand;
Use below code to execute your stored procedure, I have moved your second begin after AS
ALTER PROCEDURE [dbo].[sp_LoadReportDataCI]
AS
Begin
If (Select Count(*) from tbl_TempTableReport)>0
BEGIN
Truncate table tbl_TempTableReport
END
INSERT INTO tbl_TempTableReport (userID, VendorID, VendorName, UnitCost, UnitCount, CostValue)
SELECT
'1234',
VendorID,
VendorName,
InvValue,
BegInvOnHand,
BegCurrentValue
From vVendorsAndInvONHand
END
I guess, you didn't execute the Stored Procedure. Did you EXEC the LoadReportDataCI after running the ALTER script?
First off, I want to start by saying I am not an SQL programmer (I'm a C++/Delphi guy), so some of my questions might be really obvious. So pardon my ignorance :o)
I've been charged with writing a script that will update certain tables in a database based on the contents of a CSV file. I have it working it would seem, but I am worried about atomicity for one of the steps:
One of the tables contains only one field - an int which must be incremented each time, but from what I can see is not defined as an identity for some reason. I must create a new row in this table, and insert that row's value into another newly-created row in another table.
This is how I did it (as part of a larger script):
DECLARE #uniqueID INT,
#counter INT,
#maxCount INT
SELECT #maxCount = COUNT(*) FROM tempTable
SET #counter = 1
WHILE (#counter <= #maxCount)
BEGIN
SELECT #uniqueID = MAX(id) FROM uniqueIDTable <----Line 1
INSERT INTO uniqueIDTableVALUES (#uniqueID + 1) <----Line 2
SELECT #uniqueID = #uniqueID + 1
UPDATE TOP(1) tempTable
SET userID = #uniqueID
WHERE userID IS NULL
SET #counter = #counter + 1
END
GO
First of all, am I correct using a "WHILE" construct? I couldn't find a way to achieve this with a simple UPDATE statement.
Second of all, how can I be sure that no other operation will be carried out on the database between Lines 1 and 2 that would insert a value into the uniqueIDTable before I do? Is there a way to "synchronize" operations in SQL Server Express?
Also, keep in mind that I have no control over the database design.
Thanks a lot!
You can do the whole 9 yards in one single statement:
WITH cteUsers AS (
SELECT t.*
, ROW_NUMBER() OVER (ORDER BY userID) as rn
, COALESCE(m.id,0) as max_id
FROM tempTable t WITH(UPDLOCK)
JOIN (
SELECT MAX(id) as id
FROM uniqueIDTable WITH (UPDLOCK)
) as m ON 1=1
WHERE userID IS NULL)
UPDATE cteUsers
SET userID = rn + max_id
OUTPUT INSERTED.userID
INTO uniqueIDTable (id);
You get the MAX(id), lock the uniqueIDTable, compute sequential userIDs for users with NULL userID by using ROW_NUMBER(), update the tempTable and insert the new ids into uniqueIDTable. All in one operation.
For performance you need and index on uniqueIDTable(id) and index on tempTable(userID).
SQL is all about set oriented operations, WHILE loops are the code smell of SQL.
You need a transaction to ensure atomicity and you need to move the select and insert into one statement or do the select with an updlock to prevent two people from running the select at the same time, getting the same value and then trying to insert the same value into the table.
Basically
DECLARE #MaxValTable TABLE (MaxID int)
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO uniqueIDTable VALUES (id)
OUTPUT inserted.id INTO #MaxValTable
SELECT MAX(id) + 1 FROM uniqueIDTable
UPDATE TOP(1) tempTable
SET userID = (SELECT MAXid FROM #MaxValTable)
WHERE userID IS NULL
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
RAISERROR 'Error occurred updating tempTable' -- more detail here is good
END CATCH
That said, using an identity would make things far simpler. This is a potential concurrency problem. Is there any way you can change the column to be identity?
Edit: Ensuring that only one connection at a time will be able to insert into the uniqueIDtable. Not going to scale well though.
Edit: Table variable's better than exclusive table lock. If need be, this can be used when inserting users as well.
I have a table where I created an INSTEAD OF trigger to enforce some business rules.
The issue is that when I insert data into this table, SCOPE_IDENTITY() returns a NULL value, rather than the actual inserted identity.
Insert + Scope code
INSERT INTO [dbo].[Payment]([DateFrom], [DateTo], [CustomerId], [AdminId])
VALUES ('2009-01-20', '2009-01-31', 6, 1)
SELECT SCOPE_IDENTITY()
Trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF NOT EXISTS(SELECT 1 FROM dbo.Payment p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo)
) AND NOT EXISTS (SELECT 1 FROM Inserted p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom <> p.DateFrom AND i.DateTo <> p.DateTo) AND
((i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo))
)
BEGIN
INSERT INTO dbo.Payment (DateFrom, DateTo, CustomerId, AdminId)
SELECT DateFrom, DateTo, CustomerId, AdminId
FROM Inserted
END
ELSE
BEGIN
ROLLBACK TRANSACTION
END
END
The code worked before the creation of this trigger. I am using LINQ to SQL in C#. I don't see a way of changing SCOPE_IDENTITY to ##IDENTITY. How do I make this work?
Use ##identity instead of scope_identity().
While scope_identity() returns the last created id in the current scope, ##identity returns the last created id in the current session.
The scope_identity() function is normally recommended over the ##identity field, as you usually don't want triggers to interfer with the id, but in this case you do.
Since you're on SQL 2008, I would highly recommend using the OUTPUT clause instead of one of the custom identity functions. SCOPE_IDENTITY currently has some issues with parallel queries that cause me to recommend against it entirely. ##Identity does not, but it's still not as explicit, and as flexible, as OUTPUT. Plus OUTPUT handles multi-row inserts. Have a look at the BOL article which has some great examples.
I was having serious reservations about using ##identity, because it can return the wrong answer.
But there is a workaround to force ##identity to have the scope_identity() value.
Just for completeness, first I'll list a couple of other workarounds for this problem I've seen on the web:
Make the trigger return a rowset. Then, in a wrapper SP that performs the insert, do INSERT Table1 EXEC sp_ExecuteSQL ... to yet another table. Then scope_identity() will work. This is messy because it requires dynamic SQL which is a pain. Also, be aware that dynamic SQL runs under the permissions of the user calling the SP rather than the permissions of the owner of the SP. If the original client could insert to the table, he should still have that permission, just know that you could run into problems if you deny permission to insert directly to the table.
If there is another candidate key, get the identity of the inserted row(s) using those keys. For example, if Name has a unique index on it, then you can insert, then select the (max for multiple rows) ID from the table you just inserted to using Name. While this may have concurrency problems if another session deletes the row you just inserted, it's no worse than in the original situation if someone deleted your row before the application could use it.
Now, here's how to definitively make your trigger safe for ##Identity to return the correct value, even if your SP or another trigger inserts to an identity-bearing table after the main insert.
Also, please put comments in your code about what you are doing and why so that future visitors to the trigger don't break things or waste time trying to figure it out.
CREATE TRIGGER TR_MyTable_I ON MyTable INSTEAD OF INSERT
AS
SET NOCOUNT ON
DECLARE #MyTableID int
INSERT MyTable (Name, SystemUser)
SELECT I.Name, System_User
FROM Inserted
SET #MyTableID = Scope_Identity()
INSERT AuditTable (SystemUser, Notes)
SELECT SystemUser, 'Added Name ' + I.Name
FROM Inserted
-- The following statement MUST be last in this trigger. It resets ##Identity
-- to be the same as the earlier Scope_Identity() value.
SELECT MyTableID INTO #Trash FROM MyTable WHERE MyTableID = #MyTableID
Normally, the extra insert to the audit table would break everything, because since it has an identity column, then ##Identity will return that value instead of the one from the insertion to MyTable. However, the final select creates a new ##Identity value that is the correct one, based on the Scope_Identity() that we saved from earlier. This also proofs it against any possible additional AFTER trigger on the MyTable table.
Update:
I just noticed that an INSTEAD OF trigger isn't necessary here. This does everything you were looking for:
CREATE TRIGGER dbo.TR_Payments_Insert ON dbo.Payment FOR INSERT
AS
SET NOCOUNT ON;
IF EXISTS (
SELECT *
FROM
Inserted I
INNER JOIN dbo.Payment P ON I.CustomerID = P.CustomerID
WHERE
I.DateFrom < P.DateTo
AND P.DateFrom < I.DateTo
) ROLLBACK TRAN;
This of course allows scope_identity() to keep working. The only drawback is that a rolled-back insert on an identity table does consume the identity values used (the identity value is still incremented by the number of rows in the insert attempt).
I've been staring at this for a few minutes and don't have absolute certainty right now, but I think this preserves the meaning of an inclusive start time and an exclusive end time. If the end time was inclusive (which would be odd to me) then the comparisons would need to use <= instead of <.
Main Problem : Trigger and Entity framework both work in diffrent scope.
The problem is, that if you generate new PK value in trigger, it is different scope. Thus this command returns zero rows and EF will throw exception.
The solution is to add the following SELECT statement at the end of your Trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
in place of * you can mention all the column name including
SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
Like araqnid commented, the trigger seems to rollback the transaction when a condition is met. You can do that easier with an AFTER INSTERT trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF <Condition>
BEGIN
ROLLBACK TRANSACTION
END
END
Then you can use SCOPE_IDENTITY() again, because the INSERT is no longer done in the trigger.
The condition itself seems to let two identical rows past, if they're in the same insert. With the AFTER INSERT trigger, you can rewrite the condition like:
IF EXISTS(
SELECT *
FROM dbo.Payment a
LEFT JOIN dbo.Payment b
ON a.Id <> b.Id
AND a.CustomerId = b.CustomerId
AND (a.DateFrom BETWEEN b.DateFrom AND b.DateTo
OR a.DateTo BETWEEN b.DateFrom AND b.DateTo)
WHERE b.Id is NOT NULL)
And it will catch duplicate rows, because now it can differentiate them based on Id. It also works if you delete a row and replace it with another row in the same statement.
Anyway, if you want my advice, move away from triggers altogether. As you can see even for this example they are very complex. Do the insert through a stored procedure. They are simpler and faster than triggers:
create procedure dbo.InsertPayment
#DateFrom datetime, #DateTo datetime, #CustomerId int, #AdminId int
as
BEGIN TRANSACTION
IF NOT EXISTS (
SELECT *
FROM dbo.Payment
WHERE CustomerId = #CustomerId
AND (#DateFrom BETWEEN DateFrom AND DateTo
OR #DateTo BETWEEN DateFrom AND DateTo))
BEGIN
INSERT into dbo.Payment
(DateFrom, DateTo, CustomerId, AdminId)
VALUES (#DateFrom, #DateTo, #CustomerId, #AdminId)
END
COMMIT TRANSACTION
A little late to the party, but I was looking into this issue myself. A workaround is to create a temp table in the calling procedure where the insert is being performed, insert the scope identity into that temp table from inside the instead of trigger, and then read the identity value out of the temp table once the insertion is complete.
In procedure:
CREATE table #temp ( id int )
... insert statement ...
select id from #temp
-- (you can add sorting and top 1 selection for extra safety)
drop table #temp
In instead of trigger:
-- this check covers you for any inserts that don't want an identity value returned (and therefore don't provide a temp table)
IF OBJECT_ID('tempdb..#temp') is not null
begin
insert into #temp(id)
values
(SCOPE_IDENTITY())
end
You probably want to call it something other than #temp for safety sake (something long and random enough that no one else would be using it: #temp1234235234563785635).
Hello is possible to switch between DML commands/operations (Insert,Delete,Update) on Trigger Body?, I try to snippet some T-SQL for understand me better :
CREATE TRIGGER DML_ON_TABLEA
ON TABLEA
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
CASE
WHEN (INSERT) THEN
-- INSERT ON AUX TABLEB
WHEN (DELETE) THEN
-- DELETE ON AUX TABLEB
ELSE --OR WHEN (UPDATE) THEN
-- UPDATE ON AUX TABLEB
END
END
GO
Thanks,
I will show you a simple way to check this in SQL Server 2000 or 2005 (you forgot to mention which version you are using), but in general I agree with Remus that you should break these up into separate triggers:
DECLARE #i INT, #d INT;
SELECT #i = COUNT(*) FROM inserted;
SELECT #d = COUNT(*) FROM deleted;
IF #i + #d > 0
BEGIN
IF #i > 0 AND #d = 0
BEGIN
-- logic for insert
END
IF #i > 0 AND #d > 0
BEGIN
-- logic for update
END
IF #i = 0 AND #d > 0
BEGIN
-- logic for delete
END
END
Note that this may not be perfectly forward-compatible due to the complexity MERGE introduces in SQL Server 2008. See this Connect item for more information:
MERGE can cause a trigger to fire multiple times
So if you are planning to use SQL Server 2008 and MERGE in the future, then this is even more reason to split the trigger up into a trigger for each type of DML operation.
(And if you want more reasons to avoid MERGE, read this and this.)
You can use the inserted and deleted tables to see what changes were made to the table.
For an UPDATE, the deleted table contains the old version of the row, and inserted the new version.
DELETE and INSERT use their own table as you would expect.
You can have three separate triggers, one for INSERT one for UPDATE one for DELETE. Since each trigger is different, there is no need for switch logic.
I think the general way to do this is to create a trigger for each action, like so:
CREATE TRIGGER INSERT_ON_TABLEA
ON TABLEA
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- INSERT ON AUX TABLEB
END
GO
CREATE TRIGGER DELETE_ON_TABLEA
ON TABLEA
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
-- DELETE ON AUX TABLEB
END
GO
You can use one trigger for all commands/operations by use union join;
CREATE TRIGGER DML_ON_TABLEA
ON TABLEA
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--logic for insert
insert into Backup_table (columns_name) select columns_name from inserted i
--logic for delete
UNION ALL
insert into Backup_table (columns_name) select columns_name from deleted d
END
GO
--note update command like inserted command but have another command deleted