增加表的复合键

时间:2016-01-06 01:18:14

标签: sql-server database-design auto-increment

我在本地运行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是一个复合键,并且该表存储了值的整个历史记录(通过获取每个Revision编号最大WidgetId + PropertyId的行来获取当前值}。

我想知道如何设置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

(对于未启动的,INSTEAD OF INSERT触发器运行而不是表上的任何INSERT操作,与在INSERT之前或之后运行的正常INSERT触发器相比操作)

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

1 个答案:

答案 0 :(得分:0)

您的代码具有竞争条件 - 并发事务可能会在SELECT和INSERT之间选择并插入相同的修订版。这可能会导致并发环境中的偶然(主要)密钥冲突(强制您重试整个事务)。

不是重试整个事务,更好的策略是只重试INSERT。只需将代码置于循环中,如果发生密钥违规(并且密钥违规),请递增版本并重试。

这样的事情(从我的脑袋写):

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