Reputation: 25
I have an issue with a stored procedure where it is updating one table, but not another, and it only happens on the odd occasion, not all the time. This is the stored procedure:
ALTER PROCEDURE [dbo].[AddTransaction]
@BeneficiaryName VARCHAR(200),
@Amount DECIMAL(18,2),
@Reference VARCHAR(200),
@Direction INT,
@Currency INT,
@CurrencyCulture VARCHAR(5),
@Status INT,
@Month INT,
@Year INT,
@BankAccountId INT,
@BusinessId INT,
@FeeType INT,
@IncomingPaymentsPercentage DECIMAL(18,2),
@IncomingPaymentFee DECIMAL(18,2),
@CompletedPaymentFee DECIMAL(18,2),
@DateProcessed DATETIME
AS
BEGIN
DECLARE @oldBalance DECIMAL(18,2)
DECLARE @newBalance DECIMAL(18,2)
DECLARE @fee DECIMAL(18,2)
DECLARE @amountAfterFee DECIMAL(18,2)
BEGIN TRANSACTION
-- SET NOCOUNT ON;
-- lock table 'BankAccounts' till end of transaction
SELECT @oldBalance = availableBalance
FROM BankAccounts
WITH (TABLOCKX, HOLDLOCK)
WHERE Id = @BankAccountId
-- set the new balance
SET @newBalance = @oldBalance + @Amount
UPDATE BankAccounts SET AvailableBalance = @newBalance WHERE Id = @BankAccountId
-- insert a new transaction
IF @FeeType = 1
BEGIN
IF @Direction = 1
BEGIN
SET @fee = @Amount * (@IncomingPaymentsPercentage / 100)
SET @amountAfterFee = @Amount - @fee;
END;
ELSE
BEGIN
SET @fee = 0.00
SET @amountAfterFee = @Amount
END;
END;
ELSE
BEGIN
IF @Direction = 1
BEGIN
SET @amountAfterFee = @Amount - @IncomingPaymentFee
SET @fee = @IncomingPaymentFee
END;
ELSE
BEGIN
SET @amountAfterFee = @Amount - @CompletedPaymentFee
SET @fee = @CompletedPaymentFee
END;
END;
INSERT INTO Transactions
(BeneficiaryName, amount, Reference, Direction, Currency, CurrencyCulture, DateAdded, [Status], DateProcessed, [Month], [Year], Balance, BankAccountId, AmountAfterFee, Fee)
VALUES
(@BeneficiaryName, @Amount, @Reference, @Direction, @Currency, @CurrencyCulture, @DateProcessed, @Status, @DateProcessed, @Month, @Year, @newBalance, @BankAccountId, @amountAfterFee, @fee)
-- now commit the transaction
COMMIT
The insert in to the transactions table is working all the time, but on the odd occasion, the update on the BankAccounts table doesn't happen. This line here:
UPDATE BankAccounts SET AvailableBalance = @newBalance WHERE Id = @BankAccountId
I am not sure if this is anything to do with the lock, but I thought you could still update the locked table within the same transaction. For more info, there could be multiple calls to this stored procedure in the same second, probably no more than 10, but often the issue is when nothing else is accessing the BankAccount record it is updating.
Upvotes: 1
Views: 886
Reputation: 72480
First off, TABLOCKX
on a SELECT
statement does not take an X-lock. SELECT
statements never take X-locks anyway. Furthermore, it is unnecessary and extremely inefficient to lock the entire table.
Secondly, even HOLDLOCK
is not enough here, because if no data has been modified, the lock will not be taken. You must use UPDLOCK
for this to work as expected.
To be honest, you don't actually need the SELECT
anyway, as the whole thing can be done in one atomic statement:
I note that
@oldBalance
isn't actually used.
UPDATE BankAccounts WITH (HOLDLOCK)
SET AvailableBalance += @amount,
@newBalance = AvailableBalance + @amount
WHERE Id = @BankAccountId;
You can even combine the Transaction
insert by using an OUTPUT
clause, this enables you to entirely remove the BEGIN TRANSACTION/COMMIT
ALTER PROCEDURE [dbo].[AddTransaction]
@BeneficiaryName VARCHAR(200),
@Amount DECIMAL(18,2),
@Reference VARCHAR(200),
@Direction INT,
@Currency INT,
@CurrencyCulture VARCHAR(5),
@Status INT,
@Month INT,
@Year INT,
@BankAccountId INT,
@BusinessId INT,
@FeeType INT,
@IncomingPaymentsPercentage DECIMAL(18,2),
@IncomingPaymentFee DECIMAL(18,2),
@CompletedPaymentFee DECIMAL(18,2),
@DateProcessed DATETIME
AS
SET NOCOUNT ON;
DECLARE @fee DECIMAL(18,2);
DECLARE @amountAfterFee DECIMAL(18,2);
SET @fee = CASE WHEN @FeeType = 1 THEN
CASE WHEN @Direction = 1
THEN @Amount * (@IncomingPaymentsPercentage / 100)
ELSE 0.0 END
ELSE
CASE WHEN @Direction = 1
THEN @IncomingPaymentFee
ELSE @CompletedPaymentFee END
END;
SET @amountAfterFee = @Amount - @fee;
UPDATE BankAccounts WITH (HOLDLOCK)
SET AvailableBalance += @amount
OUTPUT
@BeneficiaryName, @Amount, @Reference, @Direction, @Currency,
@CurrencyCulture, @DateProcessed, @Status, @DateProcessed,
@Month, @Year, inserted.AvailableBalance, @BankAccountId, @amountAfterFee, @fee
INTO Transactions
(BeneficiaryName, amount, Reference, Direction, Currency,
CurrencyCulture, DateAdded, [Status], DateProcessed, [Month], [Year],
Balance, BankAccountId, AmountAfterFee, Fee)
WHERE Id = @BankAccountId;
GO
Upvotes: 1