7

我正在尝试实现您的基本 UPSERT 功能,但有一个转折:有时我不想实际更新现有行。

本质上,我正在尝试在不同的存储库之间同步一些数据,而 Upsert 函数似乎是要走的路。因此,主要基于Sam Saffron 对这个问题的回答,以及其他一些研究和阅读,我想出了这个存储过程:

(注意:我使用的是 MS SQL Server 2005,所以不能选择 MERGE 语句)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null,
    @pTeaser varchar(255) = null 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    BEGIN TRANSACTION

        UPDATE dbo.Item WITH (SERIALIZABLE)
        SET Title = @pTitle,
            Teaser = @pTeaser
        WHERE ContentID = @pContentID

        IF @@rowcount = 0
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

    COMMIT TRANSACTION
END

对于基本的 Upsert,我对此感到满意,但我想让实际更新以另一列的值为条件。将其视为“锁定”一行,以便 Upsert 过程不会进行进一步的更新。我可以像这样更改 UPDATE 语句:

UPDATE dbo.Item WITH (SERIALIZABLE)
SET Title = @pTitle,
    Teaser = @pTeaser
WHERE ContentID = @pContentID
AND RowLocked = false

但是随后的插入会在尝试插入已经存在但由于“锁定”而未更新的行时,会因违反唯一约束(对于 ContentID 字段)而失败。

那么这是否意味着我不再拥有经典的 Upsert,即我必须每次都选择行以确定它是否可以更新或插入?我敢打赌就是这样,所以我想我真正需要的是帮助正确处理事务隔离级别,以便程序安全执行。

4

5 回答 5

2

我将以下脚本拼凑在一起,以证明我在过去几年中使用的这个技巧。如果您使用它,则需要对其进行修改以适合您的目的。评论如下:

/*
CREATE TABLE Item
 (
   Title      varchar(255)  not null
  ,Teaser     varchar(255)  not null
  ,ContentId  varchar(30)  not null
  ,RowLocked  bit  not null
)


UPDATE item
 set RowLocked = 1
 where ContentId = 'Test01'

*/


DECLARE
  @Check varchar(30)
 ,@pContentID varchar(30)
 ,@pTitle varchar(255)
 ,@pTeaser varchar(255)

set @pContentID = 'Test01'
set @pTitle     = 'TestingTitle'
set @pTeaser    = 'TestingTeasier'

set @check = null

UPDATE dbo.Item
 set
   @Check = ContentId
  ,Title  = @pTitle
  ,Teaser = @pTeaser
 where ContentID = @pContentID
  and RowLocked = 0

print isnull(@check, '<check is null>')

IF @Check is null
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked)
     values (@pContentID, @pTitle, @pTeaser, 0)

select * from Item

这里的诀窍是您可以在 Update 语句中设置局部变量的值。上面,“标志”值只有在更新有效(即满足更新条件)时才被设置;否则,它不会被改变(这里,留空),你可以检查一下,并进行相应的处理。

至于事务并使其可序列化,在建议如何进行之前,我想更多地了解事务中必须封装的内容。

-- 附录,以下第二条评论的后续 ------------

Saffron 先生的想法是实现此例程的彻底而可靠的方法,因为您的主键是在外部定义并传递到数据库中的(即,您没有使用标识列——对我来说很好,它们经常被过度使用)。

我做了一些更多的测试(在 ContentId 列上添加了主键约束,将 UPDATE 和 INSERT 包装在事务中,将可序列化提示添加到更新中),是的,这应该可以满足您的所有要求。失败的更新会在索引的该部分上打一个范围锁,这将阻止任何同时尝试在列中插入该新值。当然,如果同时提交 N 个请求,“第一个”将创建该行,第二个、第三个等将立即更新该行——除非您在该行的某处设置“锁定”。好把戏!

(请注意,如果没有键列上的索引,您将锁定整个表。此外,范围锁可能会锁定新值“任一侧”的行 - 或者他们不会,我没有测试那个。没关系,因为操作的持续时间应该 [?] 以个位数毫秒为单位。)

于 2009-07-09T23:04:24.780 回答
1
BEGIN TRANSACTION

IF EXISTS(SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
     UPDATE dbo.Item WITH (SERIALIZABLE)
     SET Title = @pTitle, Teaser = @pTeaser
     WHERE ContentID = @pContentID
     AND RowLocked = false
ELSE
     INSERT INTO dbo.Item
          (ContentID, Title, Teaser)
     VALUES
          (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION
于 2009-07-10T01:29:01.053 回答
-1

您可以切换更新/插入的顺序。因此,您在 try/catch 中进行插入,如果违反约束,则进行更新。不过感觉有点脏。

于 2009-07-09T22:46:43.570 回答
-2

CREATE PROCEDURE [dbo].[usp_UpsertItem] -- 在此处添加存储过程的参数 @pContentID varchar(30) = null, @pTitle varchar(255) = null, @pTeaser varchar(255) = null AS BEGIN -- SET添加了 NOCOUNT ON 以防止额外的结果集干扰 SELECT 语句。设置无计数;

BEGIN TRANSACTION
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID
             AND RowLocked = false)
       UPDATE dbo.Item 
       SET Title = @pTitle, Teaser = @pTeaser
       WHERE ContentID = @pContentID
             AND RowLocked = false
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID)
            INSERT INTO dbo.Item (ContentID, Title, Teaser)
            VALUES (@pContentID, @pTitle, @pTeaser)

COMMIT TRANSACTION

结尾

于 2009-07-10T01:16:57.870 回答
-2

我会放弃交易。

加上@@rowcount 可能会起作用,但使用全局变量作为条件检查会导致错误。

只需进行 Exists() 检查。无论如何,你必须通过桌子,所以速度不是问题。

据我所知,不需要交易。

于 2013-09-16T20:53:13.803 回答