我有一个 API,可以添加新行或更新 SQL 表中的行。如果负载的
ID
大于0
,那么它会根据给定的ID
更新表,否则它会插入一个新行并返回新的ID
。
这里是示例 SQL 片段:
BEGIN TRANSACTION;
SET @FETCHEDID= (SELECT ID FROM dbo.SAMPLETABLE WHERE ID = @ID)
IF @FETCHEDID> 0
BEGIN
UPDATE dbo.SAMPLETABLE
SET [AMOUNT]=@AMOUNT,[DATECHANGED]=@DATEADDED,[LASTCHANGEDBYID]=@ADDEDBYID,
WHERE [ID]=@ID";
END
ELSE
BEGIN
INSERT INTO dbo.SAMPLETABLE([AMOUNT],[DATEADDED],[LASTCHANGEDBYID],[DATECHANGED])
VALUES (@AMOUNT,@DATEADDED,@ADDEDBYID,@DATEADDED);
END
COMMIT TRANSACTION;"
当使用
ID = 0
快速连续地从 UI 对该 API 进行两次连续的重复调用时,它最终会在数据库表中添加两行。
理想情况下,第一个 API 调用应该添加一个新行,第二个 API 调用应该用第一次调用返回的新 ID
更新该行。
调用是从 UI 进行的,负载中的
ID
只能在第一次成功调用时更新。这也可以通过避免重复调用在 UI 中处理。但我想确保后端也能够处理它。
第二个 API 调用是在更新之前从表中读取
ID
的陈旧值。
我们如何确保第二次 API 调用仅在第一次调用完成后才开始。
这是使用
READ COMMITTED
隔离时的常见问题。
您执行此“Upsert”逻辑的方式是众所周知的反模式。锁定没有正确完成(它需要
UPDLOCK
),即使它仍然是低效的。
相反,只需
UPDATE
然后检查行数并有条件地INSERT
.
SET XACT_ABORT ON; -- needed to clean up transaction.
BEGIN TRANSACTION;
UPDATE dbo.SAMPLETABLE WITH (HOLDLOCK, UPDLOCK)
SET AMOUNT = @AMOUNT,
DATECHANGED = @DATEADDED,
LASTCHANGEDBYID = @ADDEDBYID
WHERE ID = @ID;
IF @@ROWCOUNT = 0
INSERT INTO dbo.SAMPLETABLE
(AMOUNT, DATEADDED, LASTCHANGEDBYID, DATECHANGED)
VALUES (@AMOUNT, @DATEADDED, @ADDEDBYID, @DATEADDED);
COMMIT;
如果插入比更新更有可能那么你可以翻转它。
SET XACT_ABORT ON; -- needed to clean up transaction.
BEGIN TRANSACTION;
INSERT INTO dbo.SAMPLETABLE
(AMOUNT, DATEADDED, LASTCHANGEDBYID, DATECHANGED)
SELECT @AMOUNT, @DATEADDED, @ADDEDBYID, @DATEADDED)
WHERE NOT EXISTS (SELECT 1
FROM dbo.SAMPLETABLE WITH (HOLDLOCK, UPDLOCK)
WHERE ID = @ID);
IF @@ROWCOUNT = 0
UPDATE dbo.SAMPLETABLE
SET AMOUNT = @AMOUNT,
DATECHANGED = @DATEADDED,
LASTCHANGEDBYID = @ADDEDBYID
WHERE ID = @ID;
COMMIT;