I have a table like this:
+----+-----------+------+-------+--+
| id | Part | Seq | Model | |
+----+-----------+------+-------+--+
| 1 | Head | 0
Let's first list the challenges:
We require safety for concurrent operations (thus some form or mix of transactions, isolation levels and possibly a "kinda SQL mutex".) Gut feel here is a stored proc for a couple of reasons:
2.1 It protects more easily from sql injection
2.2 We can control the isolation levels (table locking) more easily and recover from some issues which come with this kind of requirement
2.3 We can use application level db locks to control the concurrency
So with that short prequel, let's attempt a solution:
As a start, we are creating your original table and then also a table to hold the sequence (BodyPartsCounter) which we are setting to the last used sequence + 1:
CREATE TABLE BodyParts
([id] int identity, [Part] varchar(9), [Seq] varchar(4), [Model] int)
;
INSERT INTO BodyParts
([Part], [Seq], [Model])
VALUES
('Head', NULL, 3),
('Neck', '1', 3),
('Shoulders', '2', 29),
('Shoulders', '2', 3),
('Stomach', '5', 3)
;
CREATE TABLE BodyPartsCounter
([id] int
, [counter] int)
;
INSERT INTO BodyPartsCounter
([id], [counter])
SELECT 1, MAX(id) + 1 AS id FROM BodyParts
;
Then we need to create the stored procedure which will do the magic. In short, it acts as a mutex, basically guaranteeing you concurrency (if you do not do inserts or updates into the same tables elsewhere). It then get's the next seq, updates it and inserts the new row. After this has all happened it will commit the transaction and release the stored proc for the next waiting calling thread.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Charlla
-- Create date: 2016-02-15
-- Description: Inserts a new row in a concurrently safe way
-- =============================================
CREATE PROCEDURE InsertNewBodyPart
@bodypart varchar(50),
@Model int = 3
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
BEGIN TRANSACTION;
-- Get an application lock in your threaded calls
-- Note: this is blocking for the duration of the transaction
DECLARE @lockResult int;
EXEC @lockResult = sp_getapplock @Resource = 'BodyPartMutex',
@LockMode = 'Exclusive';
IF @lockResult = -3 --deadlock victim
BEGIN
ROLLBACK TRANSACTION;
END
ELSE
BEGIN
DECLARE @newId int;
--Get the next sequence and update - part of the transaction, so if the insert fails this will roll back
SELECT @newId = [counter] FROM BodyPartsCounter WHERE [id] = 1;
UPDATE BodyPartsCounter SET [counter] = @newId + 1 WHERE id = 1;
-- INSERT THE NEW ROW
INSERT INTO dbo.BodyParts(
Part
, Seq
, Model
)
VALUES(
@bodypart
, @newId
, @Model
)
-- END INSERT THE NEW ROW
EXEC @lockResult = sp_releaseapplock @Resource = 'BodyPartMutex';
COMMIT TRANSACTION;
END;
END
GO
Now run the test with this:
EXEC @return_value = [dbo].[InsertNewBodyPart]
@bodypart = N'Stomach',
@Model = 4
SELECT 'Return Value' = @return_value
SELECT * FROM BodyParts;
SELECT * FROM BodyPartsCounter
This all works - but be careful - there's a lot to consider with any kind of multithreaded app.
Hope this helps!