我在本地运行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
操作都有一个隐式事务,并且任何关联的触发器都在同一个事务上下文中执行,这应该意味着它是安全的。除非有人可以另外声称?
答案 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