Handle nested stored procedures
Transactions
A transaction is a block of SQL statements that usually modifies data. You can either commit
or rollback
a transaction. Transactions are useful when you want to make sure a chain of statements are completed together and if any of the statements fails, undo (rollback) all modifications performed.
For example; imagine when you want to change the order status on an order with data in two different tables
- Order Header
- Order Details
In this case you will only want to save (commit) your changes if all changes can be saved. If saving changes to one of the tables fails, you don't want to save anything and undo (rollback) everything.
This is where transactions comes to the rescue.
BEGIN TRANSACTION
UPDATE OrderHeader
SET Status = 'Completed'
WHERE OrderId = 100
UPDATE OrderDetails
SET Status = 'Completed'
WHERE OrderId = 100
COMMIT TRANSACTION
Stored Procedure to set Order status
Now lets embed the code to a separate stored procedure "spOrdersSetStatus". For the sake of best practice I will also introduce a TRY
and CATCH
block to handle any errors occured.
CREATE PROCEDURE spOrderSetStatus
@OrderId INT,
@Status NVARCHAR(25)
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE OrderHeader
SET Status = @Status
WHERE OrderId = @OrderId
UPDATE OrderDetails
SET Status = @Status
WHERE OrderId = @OrderId
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
-- PUT SOME LOGGING HERE TO HANDLE YOUR ERROR
END CATCH
END
The code will behave like this.
- The code within the
TRY
block is run BEGIN TRAN
- A transaction is opened- If the code within the
TRY
block runs fine,COMMIT TRAN
is done ( transaction closed) and the code ends. - If an error occurs within the
TRY
block, the compiler jumps to theCATCH
block and continues. ROLLBACK TRANSACTION
is run (transaction undo)- Code ends.
Reflections
The procedure above looks clean and solid and will work fine when called directly from an application.
However there are some major drawbacks if the procedure would be called from another stored procedure (nested stored procedure):
- No checking how many transactions are actually open. Closing more transactions than we've opened will result in error.
- No control if there is an active user transaction to commit, and in what state the transaction is in.
Nested Stored procedure
Now lets check what will happen if our stored procedure is called from another procedure.
Lets create another procedure to handle order completion:
CREATE PROCEDURE spOrderComplete
@OrderId INT,
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
EXEC spOrderSetStatus
@OrderId = @OrderId,
@Status = 'Complete';
EXEC spOrderSendReport
@OrderId = @OrderId;
EXEC spOrderClose
@OrderId = @OrderId;
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
-- PUT SOME LOGGING HERE TO HANDLE YOUR ERROR
END CATCH
END
This code will
- BEGIN TRAN - Open transaction 1
- Run the spOrderSetStatus procedure
- Run the spOrderSendReport procedure
- Run the spOrderClose procedure
- COMMIT TRAN - Close transaction 1
What will now happen if an error occurs in spOrderSetStatus?
- BEGIN TRAN - Open transaction 1
- Run the spOrderSetStatus procedure
- BEGIN TRAN - Open transaction 2
- Code Runs
- Error Occurs
- All transactions are rolled back and we now have 0 open transactions.
- BEGIN TRAN - Open transaction 2
- Run the spOrderSetStatus procedure
- Error spotted in the TRY block, code jumps to CATCH block.
- ROLLBACK TRAN - Trying to rollback and close transactions but we have 0 open
- ERROR throw due to no open transactions.
What we need is a way to keep count of open transactions before we close any transactions.
Local transactions
There is actually a kind of local transaction or savestate, that we could use. The syntax for opening a local transaction is SAVE TRANSACTION <yourTransactionName>
. This could come really handy in our example.
We will also need a way to check if there already are any open transactions when deciding to use local or global transactions. The variable @@trancount
is a perfect match and will give us the number of open transactions.
CREATE PROCEDURE spOrderSetStatus
@OrderId INT,
@Status NVARCHAR(25)
AS
BEGIN
DECLARE @hasOpenTran BIT = 'False';
BEGIN TRY
IF @@trancount = 0
BEGIN
BEGIN TRANSACTION
SET @hasOpenTran = 'True';
END
ELSE
SAVE TRANSACTION spMyLocalTran
UPDATE OrderHeader
SET Status = @Status
WHERE OrderId = @OrderId
UPDATE OrderDetails
SET Status = @Status
WHERE OrderId = @OrderId
IF @hasOpenTran = 'True'
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF @hasOpenTran = 'True'
ROLLBACK TRANSACTION
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION spMyLocalTran
-- PUT SOME LOGGING HERE TO HANDLE YOUR ERROR
END CATCH
END
This procedure will now gracefully only close transactions opened locally by this stored procedure, and you will avoid any errors due to mismatch between opened and closed transactione.
sql, sqlserver, storedprocedures, development
- Hits: 650