SQLserver multithreaded locking with TABLOCKX - sql

I have a table "tbluser" with 2 fields:
userid = integer (autoincrement)
user = nvarchar(100)
I have a multithreaded/multi server application that uses this table.
I want to accomplish the following:
Guarantee that field user is unique in my table
Guarantee that combination userid/user is unique in each server's memory
I have the following stored procedure:
CREATE PROCEDURE uniqueuser #user nvarchar(100) AS
BEGIN
BEGIN TRAN
DECLARE #userID int
SET nocount ON
SET #userID = (SELECT #userID FROM tbluser WITH (TABLOCKX) WHERE [user] = #user)
IF #userID <> ''
BEGIN
SELECT userID = #userID
END
ELSE
BEGIN
INSERT INTO tbluser([user]) VALUES (#user)
SELECT userID = SCOPE_IDENTITY()
END
COMMIT TRAN
END
Basically the application calls the stored procedure and provides a username as parameter.
The stored procedure either gets the userid or insert the user if it is a new user.
Am I correct to assume that the table is locked (only one server can insert/query)?

I'm sure the following advice might help someone in the future.
Instead of (TABLOCKX), try (TABLOCKX, HOLDLOCK)
or even better, if this is the only procedure in which writing to tbluser takes place, you can cut down on a whole bunch of code entirely using only TABLOCKX and no transaction via
CREATE PROCEDURE uniqueuser #user nvarchar(100)
AS
--BEGIN TRAN NOT NECESSARY!
INSERT tbluser WITH (TABLOCKX) ([user])
SELECT #user
WHERE NOT EXISTS(SELECT 1 FROM tbluser WHERE [user]=#user)
--COMMIT TRAN NOT NECESSARY!
SELECT userID FROM tbluser WHERE [user]=#user
This way, the insert statement (which automatically creates a transaction for the duration of the INSERT) is the only time you need the table lock. (Yes, I stress-tested this on two windows both with and without TABLOCKX to see how it faired before posting my message.)

If you want to guarantee that the user is unique, the only way is to a unique constraint
ALTER TABLE tbluser WITH CHECK
ADD CONSTRAINT UQ_tbluser_user UQNIUE (user);
Do not "roll your own" unique checks: it will fail.
The cached data in the server's memory conforms to the same constraint
I'd do this. Look for user first, if not found insert, handle unique error just in case. And I'd use an OUTPUT parameter
CREATE PROCEDURE uniqueuser
#user nvarchar(100)
-- ,#userid int = NULL OUTPUT
AS
SET NOCOUNT, XACT_ABORT ON;
DECLARE #userID int;
BEGIN TRY
SELECT #userID FROM tbluser WHERE [user] = #user;
IF #userID IS NULL
BEGIN
INSERT INTO tbluser([user]) VALUES (#user);
SELECT userID = SCOPE_IDENTITY() ;
END
END TRY
BEGIN CATCH
-- test for a concurrent call that just inserted before we did
IF ERROR_NUMBER() = 2627
SELECT #userID FROM tbluser WHERE [user] = #user;
ELSE
-- do some error handling
END CATCH
-- I prefer output parameter for this SELECT #userID AS UserID
GO
Edit: why TABLOCKX fails...
You only lock the table for the duration of the SELECT.
A 2nd process running concurrently will starting reading the table after the lock is released by process 1
Both processes can have #userID IS NULL because process 1 has not yet executed the INSERT
Process 2 gets an error when it INSERTS
This happens because TABLOCKX modifies lock isolation and granularity, not duration.
Edit 2: for SQL Server 2000
CREATE PROCEDURE uniqueuser
#user nvarchar(100)
-- ,#userid int = NULL OUTPUT
AS
SET NOCOUNT, XACT_ABORT ON;
DECLARE #userID int;
SELECT #userID FROM tbluser WHERE [user] = #user;
IF #userID IS NULL
BEGIN
INSERT INTO tbluser([user]) VALUES (#user);
IF ##ERROR = 2627
SELECT #userID FROM tbluser WHERE [user] = #user;
ELSE
RAISERROR ('the fan needs cleaning', 16, 1);
SELECT userID = SCOPE_IDENTITY();
END
GO

Related

SQL: Set variable to a cell value inside a stored procedure

I am trying to store a single cell value from one table in a variable (inside a stored procedure), so I can use it to edit a value in another table, but I keep getting a MSG 201:
Procedure or function 'spBookReturn' expects parameter '#bookID', which was not supplied.
Every time I try to run the sp where it all should happen:
CREATE PROC spBookReturn
#loanID UNIQUEIDENTIFIER,
#bookID UNIQUEIDENTIFIER OUTPUT
AS
BEGIN
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial SET returned = 1, returnedDate = GETDATE();
SET #bookID = (SELECT TOP 1 bookID FROM BorrowedMaterial WHERE loanID = #loanID ORBER BY returnedDate);
UPDATE Books SET nHome = nHome + 1 WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END;
EXEC spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F'
Does anyone know why the way I have defined the #bookID variable doesn't work and how I can make it work?
Thanks.
EDIT:
I got two tables: BorrowedMaterial that includes the attributes loanID, bookID, returned, returnedDate and a few others that's not relevant.
The other table is Books and it includes bookID, nHome but not the loanID.
So by giving only the loanID as an input, I would like to update the nHome. I am trying to grab bookID since it is the only thing the two attributes got in common and this is where the issues happen.
Side note: I removed the variable #custID it spawned by mistake.
All parameters for a procedure are Input parameters. If you declare a parameter as an OUTPUT parameter, it is still an input one, and if it doesn't have a default value must be supplied.
If you want the OUTPUT parameters to be option, which I personally find can be quite often, then give them a default value. I also add some additional logic to your procedure, as you should be using an TRY...CATCH and an ORDER BY in your query with a TOP.
CREATE PROC dbo.spBookReturn #loanID uniqueidentifier,
#bookID uniqueidentifier = NULL OUTPUT,
#custID uniqueidentifier = NULL OUTPUT
AS
BEGIN
BEGIN TRY --If you are using tranasctions, make sure you have a ROLLBACK and THROW for errors
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial
SET returned = 1,
returnedDate = GETDATE()
WHERE loanID = #loanID;
/*
UPDATE BorrowedMaterial
SET returnedDate = GETDATE()
WHERE loanID = #loanID;
*/
SET #bookID = (SELECT TOP 1 bookID
FROM BorrowedMaterial
WHERE loanID = #loanID
ORDER BY ???); --A TOP should have an ORDER BY
UPDATE Books
SET nHome = nHome + 1
WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION tBookReturn;
THROW;
END CATCH;
END;
Then you can execute the procedure as you have, without #bookID and #custID being passed. Of course, if you don't, their values will be "lost" in the calling statement. If you need them, then pass their values too in the EXEC:
DECLARE #bookID uniqueidentifier, #CustID uniqueidentifier;
EXEC dbo.spBookReturn #loanID, #bookID OUTPUT, #CustID OUTPUT;
--SELECT #bookID, #CustID;
declare #bookID UNIQUEIDENTIFIER;
declare #custID UNIQUEIDENTIFIER;
declare #InResult;
exec #InResult= spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F',#bookID,#custID;
select #bookID,#custID;
Seems, you need something like this
My take on what you're looking for. Because there are multiple DML statements (2 Updates) in a single transaction the XACT_ABORT ON option ensures a complete rollback. Also, the THROW in the CATCH block is placed before the ROLLBACK to preserve the SQL generated error metadata. Prior to executing the proc the OUTPUT variables are declared and placed in the parameter list (this omission is what was causing the initial error).
drop proc if exists dbo.spBookReturn;
go
CREATE PROC dbo.spBookReturn
#loanID uniqueidentifier,
#bookID uniqueidentifier OUTPUT,
#custID uniqueidentifier OUTPUT
AS
set nocount on;
set xact_abort on;
BEGIN TRANSACTION tBookReturn
BEGIN TRY
declare #ins_bm table(book_id uniqueidentifier,
cust_id uniqueidentifier);
UPDATE BorrowedMaterial
SET returned = 1,
returnedDate = GETDATE()
output inserted.book_id, inserted.cust_id into #ins_bm
WHERE loanID = #loanID;
if ##rowcount=0
begin
select #bookID=0,
#custID=0;
throw 50000, 'No update performed', 1;
end
else
begin
UPDATE b
SET nHome = nHome + 1
from Books b
WHERE exists (select 1
from #ins_bm bm
where b.ID = bm.book_id);
select top(1) #bookID=book_id,
#custID=cust_id
from #ins_bm;
end
COMMIT TRANSACTION tBookReturn;
END TRY
BEGIN CATCH
THROW
ROLLBACK TRANSACTION tBookReturn;
END CATCH;
go
declare #bookID UNIQUEIDENTIFIER;
declare #custID UNIQUEIDENTIFIER;
declare #InResult int;
exec #InResult=spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F', #bookID output, #custID output;
select #bookID, #custID;
Thanks to #Larnu I figured out the only thing missing was a = on line 3 (and thanks to #charlieface I also got my code cleaned up a tiny bit):
CREATE PROC spBookReturn
#loanID UNIQUEIDENTIFIER,
#bookID UNIQUEIDENTIFIER = NULL OUTPUT
AS
BEGIN
BEGIN TRANSACTION tBookReturn
UPDATE BorrowedMaterial SET returned = 1, returnedDate = GETDATE();
SET #bookID = (SELECT TOP 1 bookID FROM BorrowedMaterial WHERE loanID = #loanID
ORDER BY returnedDate);
UPDATE Books SET nHome = nHome + 1 WHERE ID = #bookID;
COMMIT TRANSACTION tBookReturn;
END;
EXEC spBookReturn '546A444A-3D8D-412E-876D-2053D575B54F'

SQL Server procedure that changes bit on if statement

Currently I have three stored procedures:
The first will insert a userId and roleId
The second will delete targeted userId and roleId
The third is an update that is supposed to change the bit value of column isShopper
The userId is unique, but can have multiple roles. The roles are "customer", "shopper", and "admin". When I insert the role of "shopper" the column isShopper has a bit value of 1, which is what I want.
However when I delete the role of "shopper" from the user, the bit value remains at 1. I'm curious how I would go about changing the bit value of shopper back to 0 when the shopper role is removed.
Insert procedure:
ALTER proc [dbo].[Asp_Net_User_Role_Insert]
#UserId nvarchar (128),
#RoleId nvarchar (128)
AS
BEGIN
INSERT INTO [dbo].[AspNetUserRoles] ([UserId], [RoleId])
SELECT
#UserId, #RoleId
WHERE
NOT EXISTS(SELECT UserId, RoleId
FROM AspNetUserRoles
WHERE RoleId = #RoleId
AND UserId = #UserId)
EXECUTE dbo.ProfileAccountInfos_Shopper_Update
#UserId, #RoleId
END
Delete procedure:
ALTER proc [dbo].[Asp_Net_User_Role_Delete]
#UserId nvarchar (128),
#RoleId nvarchar (128)
AS
BEGIN
DELETE FROM [dbo].[AspNetUserRoles]
WHERE RoleId = #RoleId
AND UserId = #UserId
EXECUTE dbo.ProfileAccountInfos_Shopper_Update
#UserId, #RoleId
END
Update procedure (tagged on the end of the insert and delete)
ALTER proc dbo.ProfileAccountInfos_Shopper_Update
#IsShopper bit,
#UserId nvarchar (128),
#RoleId nvarchar(128)
AS
BEGIN
IF (#RoleId = '91e67659-0dcb-4171-bc76-78d18b1d1336')
AND #IsShopper = 0
BEGIN
UPDATE [dbo].[ProfileAccountInfos]
SET [DateModified] = getutcdate(),
[IsShopper] = 1
WHERE UserId = #UserId
END
END
In Procedure "ProfileAccountInfos_Shopper_Update" you have parameter #IsShooper but you are not passing value for that parameter when you are executing from "Asp_Net_User_Role_Delete" or "Asp_Net_User_Role_Insert".
If your the RoleId value is going to be the same all the time then you can decide the value of #IsShopper in the Asp_Net_User_Role_Insert and Asp_Net_User_Role_Delete procedures and pass it to the ProfileAccountInfos_Shopper_Update procedure. You don't need #RoleId parameter in ProfileAccountInfos_Shopper_Update Procedure.
So ProfileAccountInfos_Shopper_Update will be simplified as following.
ALTER proc dbo.ProfileAccountInfos_Shopper_Update
,#UserId nvarchar (128)
,#IsShopper BIT
AS
BEGIN
UPDATE [dbo].[ProfileAccountInfos] SET [DateModified] = getutcdate(),[IsShopper] = #IsShopper WHERE UserId = #UserI
END
Procedure Asp_Net_User_Role_Insert will be changed as following.
ALTER proc [dbo].[Asp_Net_User_Role_Insert]
#UserId nvarchar (128),
#RoleId nvarchar (128)
AS
BEGIN
DECLARE #IsShoper BIT
SET #IsShopper = 0
INSERT INTO [dbo].[AspNetUserRoles] ([UserId], [RoleId])
SELECT
#UserId, #RoleId
WHERE
NOT EXISTS(SELECT UserId, RoleId
FROM AspNetUserRoles
WHERE RoleId = #RoleId
AND UserId = #UserId)
IF (#RoleId = '91e67659-0dcb-4171-bc76-78d18b1d1336') -- If role being inserted is Shopper then mark user as a Shopper.
BEGIN
SET #IsShopper = 1
END
EXECUTE dbo.ProfileAccountInfos_Shopper_Update #UserId, #IsShopper
END
Procedure Asp_Net_User_Role_Insert will be changed as following.
ALTER proc [dbo].[Asp_Net_User_Role_Delete]
#UserId nvarchar (128),
#RoleId nvarchar (128)
AS
BEGIN
DECLARE #IsShoper BIT
SET #IsShopper = 1
DELETE FROM [dbo].[AspNetUserRoles] WHERE RoleId = #RoleId AND UserId = #UserId
IF (#RoleId = '91e67659-0dcb-4171-bc76-78d18b1d1336') -- If role being deleted is Shopper then mark user as non-shopper.
BEGIN
SET #IsShopper = 0
END
EXECUTE dbo.ProfileAccountInfos_Shopper_Update #UserId, #IsShopper
END
You can also avoid having ProfileAccountInfos_Shopper_Update stored procedure. You can put the update statement in Asp_Net_User_Role_Insert and Asp_Net_User_Role_Delete

Audit table update trigger does not work using a stored procedure

I have table tblA
Which has a trigger defined on it, if any rows are updated it writes the changes to an audit table.
Now if on SQL Server I right click and edit a row directly, I see changes go to the audit table.
If I call a stored procedure to do an update, I do see tblA updated but the changed values do not go into the audit table. It seems like the trigger does not get fired at all.
What difference is there between directly editing a table or updating through a stored procedurein term of the trigger being fired.
Trigger
USE [dbSuppHousing]
GO
/****** Object: Trigger [dbo].[tblSurvey_trigger] Script Date: 9/22/2015 2:32:51 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/* =============================================
Create date: 08/27/15
Description: Trigger to Add audit records to audtSurvey
// =============================================*/
ALTER TRIGGER [dbo].[tblSurvey_trigger] on [dbo].[tblSurvey]
FOR UPDATE,DELETE
AS
SET NOCOUNT ON
Declare #FieldName varchar(100)
, #Deleted varchar(4000)
, #Inserted varchar(4000)
, #SurveyID numeric
, #LoginName varchar(100)
Declare #currentDate datetime = null
SET #currentDate = getdate()
Set #Deleted = ''
Set #Inserted = ''
Select * into #Deleted From Deleted
Select * into #Inserted From Inserted
Create Table #DT ([NameValue] varchar(1000))
Create Table #IT ([NameValue] varchar(1000))
Begin Transaction
Declare auSurveyCur cursor for Select Survey_ID from #Deleted Order By Survey_ID
open auSurveyCur
fetch next from auSurveyCur into #SurveyID
while ##fetch_status = 0
Begin
Declare auU cursor
for Select [name] from syscolumns where id = object_id('dbo.tblSurvey')
open auU
fetch next from auU into #FieldName
while ##fetch_status = 0
Begin
Insert into #DT execute ('Select ' + #FieldName + ' From #Deleted Where Survey_ID = ' + #SurveyID)
Insert into #IT execute ('Select ' + #FieldName + ' From #Inserted Where Survey_ID = ' + #SurveyID)
Set #Deleted = (Select isnull(NameValue,'--') From #DT)
Set #Inserted = (Select isnull(NameValue,'--') From #IT)
If (#Deleted <> #Inserted)
Begin
SELECT #LoginName=Login_Name
From Inserted Where Survey_ID=#SurveyID
if #Deleted = '--' begin set #Deleted = null end
if #Inserted = '--' begin set #Inserted = null end
--#ForWhat=1 means info is for tbSurvey
--In future there may be more tables for auditing and we use same
--Stored procedure to insert audit. Each table has its own audit table.
Execute dbo.InsertauInfo #Surv_ID=#SurveyID
, #auSurveyFieldName=#FieldName
, #auSurveyChangedFrom=#Deleted
, #auSurveyChangedTo=#Inserted
, #auSurveyUpdatedBy=#LoginName
, #auSurveyUpdateDate=#currentDate
, #ForWhat=1
End
Delete From #DT
Delete From #IT
fetch next from auU into #FieldName
End
close auU
deallocate auU
fetch next from auSurveyCur into #SurveyID
END
close auSurveyCur
deallocate auSurveyCur
Commit Transaction
Code for InsertauInfo
ALTER PROCEDURE [dbo].[InsertauInfo]
#Surv_ID bigint,
#auSurveyFieldName varchar(100),
#auSurveyChangedFrom varchar(max),
#auSurveyChangedTo varchar(max),
#auSurveyUpdatedBy varchar(100),
#auSurveyUpdateDate datetime,
#ForWhat int
AS
BEGIN
SET NOCOUNT ON;
IF (#ForWhat=1)
BEGIN
INSERT INTO auSurvey
( Survey_ID
, auSurveyFieldName
, auSurveyChangedFrom
, auSurveyChangedTo
, auSurveyUpdateDate
, auSurveyUpdatedBy
)
VALUES
( #Surv_ID
, #auSurveyFieldName
, #auSurveyChangedFrom
, #auSurveyChangedTo
, #auSurveyUpdateDate
, #auSurveyUpdatedBy
)
END
--ELSE IF (#ForWhat=2)
-- BEGIN
-- INSERT INTO ABC
-- END
END
Example OF stored procedure which does not cause trigger to fire:
OK I'm going crazy but if I Run this very simpple stored procedure directly in DB tirgger gets fired. But if I run from my C# web, Stored procedure will run since tblSurvey gets updated but trigger wont get fired.
ALTER PROCEDURE [dbo].[spUpdateSurveyDataTest]
AS
BEGIN
Update tblSurvey
Set
[s1FirstName]='YES TRIGGER WORKS for sureYES'
Where Survey_Id=327
END
Your trigger only fires on updates and deletes which is why it fired when you edited the table. The stored procedure is performing an insert which is undetected by your trigger. Your trigger should be for INSERT,UPDATE,DELETE.
The point of matter was as I mentioned in the question:
Trigger could not be run from App but I was able to fire it directly from a database update.
The problem was application was connecting to DB with its own generic User which did not have Execute permission for Trigger.
To make things more complicated there was no exception at application level.
The fix was to add
WITH EXECUTE AS OWNER
To the trigger.

Save execute procedure results into a var in SQL Server 2008

I have a procedure that executes another procedure and I need to save the results into a variable.
How can I do that?
ALTER PROCEDURE sp_GetDetailsByUserId
#userId int
AS
BEGIN
SELECT
usr.FirstName, usr.LastName, usr.UserName, usr.Email
FROM
[User] usr
WHERE
usr.UserID = #userId
EXEC sp_GenerateRandomPass #userId // this result need to be inside a var
END
I am a beginner and need help.
Thank you.
you can declare a Table or you can use a Temp Table:
ALTER PROCEDURE sp_GetDetailsByUserId
#userId int
AS
BEGIN
SELECT usr.FirstName, usr.LastName, usr.UserName, usr.Email
FROM [User] usr
WHERE usr.UserID = #userId
declare #tbl table (the columns should be compatible with the result columns)
insert into #tbl
exec sp_GenerateRandomPass #userId // this result need to be inside a var
END
with temptable you can do:
ALTER PROCEDURE sp_GetDetailsByUserId
#userId int
AS
BEGIN
SELECT usr.FirstName, usr.LastName, usr.UserName, usr.Email
FROM [User] usr
WHERE usr.UserID = #userId
create table #tempTable(the columns should be compatible with the result columns)
insert into #tempTable
exec sp_GenerateRandomPass #userId // this result need to be inside a var
END
Can you change the procedure? Having output parameters would probably be best, assuming the select always returns just one row.
exec sp_GenerateRandomPass #userId, #password output
output parameters work this way:
ALTER PROCEDURE sp_GenerateRandomPass
#userId int,
#password varchar (100) output
AS
BEGIN
-- Magic happens
set #password = 'xxxx'
END
Other option is to change GenerateRandomPass to be a scalar function, you can assign value from it to a variable directly in your procedure
Also, please do not prefix your procedures with "sp_", it's intended for built in procedures.
You need to modify sp_GenerateRandomPass , Store result of query within this stored procedure to temptable. Find more about temptable.
Temp table will be accessible among stored procedures. So you can use that in sp_GetDetailsByUserId

SQL Stored Procedure not handling error from nested stored procedure

I have two stored procedures, one nested inside the other. When the nested stored procedure is called, at the moment, it should error with a foreign key constraint violation and then rollback the earlier call to insert into the ProductLicense table. The nested procedure does not perform any action on the database because of the foreign key violation but the calling stored procedure isn't catching the error and rolling back. If I execute the nested stored procedure by itself it does return error 547 Foreign key violation.
How can I get the two stored procedures to work together?
Outer procedure:
ALTER PROCEDURE [dbo].[AddNewLicense2_i]
-- Add the parameters for the stored procedure here
#customerId nvarchar(10),
#licenseModeId int,
#licenseModeProgramId int,
#createdBy int,
#updateBy int,
#systemId nvarchar(50),
#productId int
AS
BEGIN TRY
BEGIN TRANSACTION
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--SET XACT_ABORT ON; --used for automatic rollback when an error occurs
DECLARE #tempDays INT
DECLARE #programCornerAmt INT
DECLARE #tempEndDate DATETIME
DECLARE #tempExpDate DATETIME
DECLARE #err INT
SET #err = 0
/*SET #tempDays = (SELECT lmp.TimeoutDays
FROM LicenseModeProgram lmp
WHERE lmp.LicenseModeProgramId = #licenseModeProgramId)*/
SELECT #tempDays = TimeoutDays, #programCornerAmt = MonthlyCornersAmount
FROM LicenseModeProgram
WHERE LicenseModeProgramId = #licenseModeProgramId
--Build Expiration and End Dates.
IF #tempDays = NULL --then this is NOT a time rental or metered system
BEGIN
SET #tempEndDate = NULL
SET #tempExpDate = NULL
END
ELSE
BEGIN
SET #tempEndDate = DATEADD("d", #tempDays, GETDATE())
SET #tempExpDate = DATEADD("d", #tempDays, GETDATE())
END
-- Create new product license record
INSERT INTO ProductLicense (CustomerId, LicenseModeId, LicenseModeProgramId, CreatedBy, UpdatedBy, SystemId, ProductId, ExpirationDate, LicenseEndDate)
VALUES (#customerId, #licenseModeId, #licenseModeProgramId, #createdBy, #updateBy, #systemId, #productId, #tempExpDate, #tempEndDate)
IF #licenseModeId = 4 AND #systemId NULL AND #programCornerAmt NULL
--call stored procedure to add corners to the customer account
EXECUTE #err = AddMeteredTx_i #systemId, 1, 1, #programCornerAmt , 'Initial License Creation'
PRINT #err
COMMIT TRANSACTION
END TRY
BEGIN CATCH
RAISERROR('Failed to Create License', 11, 2)
ROLLBACK TRANSACTION
RETURN 1
END CATCH
--COMMIT TRANSACTION
RETURN 0
GO
Inner procedure:
ALTER PROCEDURE [dbo].[AddMeteredTx_i]
-- Add the parameters for the stored procedure here
#systemId nvarchar(50),
#activityEventId int,
#createdBy int,
#amount int,
#notes text
AS
BEGIN TRY
BEGIN TRANSACTION
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--SET XACT_ABORT ON; --used for automatic rollback when an error occurs
INSERT INTO CustomerAccountActivity (SystemId, ActivityEventId, CreatedBy, Amount, Notes)
VALUES (#systemId, #activityEventId, #createdBy, #amount, #notes)
UPDATE CustomerAccount
SET MeteredBalance = (SELECT MeteredBalance FROM CustomerAccount WHERE SystemId = #systemId) + #amount
WHERE SystemId = #systemId
COMMIT TRANSACTION
END TRY
BEGIN CATCH
RAISERROR('Error Update to Customer Account Record ', 11, 2)
ROLLBACK TRANSACTION
RETURN 1
--COMMIT TRANSACTION
END CATCH
RETURN 0
GO
Catching errors with a call stack like this using ##Error can be problematic. It's a lot more reliable to use TRY/CATCH
The basic format is:
BEGIN TRY
<BEGIN TRAN>
... do stuff ...
<COMMIT TRAN>
END TRY
BEGIN CATCH
<ROLLBACK TRAN>
... do error stuff like re-raise the error to outer scope ...
END CATCH
Any error encountered in the try will automatically take you to the CATCH block without additional checking.