在表复合键中增加版本号

Posted

技术标签:

【中文标题】在表复合键中增加版本号【英文标题】:Incrementing revision numbers in table's composite key 【发布时间】:2016-04-10 00:18:27 【问题描述】:

我正在本地运行 SQL Server 2014,以获取将部署到 Azure SQL V12 数据库的数据库。

我有一个存储业务实体对象的可扩展属性值的表,在本例中,三个表如下所示:

CREATE TABLE Widgets (
    WidgetId bigint IDENTITY(1,1),
    ...
)

CREATE TABLE WidgetProperties (
    PropertyId int IDENTITY(1,1),
    Name       nvarchar(50)
    Type       int -- 0 = int, 1 = string, 2 = date, etc
)

CREATE TABLE WidgetPropertyValues (
    WidgetId   bigint,
    PropertyId int,
    Revision   int,
    DateTime   datetimeoffset(7),
    Value      varbinary(255)

    CONSTRAINT [PK_WidgetPropertyValues] PRIMARY KEY CLUSTERED (
        [WidgetId] ASC,
        [PropertyIdId] ASC,
        [Revision] ASC
    )
)

ALTER TABLE dbo.WidgetPropertyValues WITH CHECK ADD CONSTRAINT FK_WidgetPropertyValues_WidgetProperties FOREIGN KEY( PropertyId )
REFERENCES dbo.WidgetProperties ( PropertyId )

ALTER TABLE dbo.WidgetPropertyValues WITH CHECK ADD  CONSTRAINT FK_WidgetPropertyValues_Widgets FOREIGN KEY( WidgetId )
REFERENCES dbo.Widgets ( WidgetId )

所以你看到WidgetId, PropertyId, Revision是一个复合键,并且表存储了Values的整个历史(当前值是通过为每个WidgetId + PropertyId获取具有最大Revision数字的行来获得的。

我想知道如何将Revision 列设置为每个WidgetId + PropertyId 增加1。我想要这样的数据:

WidgetId, PropertyId, Revision, DateTime, Value
------------------------------------------------
       1           1         1               123
       1           1         2               456
       1           1         3               789
       1           2         1               012

IDENTITY 不起作用,因为它对表是全局的,同样适用于 SEQUENCE 对象。

更新我能想到一个使用INSTEAD OF INSERT触发器的可能解决方案:

CREATE TRIGGER WidgetPropertyValueInsertTrigger ON WidgetPropertyValues
    INSTEAD OF INSERT
AS
BEGIN
    DECLARE @maxRevision int
    SELECT @maxRevision = ISNULL( MAX( Revision ), 0 ) FROM WidgetPropertyValues WHERE WidgetId = INSERTED.WidgetId AND PropertyId = INSERTED.PropertyId

    INSERT INTO WidgetPropertyValues VALUES (
        INSERTED.WidgetId,
        INSERTED.PropertyId,
        @maxRevision + 1,
        INSERTED.DateTime,
        INSERTED.Value,
    )
END

(与在INSERT 操作之前或之后运行的普通INSERT-trigger 相比,INSTEAD OF INSERT 触发器在表上运行而不是任何 INSERT 操作)

我认为这将是并发安全的,因为所有INSERT 操作都有一个隐式事务,并且任何关联的触发器都在同一个事务上下文中执行,这应该意味着它是安全的。除非任何人都可以提出其他要求?

【问题讨论】:

如果您有IDENTITY,您可以使用row_number() 窗口函数根据插入顺序枚举修订。 或者使用序列来执行@ConsiderMe 建议,而不向表本身添加字段 @Dai 如果您的表中已经有数据,您需要某种列才能分配正确的修订号。将此类信息存储在多用户环境中会因为并发性而导致问题。不过,有一些解决方法,包括将正在应用修订的相关部分存储在另一个表中并获取行级锁。 @ConsiderMe 实际上,这没有实际意义。我决定使用非连续的 Revision 值并改用 IDENTITY @Dai 就触发解决方案而言,选择和插入之间存在时间间隔 - 这就是它可能中断的原因。 【参考方案1】:

您的代码存在竞争条件 - 并发事务可能会在您的 SELECT 和 INSERT 之间选择并插入相同的修订。这可能会导致并发环境中的偶尔(主要)密钥违规(迫使您重试整个事务)。

与其重试整个事务,更好的策略是仅重试 INSERT。只需将您的代码放入一个循环中,如果发生键违规(并且键违规),请增加 Revision 并重试。

类似这样的东西(从我的脑海中写出来):

DECLARE @maxRevision int = (
    SELECT
        @maxRevision = ISNULL(MAX(Revision), 0)
    FROM
        WidgetPropertyValues
    WHERE
        WidgetId = INSERTED.WidgetId
        AND PropertyId = INSERTED.PropertyId
);

WHILE 0 = 0 BEGIN

    SET @maxRevision = @maxRevision + 1;

    BEGIN TRY

        INSERT INTO WidgetPropertyValues
        VALUES (
            INSERTED.WidgetId,
            INSERTED.PropertyId,
            @maxRevision,
            INSERTED.DateTime,
            INSERTED.Value,
        );

        BREAK;

    END TRY
    BEGIN CATCH

        -- The error was different from key violation,
        -- in which case we just pass it back to caller.
        IF ERROR_NUMBER() <> 2627
            THROW;

        -- Otherwise, this was a key violation, and we can let the loop 
        -- enter the next iteration (to retry with the incremented value).

    END CATCH

END

【讨论】:

以上是关于在表复合键中增加版本号的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 如何使用 Fastlane 增加版本号?

复合键中的右联接 - Access SQL

如何增加语义版本号?

Egret版本更新(H5增加版本号)

如何在 Azure Pipelines 中自动增加 Xamarin Android 内部版本号和版本号?

如何设计APP版本号?